Compare commits
1367 Commits
clients-us
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
756f3f32ca | ||
|
|
362981ad19 | ||
|
|
fa4f7e4ac2 | ||
|
|
c6bca4e2ab | ||
|
|
e28b361e05 | ||
|
|
a18691011b | ||
|
|
c4a6403cba | ||
|
|
1851bf941a | ||
|
|
b7ab3c2e92 | ||
|
|
ce1ad032ba | ||
|
|
8446c68e1b | ||
|
|
40ed388b0f | ||
|
|
ce1693aa2f | ||
|
|
11d16a1552 | ||
|
|
0ac54a2c88 | ||
|
|
b7d8b32123 | ||
|
|
5987f6b2cd | ||
|
|
7ad76f5683 | ||
|
|
09a9457021 | ||
|
|
ca4643ec36 | ||
|
|
e2f78ba476 | ||
|
|
5d92190d50 | ||
|
|
2b0d6de986 | ||
|
|
057f82a561 | ||
|
|
719d2a5ffe | ||
|
|
d4bff9d5cb | ||
|
|
19fcc1f93b | ||
|
|
d45ea127c2 | ||
|
|
f591cf8601 | ||
|
|
6661a76aa8 | ||
|
|
a2ed22bfcc | ||
|
|
e370f8891a | ||
|
|
8a83e32c42 | ||
|
|
831eb6325c | ||
|
|
4d6240c987 | ||
|
|
79cf7c84dc | ||
|
|
b71f582329 | ||
|
|
8315d4b6ae | ||
|
|
b8c3cc751a | ||
|
|
d00262dc31 | ||
|
|
3debc6c8d3 | ||
|
|
5092eb58fb | ||
|
|
f0b9240575 | ||
|
|
9cf59c409e | ||
|
|
bfd5aa30a7 | ||
|
|
9737170665 | ||
|
|
922a040466 | ||
|
|
9eacefb155 | ||
|
|
33f0782f3a | ||
|
|
e6a5cef945 | ||
|
|
4c8edb80b3 | ||
|
|
d4668fae99 | ||
|
|
ddfe55e3ae | ||
|
|
761a5f1d4c | ||
|
|
1fbcad8787 | ||
|
|
aba586e605 | ||
|
|
27b21b5ad4 | ||
|
|
b6e54dab17 | ||
|
|
1f8e89772d | ||
|
|
843b13ed57 | ||
|
|
be89e5ca55 | ||
|
|
5f3657fd56 | ||
|
|
494162400e | ||
|
|
ab65bb6a8a | ||
|
|
333625f199 | ||
|
|
dbfd715381 | ||
|
|
f1d989964e | ||
|
|
b701629498 | ||
|
|
8250946325 | ||
|
|
71f63d8e6f | ||
|
|
dd5e834db0 | ||
|
|
970ecb52f0 | ||
|
|
62ea1b40e1 | ||
|
|
3b0fd5c592 | ||
|
|
b7616026dd | ||
|
|
16ad60b89a | ||
|
|
db7971d2f7 | ||
|
|
f3f8bd3125 | ||
|
|
516fd0ee8f | ||
|
|
8d6700d493 | ||
|
|
9d4ace9b3e | ||
|
|
2800655e33 | ||
|
|
91eecee11d | ||
|
|
899e5aa395 | ||
|
|
d5820c4902 | ||
|
|
a91c002274 | ||
|
|
4d142b93dd | ||
|
|
04dcf57ff3 | ||
|
|
975550c755 | ||
|
|
a964a80d85 | ||
|
|
22c3b8f116 | ||
|
|
c4b1831cfe | ||
|
|
cdb6813384 | ||
|
|
b14b68d83c | ||
|
|
3c2f930e6b | ||
|
|
ca9c7ce555 | ||
|
|
c2e95a0607 | ||
|
|
2767ee9e80 | ||
|
|
d998a8087f | ||
|
|
fdce016921 | ||
|
|
c73d70933b | ||
|
|
e9d0ad6e37 | ||
|
|
a35586f762 | ||
|
|
f527c30923 | ||
|
|
94e70219cf | ||
|
|
6496763aae | ||
|
|
a409ec269b | ||
|
|
bc7bc8da66 | ||
|
|
52484c774e | ||
|
|
4e1e0cade1 | ||
|
|
fda5904dac | ||
|
|
69ecc22318 | ||
|
|
bff9d33ee6 | ||
|
|
edf506953b | ||
|
|
5e11746549 | ||
|
|
1ae315e303 | ||
|
|
758b03ab25 | ||
|
|
e756fad573 | ||
|
|
3547450b03 | ||
|
|
733f6692c6 | ||
|
|
2d83160b16 | ||
|
|
256fa880dd | ||
|
|
b08c5f5c67 | ||
|
|
d0862a2d26 | ||
|
|
e97340ed52 | ||
|
|
e27c81eea6 | ||
|
|
7f7f3d43b2 | ||
|
|
4b1b772098 | ||
|
|
f66b88490f | ||
|
|
18f9157169 | ||
|
|
6eb82a807b | ||
|
|
bf57a97833 | ||
|
|
e9e2093220 | ||
|
|
c3540da2e3 | ||
|
|
d228cf56dd | ||
|
|
8f4cecd963 | ||
|
|
66adff44bb | ||
|
|
be41c094dc | ||
|
|
273848ca18 | ||
|
|
1e9dbead3b | ||
|
|
aeaa8ba133 | ||
|
|
24654af635 | ||
|
|
e88a21d6db | ||
|
|
bcd01badaf | ||
|
|
8e063506e0 | ||
|
|
84f5d6137a | ||
|
|
0a8565f5e8 | ||
|
|
bd8da25a46 | ||
|
|
a841f588dd | ||
|
|
75a4362ce3 | ||
|
|
e763e001e5 | ||
|
|
69475a0ae7 | ||
|
|
53e14c2ad7 | ||
|
|
1edc33148a | ||
|
|
a4cbfc74e4 | ||
|
|
c0d25aeb02 | ||
|
|
40f49bf6da | ||
|
|
0bfce87dc6 | ||
|
|
2a0655e9de | ||
|
|
a86cfa5934 | ||
|
|
54b77523c5 | ||
|
|
ba06c8928d | ||
|
|
c8a4ac1ed4 | ||
|
|
143acbae48 | ||
|
|
937f6fdae8 | ||
|
|
ba7239ac08 | ||
|
|
2e748274c0 | ||
|
|
eab2750953 | ||
|
|
17b6cb0c73 | ||
|
|
98a4c453c1 | ||
|
|
6475dceab9 | ||
|
|
040a945774 | ||
|
|
47743a5fa8 | ||
|
|
d47d6de985 | ||
|
|
37818b8594 | ||
|
|
3b184acddd | ||
|
|
9c80404d17 | ||
|
|
aaa7082f9d | ||
|
|
a45b45b2ce | ||
|
|
e4bfbd267e | ||
|
|
65b4dcc672 | ||
|
|
36fc30b524 | ||
|
|
e724ed9137 | ||
|
|
7ca992af05 | ||
|
|
37f1c714ac | ||
|
|
397a43fb60 | ||
|
|
45e0a648c6 | ||
|
|
7336aa81d9 | ||
|
|
d727c10d98 | ||
|
|
321d77a317 | ||
|
|
19b8a6b737 | ||
|
|
f2e69dfb96 | ||
|
|
8207e49317 | ||
|
|
b75600b9ea | ||
|
|
7b01f1bef6 | ||
|
|
e7bd2c0001 | ||
|
|
a26076e9db | ||
|
|
9711a0fb8e | ||
|
|
accc670411 | ||
|
|
071c41a54f | ||
|
|
35ba6c19c3 | ||
|
|
14c8348166 | ||
|
|
7d6ee72025 | ||
|
|
ea0e770b57 | ||
|
|
193b7ff21e | ||
|
|
d814ad9f3e | ||
|
|
da8b620c75 | ||
|
|
911b5e6814 | ||
|
|
f991fd9c71 | ||
|
|
652e4c922d | ||
|
|
4364e3fbc1 | ||
|
|
a783fdecbc | ||
|
|
16f67455a2 | ||
|
|
0850a28d20 | ||
|
|
5ca598139e | ||
|
|
df1bf09163 | ||
|
|
50bc8d3e9c | ||
|
|
86d089024e | ||
|
|
d5c1cf594d | ||
|
|
a0b5731e69 | ||
|
|
ceb359d614 | ||
|
|
a49a9f8e3b | ||
|
|
766606b08d | ||
|
|
fed56c1959 | ||
|
|
ae6ed8ad97 | ||
|
|
c1ca0b8e2c | ||
|
|
569dc735ce | ||
|
|
dd11c2c871 | ||
|
|
8def4a2b68 | ||
|
|
13a5f24b07 | ||
|
|
0989d6353e | ||
|
|
4139a7b73f | ||
|
|
be60d66ce3 | ||
|
|
0a33043874 | ||
|
|
96d1d983e5 | ||
|
|
7ffb260d7c | ||
|
|
ce74489df5 | ||
|
|
342b188fae | ||
|
|
fa6fee7b55 | ||
|
|
c53d5a4d7d | ||
|
|
521e905724 | ||
|
|
4623090050 | ||
|
|
dd9e5cc541 | ||
|
|
626be6a347 | ||
|
|
56327ed503 | ||
|
|
6d1665004b | ||
|
|
59b8119fbd | ||
|
|
9ff863db5e | ||
|
|
e2ac6e6d4d | ||
|
|
df4101875a | ||
|
|
3f5c788d48 | ||
|
|
45cd4df6e5 | ||
|
|
94ac3ec76e | ||
|
|
af7263a0b1 | ||
|
|
035396f95c | ||
|
|
f318f6304b | ||
|
|
9d0ff472e5 | ||
|
|
d27482e812 | ||
|
|
d5b6de70da | ||
|
|
69c2212ea0 | ||
|
|
10be9bcd56 | ||
|
|
f531def0d2 | ||
|
|
ed40eae655 | ||
|
|
ba5ae6ed04 | ||
|
|
d6ade102dc | ||
|
|
0a6301697e | ||
|
|
13b4fc6725 | ||
|
|
c94d246c24 | ||
|
|
5b779ba9fe | ||
|
|
3ba2cb19a9 | ||
|
|
a095dddd01 | ||
|
|
1b5cfaa49b | ||
|
|
66f3fabbae | ||
|
|
0be8fb7931 | ||
|
|
431e6ffaae | ||
|
|
7d8185e0ee | ||
|
|
dff45748bd | ||
|
|
da514ef314 | ||
|
|
7f73cde794 | ||
|
|
b0af0d9cd5 | ||
|
|
e6464929ff | ||
|
|
122053939d | ||
|
|
8429197b07 | ||
|
|
44f2081882 | ||
|
|
300b4a3706 | ||
|
|
81ef2db7f8 | ||
|
|
c41e8be3e8 | ||
|
|
41bab0ce0b | ||
|
|
5f26b9eeea | ||
|
|
1cca69ad23 | ||
|
|
410ed3949b | ||
|
|
efc6ef3075 | ||
|
|
63f7dd1d20 | ||
|
|
57b8c69983 | ||
|
|
aad060810a | ||
|
|
9222b00a6f | ||
|
|
ff61b22e7e | ||
|
|
577cb91343 | ||
|
|
1889386f64 | ||
|
|
5d7f082ebf | ||
|
|
db6327c4ff | ||
|
|
fd7f6b2b99 | ||
|
|
49435398a8 | ||
|
|
e101ac341b | ||
|
|
6cfc7b7c69 | ||
|
|
313acabc86 | ||
|
|
34cced872f | ||
|
|
ac09e3aaf9 | ||
|
|
9f2fd34e99 | ||
|
|
67b63d3084 | ||
|
|
4a31a7b84b | ||
|
|
538b601b1e | ||
|
|
588f064c25 | ||
|
|
d521e79662 | ||
|
|
ccddb9244d | ||
|
|
0547396213 | ||
|
|
6c85171091 | ||
|
|
a8f6b6c1da | ||
|
|
f899326189 | ||
|
|
0f4d1d2a74 | ||
|
|
941d5c08e3 | ||
|
|
db9f74158b | ||
|
|
b4c01349d1 | ||
|
|
165bbd3584 | ||
|
|
ffb253e0e9 | ||
|
|
e5e9fe456f | ||
|
|
c63589b204 | ||
|
|
11408c2656 | ||
|
|
7d4aed8819 | ||
|
|
609ffccd67 | ||
|
|
508369a59d | ||
|
|
748af1d8cb | ||
|
|
26a91cd5e1 | ||
|
|
48dd4d5913 | ||
|
|
d309ec249e | ||
|
|
72d46b7352 | ||
|
|
4613aae47d | ||
|
|
1bc4480d84 | ||
|
|
b5d76f73e8 | ||
|
|
a5c7913e77 | ||
|
|
34b914f509 | ||
|
|
5a3d75ca12 | ||
|
|
158d7b23d8 | ||
|
|
67949b4968 | ||
|
|
1fc40b3017 | ||
|
|
bb1a375484 | ||
|
|
bf5dd3b0a1 | ||
|
|
e4d4c62833 | ||
|
|
20ae903d7f | ||
|
|
f5f757e4bd | ||
|
|
13c011895d | ||
|
|
bd8d0e3392 | ||
|
|
5ad564d21b | ||
|
|
8f8775cb93 | ||
|
|
37695827aa | ||
|
|
7a72d209ea | ||
|
|
cda6b67bef | ||
|
|
066305b095 | ||
|
|
f2ba4b270f | ||
|
|
89695df012 | ||
|
|
b0566d3c6f | ||
|
|
5dda8c384f | ||
|
|
b04385a340 | ||
|
|
d374ea6ea6 | ||
|
|
01a2820390 | ||
|
|
c89c1a03da | ||
|
|
873408270e | ||
|
|
8fec8f35bc | ||
|
|
141c846fe2 | ||
|
|
cb569ff14d | ||
|
|
1497469016 | ||
|
|
e356a6d33b | ||
|
|
38ac4c5980 | ||
|
|
ed3ee64e4b | ||
|
|
12aea2901d | ||
|
|
5ff56467ea | ||
|
|
3a8718a4b0 | ||
|
|
37c4a7b690 | ||
|
|
b735e7c34d | ||
|
|
5f85c3b3b8 | ||
|
|
5d9cb9fa21 | ||
|
|
643d56958d | ||
|
|
f378d6f040 | ||
|
|
bb57794388 | ||
|
|
a9ca49b8a2 | ||
|
|
c1b473294e | ||
|
|
e3e4bdfe09 | ||
|
|
bfbeace2e2 | ||
|
|
efcf46ce8a | ||
|
|
2085715965 | ||
|
|
d227db7b7b | ||
|
|
2af67ad355 | ||
|
|
f100854423 | ||
|
|
92331d7a33 | ||
|
|
9a5bcb9099 | ||
|
|
8eb6bb2a95 | ||
|
|
2aa65ccab3 | ||
|
|
be1577a3e7 | ||
|
|
c8e1b3bf29 | ||
|
|
e17b986628 | ||
|
|
5f19918ca0 | ||
|
|
2959ad0e70 | ||
|
|
a76eec7bb7 | ||
|
|
068b2a0dcd | ||
|
|
316b7e5653 | ||
|
|
00fc1da33c | ||
|
|
9ef93df54f | ||
|
|
fd9fdf6399 | ||
|
|
8fa1701e06 | ||
|
|
4abe83f8a9 | ||
|
|
0a7564acb6 | ||
|
|
db0f7cfbae | ||
|
|
1724885371 | ||
|
|
a97e9ea8b1 | ||
|
|
9d30e97526 | ||
|
|
b91330a27a | ||
|
|
744bc9ebe9 | ||
|
|
89ed9e6d7f | ||
|
|
b007e7f54a | ||
|
|
6651a6df42 | ||
|
|
3f29b165aa | ||
|
|
b13b91face | ||
|
|
63c14fe2d5 | ||
|
|
14e74ed02d | ||
|
|
7e30750618 | ||
|
|
4d1dd16be5 | ||
|
|
fa49cf5eba | ||
|
|
26b39fc1c6 | ||
|
|
0d36e368ea | ||
|
|
859f265c68 | ||
|
|
3219f520ba | ||
|
|
97e27b6caf | ||
|
|
09da83a72b | ||
|
|
d13b210e2f | ||
|
|
09fb672718 | ||
|
|
9797ad0e17 | ||
|
|
8b3d61ac36 | ||
|
|
7161c9547a | ||
|
|
60d4362a87 | ||
|
|
1836e0c8fc | ||
|
|
d3344aeb34 | ||
|
|
cfeb093fa6 | ||
|
|
a469b3ffcc | ||
|
|
14b3a3fdd8 | ||
|
|
94367ce387 | ||
|
|
5be518aa50 | ||
|
|
d059a8da9e | ||
|
|
1dcacbef7a | ||
|
|
a25edeccf7 | ||
|
|
315f73c77d | ||
|
|
666288fccc | ||
|
|
0ccf61c2a9 | ||
|
|
c16b1b27a3 | ||
|
|
ed9ba60be6 | ||
|
|
24d047e3d8 | ||
|
|
9671079ffb | ||
|
|
688892523c | ||
|
|
b02c341f62 | ||
|
|
3e9bcada1e | ||
|
|
93d4bd6438 | ||
|
|
5146498b33 | ||
|
|
72da4f39a8 | ||
|
|
a2b2fb804b | ||
|
|
3eac80e666 | ||
|
|
718d2122a4 | ||
|
|
310c6c90a3 | ||
|
|
9d80f62d58 | ||
|
|
77032fc989 | ||
|
|
64e6086f0c | ||
|
|
3aa58fdc8f | ||
|
|
93bc6ba615 | ||
|
|
36690d63cb | ||
|
|
9896e9799a | ||
|
|
27afc82b79 | ||
|
|
1c8f01ce7b | ||
|
|
4038ccff0d | ||
|
|
5b41bc2f59 | ||
|
|
014ba760b5 | ||
|
|
96a91ccf09 | ||
|
|
347fbd2a48 | ||
|
|
29723052ab | ||
|
|
86415d675b | ||
|
|
8fc4a0dc48 | ||
|
|
e14670cdda | ||
|
|
4d73488f0c | ||
|
|
46e62b24cf | ||
|
|
17c3041fe9 | ||
|
|
d5ae381528 | ||
|
|
e2e09527ec | ||
|
|
3ce1afbcc9 | ||
|
|
1f077d7ec2 | ||
|
|
adf3d0347b | ||
|
|
7ed8b16a53 | ||
|
|
9f7c162107 | ||
|
|
fb15f8cde6 | ||
|
|
45ecfcc6bb | ||
|
|
c6f947e470 | ||
|
|
adf5caf18a | ||
|
|
0b8068e13d | ||
|
|
f143d2e214 | ||
|
|
2e802301ae | ||
|
|
7305c721a6 | ||
|
|
b299f3d6aa | ||
|
|
e09cd6c16c | ||
|
|
b7df8b7319 | ||
|
|
c92b5942fc | ||
|
|
fe729ec762 | ||
|
|
915673798e | ||
|
|
9527fe4f26 | ||
|
|
e8a8b3f664 | ||
|
|
d6a829abc2 | ||
|
|
1a36cd0317 | ||
|
|
75005ccf81 | ||
|
|
fd6c600531 | ||
|
|
6996c2501e | ||
|
|
efbd9bdb56 | ||
|
|
0d34213647 | ||
|
|
870b85d71b | ||
|
|
86ba6b6f86 | ||
|
|
02be3cd0c4 | ||
|
|
1b756ef9a0 | ||
|
|
ceda06f9ae | ||
|
|
068eba015b | ||
|
|
7ae6b2df05 | ||
|
|
6765d5ad26 | ||
|
|
35cfd6bec9 | ||
|
|
90f66baf85 | ||
|
|
5edfed78f2 | ||
|
|
fd6a3e5a17 | ||
|
|
14a4b1b4b4 | ||
|
|
5743c0bb72 | ||
|
|
acca1b6a91 | ||
|
|
355265cd1e | ||
|
|
6ec8d143fa | ||
|
|
8ae327e8f5 | ||
|
|
c03a61f613 | ||
|
|
89928c753c | ||
|
|
a56fcc0fba | ||
|
|
43c60bcdbc | ||
|
|
a3fa12f0e4 | ||
|
|
d696556097 | ||
|
|
6a45151741 | ||
|
|
34e2fbefb9 | ||
|
|
f7cede4713 | ||
|
|
610b20c1ff | ||
|
|
fb19e10cdc | ||
|
|
2f1756ccf2 | ||
|
|
ce632a25cf | ||
|
|
ec10c37468 | ||
|
|
5ee3e140ed | ||
|
|
888f5f8bb6 | ||
|
|
9114dd5992 | ||
|
|
a126494c12 | ||
|
|
79ba804c88 | ||
|
|
e2cbe11a5f | ||
|
|
05748bf8ff | ||
|
|
f8c98bf6bf | ||
|
|
f4496bb23a | ||
|
|
c93766bb48 | ||
|
|
a1ea3f74b3 | ||
|
|
06aaa7c680 | ||
|
|
65e8bfc93e | ||
|
|
ff5e12655f | ||
|
|
1065004fa3 | ||
|
|
6d90d734f4 | ||
|
|
6c8757f230 | ||
|
|
40e37b1798 | ||
|
|
8e1fd4474f | ||
|
|
bd87585396 | ||
|
|
e9e935d6c4 | ||
|
|
2f2c2b4222 | ||
|
|
9749a272ec | ||
|
|
b76a50238e | ||
|
|
a4f3963a5a | ||
|
|
d52bd65d21 | ||
|
|
fb51f42f35 | ||
|
|
c910a715bd | ||
|
|
9040f9b82a | ||
|
|
fc0ec0d754 | ||
|
|
b3569174b6 | ||
|
|
0cae624995 | ||
|
|
cbf184342b | ||
|
|
ce123a7f1a | ||
|
|
0c5daa7173 | ||
|
|
bc20a34a49 | ||
|
|
d5b6a426a9 | ||
|
|
4c78e93143 | ||
|
|
5f184e9e5e | ||
|
|
2201b0395d | ||
|
|
51818044b1 | ||
|
|
30943010e6 | ||
|
|
dd5ca10226 | ||
|
|
a56b058858 | ||
|
|
eade72e2c6 | ||
|
|
e9bc9747b8 | ||
|
|
eb0cdda0f9 | ||
|
|
552adf3200 | ||
|
|
eba25fcc4d | ||
|
|
673cd0fcd1 | ||
|
|
b941b5571f | ||
|
|
ca026b41c0 | ||
|
|
29a683a815 | ||
|
|
69dbd20ea5 | ||
|
|
427ee026ac | ||
|
|
0a537c6830 | ||
|
|
89682a2ee4 | ||
|
|
78b00a18cc | ||
|
|
192702daf9 | ||
|
|
fcee735578 | ||
|
|
2ba49e84bb | ||
|
|
262376aa75 | ||
|
|
4c8d2266ec | ||
|
|
bb98bf03aa | ||
|
|
19c3efc9e9 | ||
|
|
7164721ee0 | ||
|
|
74b16809ec | ||
|
|
220723d25f | ||
|
|
fdb03c9626 | ||
|
|
a81bbb9192 | ||
|
|
7a4aff8e4b | ||
|
|
2810632f4a | ||
|
|
2d0dd067b8 | ||
|
|
3ab25f5ff1 | ||
|
|
39bebea5f7 | ||
|
|
57681dcd3d | ||
|
|
168ce549f7 | ||
|
|
9ec94441f3 | ||
|
|
53e7b99605 | ||
|
|
abfe476cb9 | ||
|
|
bbca200ceb | ||
|
|
cb21cab117 | ||
|
|
1f80845a7a | ||
|
|
20088ef82b | ||
|
|
1e0b1a3607 | ||
|
|
24e8455c73 | ||
|
|
e42a732e93 | ||
|
|
0f2b94307f | ||
|
|
d333cb5199 | ||
|
|
a6db4f20ad | ||
|
|
9ed9472c01 | ||
|
|
f7fcde8312 | ||
|
|
6660c850f3 | ||
|
|
8a08bdf9f0 | ||
|
|
87807e22e0 | ||
|
|
0eb39abdb4 | ||
|
|
a499ebc158 | ||
|
|
9467e6c032 | ||
|
|
9d849a0ced | ||
|
|
2ca400ab16 | ||
|
|
4183067c77 | ||
|
|
5eb4691973 | ||
|
|
d14dfbf360 | ||
|
|
493a5ad02a | ||
|
|
481beff028 | ||
|
|
f1f7e438b4 | ||
|
|
00f84c9d8e | ||
|
|
f75b9c6c86 | ||
|
|
31bc6d5773 | ||
|
|
51dc1450d3 | ||
|
|
fcbea08c87 | ||
|
|
8d60a87aa1 | ||
|
|
956aa64519 | ||
|
|
fd1cb6ca23 | ||
|
|
37082ae436 | ||
|
|
bb47ca3d2e | ||
|
|
0dd3c84b24 | ||
|
|
848fca7e1b | ||
|
|
2500f99722 | ||
|
|
c7737c444f | ||
|
|
4d1a7ed69b | ||
|
|
626d5df67e | ||
|
|
e4c369deec | ||
|
|
307209e73f | ||
|
|
dc84935ee6 | ||
|
|
998c1f52ca | ||
|
|
2766758c66 | ||
|
|
258d1d82f3 | ||
|
|
46aaadb76a | ||
|
|
ea7a618810 | ||
|
|
c0e503b31f | ||
|
|
55f5a41752 | ||
|
|
b0be82be86 | ||
|
|
96a9bdb700 | ||
|
|
74e6d39c24 | ||
|
|
61dfa00222 | ||
|
|
476281db2b | ||
|
|
f32e31c73d | ||
|
|
ea72279080 | ||
|
|
16ba56af84 | ||
|
|
f13ddde988 | ||
|
|
67dc10dfe9 | ||
|
|
5fd216adc2 | ||
|
|
6f0268f6c0 | ||
|
|
2996dfb33a | ||
|
|
c92f2cd4ba | ||
|
|
8164d5c1ad | ||
|
|
d9d8d85f6e | ||
|
|
d49720703f | ||
|
|
2362a9b4dd | ||
|
|
a8265a5286 | ||
|
|
9ea7431b73 | ||
|
|
37e6f320fe | ||
|
|
c0c0d48edf | ||
|
|
284cccbe17 | ||
|
|
81a9a94264 | ||
|
|
dccf101554 | ||
|
|
a01c06bbc7 | ||
|
|
db43cf1b30 | ||
|
|
2f561b5604 | ||
|
|
5a30f036ff | ||
|
|
768b9ffd09 | ||
|
|
8732e50047 | ||
|
|
d6e0024c96 | ||
|
|
9759e86921 | ||
|
|
982c692c40 | ||
|
|
0c3ce7836c | ||
|
|
7ef86c5707 | ||
|
|
f62b88b930 | ||
|
|
03a326c841 | ||
|
|
4df4cafd70 | ||
|
|
4b9539cc6d | ||
|
|
87135c90bd | ||
|
|
853d416b2f | ||
|
|
bfd14b87bd | ||
|
|
88aba4e169 | ||
|
|
99e2fcb2e8 | ||
|
|
1f138ab68c | ||
|
|
99ded7454e | ||
|
|
f82cacac6d | ||
|
|
a548f61ea6 | ||
|
|
bfae715076 | ||
|
|
358e25b7c2 | ||
|
|
2c3fa54933 | ||
|
|
00cdd5833e | ||
|
|
52b1164e58 | ||
|
|
657bc9cdf0 | ||
|
|
ec6bcd41b0 | ||
|
|
1721cce040 | ||
|
|
e41a5ad6b0 | ||
|
|
ee1eca9e66 | ||
|
|
d049369172 | ||
|
|
6280a68d51 | ||
|
|
32054dc4f6 | ||
|
|
831c631048 | ||
|
|
e23711bcce | ||
|
|
440bff57d0 | ||
|
|
7345cc81c1 | ||
|
|
164ab26069 | ||
|
|
4b6ace80d3 | ||
|
|
653127a0f7 | ||
|
|
bf3a1e20fc | ||
|
|
d7a44e7589 | ||
|
|
6c0d583557 | ||
|
|
13f0fb25da | ||
|
|
818aca9ec8 | ||
|
|
1c7fb476b0 | ||
|
|
93843ed733 | ||
|
|
0973313703 | ||
|
|
bfbfbe8b11 | ||
|
|
8c62d9fe78 | ||
|
|
d5558f55ed | ||
|
|
a96ad6bd07 | ||
|
|
00d9482a99 | ||
|
|
0f90e2a30f | ||
|
|
3eed636404 | ||
|
|
a67f88381f | ||
|
|
808fd856d1 | ||
|
|
5b9b532458 | ||
|
|
9fba9bd6b7 | ||
|
|
c5ece144d0 | ||
|
|
b64e2e11db | ||
|
|
0ccd5714f9 | ||
|
|
e2dfc3eb20 | ||
|
|
40eeb9b7cb | ||
|
|
8fa62a0908 | ||
|
|
446eba8bc9 | ||
|
|
18579c0647 | ||
|
|
2bb94e24eb | ||
|
|
0d37e08638 | ||
|
|
ca89c5feca | ||
|
|
729c2adb3f | ||
|
|
a21f49cb02 | ||
|
|
ef697c4864 | ||
|
|
2652dea09a | ||
|
|
efa9312fca | ||
|
|
074ee70025 | ||
|
|
77117e48e3 | ||
|
|
da112d3417 | ||
|
|
ddaaf34dbd | ||
|
|
373e35324e | ||
|
|
09b2f27749 | ||
|
|
7e9f18bf24 | ||
|
|
ab3be26790 | ||
|
|
5c67a1cb12 | ||
|
|
e28ab19ed4 | ||
|
|
59f8334cfd | ||
|
|
718bec4bbc | ||
|
|
2d731cb24b | ||
|
|
1905936950 | ||
|
|
c362bc673c | ||
|
|
4da0a752ef | ||
|
|
221ee6a1c2 | ||
|
|
2e60ecec87 | ||
|
|
71386d3b05 | ||
|
|
89a7e2e4dc | ||
|
|
27440700a5 | ||
|
|
b5019cef12 | ||
|
|
7e48cbe1aa | ||
|
|
4b2c570e73 | ||
|
|
972febf0ea | ||
|
|
6060b1d60d | ||
|
|
c91b4beac5 | ||
|
|
3577b5efb9 | ||
|
|
6069b84e58 | ||
|
|
cbccea0bbc | ||
|
|
e17212c584 | ||
|
|
e38102b022 | ||
|
|
5749704cf1 | ||
|
|
f584cba6be | ||
|
|
0661a950c7 | ||
|
|
8f5ac1282a | ||
|
|
232a178e0f | ||
|
|
4f64db1d82 | ||
|
|
d91356574b | ||
|
|
bd666d46b2 | ||
|
|
881346c31a | ||
|
|
ec1b41ebbb | ||
|
|
abe7bbf068 | ||
|
|
a1d33f8103 | ||
|
|
b55386e301 | ||
|
|
59fc5713ca | ||
|
|
b9bd6433a7 | ||
|
|
6b2e77262e | ||
|
|
8904db8dd1 | ||
|
|
0da15ae1e6 | ||
|
|
0086818928 | ||
|
|
d7abf9369e | ||
|
|
5fcc7bbff4 | ||
|
|
e90d87f26d | ||
|
|
46a6d2be9e | ||
|
|
f6709c1bdf | ||
|
|
c2a721791f | ||
|
|
ec1f5eff19 | ||
|
|
10463e5b55 | ||
|
|
30fa048637 | ||
|
|
6bae89023b | ||
|
|
874aac010e | ||
|
|
465a007f2e | ||
|
|
a678a18bcf | ||
|
|
dbb0979e86 | ||
|
|
2571ade633 | ||
|
|
38bae0dc6a | ||
|
|
7409d44923 | ||
|
|
84b4f15ed4 | ||
|
|
412c4717ad | ||
|
|
88d3e76c44 | ||
|
|
8626454811 | ||
|
|
f7c0a6875c | ||
|
|
f89f3398fa | ||
|
|
32898eb5d3 | ||
|
|
9fe05b7af4 | ||
|
|
f8be34370d | ||
|
|
91bd38227e | ||
|
|
101ee581e8 | ||
|
|
8d099c51e1 | ||
|
|
939264014b | ||
|
|
bbeb4c029c | ||
|
|
59a37cc606 | ||
|
|
ce3962f97a | ||
|
|
3c20fd0a55 | ||
|
|
e320f9f16e | ||
|
|
11200b99d3 | ||
|
|
7507806aaa | ||
|
|
90c48f20e0 | ||
|
|
9e68c6c004 | ||
|
|
130c890678 | ||
|
|
5e183911e1 | ||
|
|
74479c984c | ||
|
|
1d5d856799 | ||
|
|
8ea6b0cd9e | ||
|
|
90d07f9794 | ||
|
|
e9a29e7db2 | ||
|
|
b2df8eb72e | ||
|
|
3f81b88073 | ||
|
|
dedc13ab98 | ||
|
|
2f8ecf17ed | ||
|
|
81149085fa | ||
|
|
0aa56d441e | ||
|
|
757b735d98 | ||
|
|
4af7900dae | ||
|
|
a3610b7dde | ||
|
|
af4f85a081 | ||
|
|
6a5939599c | ||
|
|
51ef859349 | ||
|
|
e477a5a1b5 | ||
|
|
4a98061a62 | ||
|
|
be20289140 | ||
|
|
3ce0cc1992 | ||
|
|
a9a0fbe244 | ||
|
|
03d1f4bbb9 | ||
|
|
9b3d066a91 | ||
|
|
ccd4f9b65c | ||
|
|
d8344988c0 | ||
|
|
bb5594ab2f | ||
|
|
19f8cda3d9 | ||
|
|
d3c4688c0f | ||
|
|
2d92111f1d | ||
|
|
b8ffc601d4 | ||
|
|
8af95ea1ca | ||
|
|
beddb0d187 | ||
|
|
bd20bb0dd1 | ||
|
|
662e63317b | ||
|
|
d82535d3e1 | ||
|
|
1d862131dd | ||
|
|
150c51c9eb | ||
|
|
8618a5c2fd | ||
|
|
795302a351 | ||
|
|
096a2bfa10 | ||
|
|
188994ce84 | ||
|
|
800bdcb277 | ||
|
|
c033fd4e8b | ||
|
|
d2fa55dd11 | ||
|
|
7e047d9e34 | ||
|
|
9d9401d2ee | ||
|
|
9a621044d8 | ||
|
|
3a6fbb67a5 | ||
|
|
c7c70fa736 | ||
|
|
eafcefbe45 | ||
|
|
8ed13b41d9 | ||
|
|
b80757a129 | ||
|
|
13ddf30781 | ||
|
|
4ecca88856 | ||
|
|
4f154d212e | ||
|
|
981d777a65 | ||
|
|
dd13758085 | ||
|
|
3d8153aeb1 | ||
|
|
ce3cb98422 | ||
|
|
ae5bdcd88b | ||
|
|
428a76d742 | ||
|
|
8d2955475b | ||
|
|
9ffa391416 | ||
|
|
1f4ebf1907 | ||
|
|
4cb5c22268 | ||
|
|
b7b65bb295 | ||
|
|
75b9703793 | ||
|
|
afc19f192b | ||
|
|
e983e1166a | ||
|
|
5587bd9d59 | ||
|
|
b5f8e8feb2 | ||
|
|
322f3bfb1d | ||
|
|
9bd66fa306 | ||
|
|
fea4d43920 | ||
|
|
009b86c33b | ||
|
|
d414617f9d | ||
|
|
1d7e55bf98 | ||
|
|
bc45e16109 | ||
|
|
a5775a0f4f | ||
|
|
4f1dc19569 | ||
|
|
1af938d7ea | ||
|
|
fc924f707c | ||
|
|
6e7ba1dc52 | ||
|
|
3e01bfef7d | ||
|
|
d8b662496b | ||
|
|
e0de003c2c | ||
|
|
6e35c182b0 | ||
|
|
2479a3c53c | ||
|
|
6b609bb078 | ||
|
|
9c21e3da16 | ||
|
|
7ccde11e3e | ||
|
|
56b0185c8f | ||
|
|
8b47b2aabe | ||
|
|
416fd914cb | ||
|
|
16653dd524 | ||
|
|
e2d3d172af | ||
|
|
137d6c2523 | ||
|
|
1a976c78ef | ||
|
|
e309a125f5 | ||
|
|
2bdb1ddb6f | ||
|
|
8ff588407c | ||
|
|
c2e06725a8 | ||
|
|
bb43e0c325 | ||
|
|
35ea01610a | ||
|
|
79eefc0ac7 | ||
|
|
3a781f9ac4 | ||
|
|
cc1e551f43 | ||
|
|
68191d5921 | ||
|
|
2b3d065650 | ||
|
|
7ae80d2cad | ||
|
|
acf08e3ef6 | ||
|
|
6f50fb8a4f | ||
|
|
a5b203af27 | ||
|
|
443b53ee37 | ||
|
|
e033c10021 | ||
|
|
ad4c44c325 | ||
|
|
4aef7ca8d5 | ||
|
|
f892acbc4c | ||
|
|
9010ed6237 | ||
|
|
9f29657570 | ||
|
|
1b13132845 | ||
|
|
553fda265c | ||
|
|
0f79826535 | ||
|
|
14438bd2b4 | ||
|
|
c4445c329f | ||
|
|
5c032ee0c3 | ||
|
|
d3d5a1c204 | ||
|
|
809bb4a7b4 | ||
|
|
e8f763a77f | ||
|
|
3ad4a76f03 | ||
|
|
b133593ea2 | ||
|
|
43fb06084f | ||
|
|
9de39dbe42 | ||
|
|
c98d61a8fb | ||
|
|
fccff9c23a | ||
|
|
e02fa7c148 | ||
|
|
a21029582e | ||
|
|
9ef7faace7 | ||
|
|
3d5ae9dd5c | ||
|
|
6072ee93fa | ||
|
|
7f7f6eeaea | ||
|
|
1b4884afd8 | ||
|
|
0c0ad7029f | ||
|
|
10f1437496 | ||
|
|
c44c1a5518 | ||
|
|
48110ccda3 | ||
|
|
e94f21bc05 | ||
|
|
65f8a414be | ||
|
|
8dad38775c | ||
|
|
0d14cb853e | ||
|
|
778e6bf623 | ||
|
|
5a960649db | ||
|
|
23a7688789 | ||
|
|
0e3b6b90b7 | ||
|
|
872bb557c2 | ||
|
|
9125a7bccb | ||
|
|
5a0a8893e8 | ||
|
|
abe76e5002 | ||
|
|
474b9a685d | ||
|
|
97631c068c | ||
|
|
98c77ad7e2 | ||
|
|
3915df3200 | ||
|
|
9b98acb553 | ||
|
|
a767a31c21 | ||
|
|
f2d4c2f83c | ||
|
|
25fed23758 | ||
|
|
5cb3fa1127 | ||
|
|
deac26bad2 | ||
|
|
c7747fd4b4 | ||
|
|
1aaad43871 | ||
|
|
143175bde7 | ||
|
|
9f55d6b20a | ||
|
|
4366ca5836 | ||
|
|
9cb95576d0 | ||
|
|
d5307adef0 | ||
|
|
3d857c3b52 | ||
|
|
a012369f83 | ||
|
|
9cee3d9c79 | ||
|
|
8257dca340 | ||
|
|
5e0a1cf9c5 | ||
|
|
b3ec9dfda2 | ||
|
|
93d4f60314 | ||
|
|
769d20cea1 | ||
|
|
124ba208de | ||
|
|
ba99614d58 | ||
|
|
27db77bca4 | ||
|
|
29b924230f | ||
|
|
8eb3f6aacc | ||
|
|
7f07ccea44 | ||
|
|
c13bfc709f | ||
|
|
6fc54bcc9e | ||
|
|
5d6ee45125 | ||
|
|
fceaedfcd8 | ||
|
|
181612ce25 | ||
|
|
224b78fc64 | ||
|
|
757e540be6 | ||
|
|
bf1675686c | ||
|
|
f81909489a | ||
|
|
963468d7fa | ||
|
|
f67f4f8834 | ||
|
|
4c819d264b | ||
|
|
cbcb23ccea | ||
|
|
d8b27de5ac | ||
|
|
01f7842fd5 | ||
|
|
d409e58186 | ||
|
|
c9e1c4da1c | ||
|
|
9c38f65ad4 | ||
|
|
2316462721 | ||
|
|
7cc990107a | ||
|
|
9917a569ac | ||
|
|
aab0471b6b | ||
|
|
de684b212f | ||
|
|
fbd3802e46 | ||
|
|
4e842a660a | ||
|
|
ce6b609ca2 | ||
|
|
78369b6f6a | ||
|
|
ea43bf97c7 | ||
|
|
c56574e431 | ||
|
|
f9c0e0ec3d | ||
|
|
85986dcccb | ||
|
|
c9779254c3 | ||
|
|
5b620469c7 | ||
|
|
df4b9de334 | ||
|
|
d490cab48c | ||
|
|
b68c0962c6 | ||
|
|
ee2a438602 | ||
|
|
74dd3fdc9f | ||
|
|
314da3ee3e | ||
|
|
68cfc84249 | ||
|
|
0bcf5c2b42 | ||
|
|
9210e005e9 | ||
|
|
f245632371 | ||
|
|
6453b070bb | ||
|
|
8c4db93a93 | ||
|
|
f9b03943c3 | ||
|
|
fa839a811f | ||
|
|
88d2c2eac8 | ||
|
|
c84cc1815b | ||
|
|
2c23ffd178 | ||
|
|
da3f7ae404 | ||
|
|
f460559a4b | ||
|
|
0c9deeb2d7 | ||
|
|
1289b99f14 | ||
|
|
1a7a6e5b6f | ||
|
|
f56135eed3 | ||
|
|
23e9a61f3e | ||
|
|
5428ad1009 | ||
|
|
bba28bc5f2 | ||
|
|
18498a32ce | ||
|
|
887af85db1 | ||
|
|
a306aa971b | ||
|
|
0a9b19ecfc | ||
|
|
e011580b96 | ||
|
|
048ce850a8 | ||
|
|
2ca1f15add | ||
|
|
05ebd547b5 | ||
|
|
5a8b1383a4 | ||
|
|
ede51bebb5 | ||
|
|
fd29071d57 | ||
|
|
8e1af79dc4 | ||
|
|
dc8c28626d | ||
|
|
9db2feff77 | ||
|
|
adf76bfb53 | ||
|
|
e0a79b7d4d | ||
|
|
b63a8fd3ed | ||
|
|
ada3c6f2ef | ||
|
|
aafca7694d | ||
|
|
9ea3914a93 | ||
|
|
4345669793 | ||
|
|
1aeb31be04 | ||
|
|
66cae9802d | ||
|
|
64120ea878 | ||
|
|
0003ec021b | ||
|
|
2325e30f26 | ||
|
|
d1c98cf650 | ||
|
|
d06cd9b5be | ||
|
|
2eb440d019 | ||
|
|
4084c85c00 | ||
|
|
4fee65e5a4 | ||
|
|
17ee51249c | ||
|
|
f239c4370e | ||
|
|
c2a32a50cd | ||
|
|
7229bfa51b | ||
|
|
080e2f0a3a | ||
|
|
64e5cc172d | ||
|
|
c51a1c9c4d | ||
|
|
79958be380 | ||
|
|
05daedc6ad | ||
|
|
0234234108 | ||
|
|
f9b15b9156 | ||
|
|
37830d211d | ||
|
|
c9a1da210f | ||
|
|
ace402af2d | ||
|
|
e60dce25c9 | ||
|
|
24cdac95cd | ||
|
|
e10f7efcbe | ||
|
|
1d7f4322e3 | ||
|
|
ccfff030e5 | ||
|
|
00765c1faf | ||
|
|
f6bbdeadb9 | ||
|
|
9cf520574a | ||
|
|
e8f10b049e | ||
|
|
a3ba4fff54 | ||
|
|
eecfcd640c | ||
|
|
40c38fa070 | ||
|
|
042c88ccb8 | ||
|
|
5a60f66ae0 | ||
|
|
4d665e8596 | ||
|
|
9221bcf889 | ||
|
|
2418813902 | ||
|
|
f66a9bdd33 | ||
|
|
bc7a1f4673 | ||
|
|
9010803046 | ||
|
|
311233b9f7 | ||
|
|
38203a0e7c | ||
|
|
5e9d660e26 | ||
|
|
110e950476 | ||
|
|
f8ab5b7af7 | ||
|
|
4e7843c1f3 | ||
|
|
502d15b9dc | ||
|
|
71db29c09c | ||
|
|
8cced5011b | ||
|
|
a812dde026 | ||
|
|
58374f77c9 | ||
|
|
8df3fa0ac0 | ||
|
|
840e9914cb | ||
|
|
f30a4f3cfd | ||
|
|
27004f9d0c | ||
|
|
427638ed3d | ||
|
|
350379b0c7 | ||
|
|
cf80c9d45c | ||
|
|
2d801b8ea5 | ||
|
|
f82d01d39b | ||
|
|
e959ce1698 | ||
|
|
25e176e8d5 | ||
|
|
8df01eb13a | ||
|
|
8d87f31bec | ||
|
|
2b3594a5ea | ||
|
|
72b7c8de0c | ||
|
|
b329dbb585 | ||
|
|
56d30ad6bd | ||
|
|
e24a13fb11 | ||
|
|
d7e06161a8 | ||
|
|
8a8c0edad3 | ||
|
|
66fc8529c2 | ||
|
|
0beaadf512 | ||
|
|
58177f4a02 | ||
|
|
28725dd164 | ||
|
|
1714140ee7 | ||
|
|
6329c3d140 | ||
|
|
44113ad93a | ||
|
|
ee1af459cc | ||
|
|
69561caa74 | ||
|
|
6f03d099b8 | ||
|
|
1581b5cb74 | ||
|
|
e09ec56fad | ||
|
|
8bcad76eb5 | ||
|
|
ff4a6b1d3f | ||
|
|
07b04b2603 | ||
|
|
54471c703c | ||
|
|
8a160ec0fe | ||
|
|
15da2f130b | ||
|
|
d64d2d6916 | ||
|
|
68928843a5 | ||
|
|
1228fddb01 | ||
|
|
3fa0b01c41 | ||
|
|
a4884f90a9 | ||
|
|
d7311ad947 | ||
|
|
1aa155a0af | ||
|
|
4f1c207083 | ||
|
|
dc6ee70eba | ||
|
|
0f9f4dfaeb | ||
|
|
22941c0653 | ||
|
|
d714f7d52c | ||
|
|
4f2dd92e81 | ||
|
|
090706c816 | ||
|
|
f449fdc7ec | ||
|
|
394d1503dd | ||
|
|
60380b70ed | ||
|
|
cece7a59bf | ||
|
|
00174be8c0 | ||
|
|
1d303feca2 | ||
|
|
3f4fae8f09 | ||
|
|
dab795e94a | ||
|
|
bd2165c553 | ||
|
|
646497cda0 | ||
|
|
dbc046397b | ||
|
|
fbafb48562 | ||
|
|
ccb17cdbbf | ||
|
|
c56512dc7d | ||
|
|
a92edf519e | ||
|
|
6cd3f2df1b | ||
|
|
b9c0089fac | ||
|
|
b2f78c9149 | ||
|
|
2a361b010f | ||
|
|
7bfa732a90 | ||
|
|
c554364001 | ||
|
|
5e52c48e77 | ||
|
|
c233fc564e | ||
|
|
72bc26f0f8 | ||
|
|
151cd3e6de | ||
|
|
97489b9564 | ||
|
|
d263d282ee | ||
|
|
2ec2295cd6 | ||
|
|
d1c7832e40 | ||
|
|
313d3c72da | ||
|
|
c8ec94c307 | ||
|
|
4809b64f7d | ||
|
|
26e49ca39d | ||
|
|
bb1472d25c | ||
|
|
a0a369dc43 | ||
|
|
8ea7b2ce02 | ||
|
|
1ee70e04ed | ||
|
|
d0157ea7a5 | ||
|
|
d90f3bb6be | ||
|
|
149f4c1332 | ||
|
|
8e3b5688d5 | ||
|
|
bfd1293847 | ||
|
|
f4701f3da5 | ||
|
|
93af09ee97 | ||
|
|
897ddbec01 | ||
|
|
889b381e96 | ||
|
|
54c05c8345 | ||
|
|
8d62fb3865 | ||
|
|
6217086cd5 | ||
|
|
6fbe25e91f | ||
|
|
57b3f49819 | ||
|
|
d89f5279bf | ||
|
|
744305ab39 | ||
|
|
ba9048a377 | ||
|
|
2cdc23d63e | ||
|
|
46ed27a218 | ||
|
|
336d31ce39 | ||
|
|
8df62e8b6a | ||
|
|
3eab3b0827 | ||
|
|
fbbab60956 | ||
|
|
a8d11d78fc | ||
|
|
e16aa6e90b | ||
|
|
be4d697dfe | ||
|
|
94b34c489c | ||
|
|
ff089ec6d7 | ||
|
|
dc4f9a9bd1 | ||
|
|
e867de023a | ||
|
|
e00c3f2193 | ||
|
|
8c30995228 | ||
|
|
3ba65a3311 | ||
|
|
447b706909 | ||
|
|
c5914dc0c0 | ||
|
|
30f3ab11b2 | ||
|
|
66b01b764f | ||
|
|
ee7e7778b6 | ||
|
|
0d0c43f72b | ||
|
|
83f36bce9d | ||
|
|
2466d24c1a | ||
|
|
2f34def4d7 | ||
|
|
8e8f992876 | ||
|
|
1d9ed9d219 | ||
|
|
616fb9c8e9 | ||
|
|
a2ab7191e5 | ||
|
|
7a31292ec7 | ||
|
|
196fbbe334 | ||
|
|
5bb5aeff36 | ||
|
|
2ada05b286 | ||
|
|
87f23f582c | ||
|
|
29a52f6ac4 | ||
|
|
790f7083e2 | ||
|
|
5c851e82ff | ||
|
|
854f638da3 | ||
|
|
4842648e7b | ||
|
|
8f152bdf9f | ||
|
|
d003436179 | ||
|
|
9776ef43ea | ||
|
|
e2c4a906c4 | ||
|
|
27e8250cd1 | ||
|
|
0d84b7af6e | ||
|
|
b961271aa6 | ||
|
|
b505cc60b0 | ||
|
|
955f927c59 | ||
|
|
4beed9d464 | ||
|
|
228481444f | ||
|
|
02cd2cfb17 | ||
|
|
d218a4bbc3 | ||
|
|
4bd1c4e0c6 | ||
|
|
cfde4e7443 | ||
|
|
f58cf68f7c | ||
|
|
08e43400e4 | ||
|
|
46d60bd090 | ||
|
|
5641a2aa31 | ||
|
|
0abc561bb8 |
@@ -32,3 +32,5 @@ migrations/
|
|||||||
config/
|
config/
|
||||||
build.ts
|
build.ts
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
|
Dockerfile*
|
||||||
|
migrations/
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||||
"next/core-web-vitals",
|
|
||||||
"next/typescript"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
14
.github/dependabot.yml
vendored
@@ -44,19 +44,9 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: "daily"
|
interval: "daily"
|
||||||
groups:
|
groups:
|
||||||
dev-patch-updates:
|
patch-updates:
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
update-types:
|
||||||
- "patch"
|
- "patch"
|
||||||
dev-minor-updates:
|
minor-updates:
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
prod-patch-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
prod-minor-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
update-types:
|
||||||
- "minor"
|
- "minor"
|
||||||
476
.github/workflows/cicd.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: CI/CD Pipeline
|
name: Public CICD Pipeline
|
||||||
|
|
||||||
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
|
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
|
||||||
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
||||||
@@ -17,16 +17,42 @@ on:
|
|||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "[0-9]+.[0-9]+.[0-9]+"
|
- "[0-9]+.[0-9]+.[0-9]+"
|
||||||
- "[0-9]+.[0-9]+.[0-9]+.rc.[0-9]+"
|
- "[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.ref }}
|
group: ${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
pre-run:
|
||||||
name: Build and Release
|
runs-on: ubuntu-latest
|
||||||
runs-on: [self-hosted, linux, x64]
|
permissions: write-all
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Start EC2 instances
|
||||||
|
run: |
|
||||||
|
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||||
|
echo "EC2 instances started"
|
||||||
|
|
||||||
|
|
||||||
|
release-arm:
|
||||||
|
name: Build and Release (ARM64)
|
||||||
|
runs-on: [self-hosted, linux, arm64, us-east-1]
|
||||||
|
needs: [pre-run]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
needs.pre-run.result == 'success'
|
||||||
|
}}
|
||||||
# Job-level timeout to avoid runaway or stuck runs
|
# Job-level timeout to avoid runaway or stuck runs
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
env:
|
env:
|
||||||
@@ -36,13 +62,19 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Monitor storage space
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
run: |
|
||||||
|
THRESHOLD=75
|
||||||
- name: Set up Docker Buildx
|
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
||||||
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
echo "Used space: $USED_SPACE%"
|
||||||
|
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
||||||
|
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
||||||
|
echo y | docker system prune -a
|
||||||
|
else
|
||||||
|
echo "Storage space is above the threshold. No action needed."
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
@@ -50,13 +82,189 @@ jobs:
|
|||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract tag name
|
||||||
|
id: get-tag
|
||||||
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Update version in package.json
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||||
|
cat server/lib/consts.ts
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Check if release candidate
|
||||||
|
id: check-rc
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Build and push Docker images (Docker Hub - ARM64)
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [ "$IS_RC" = "true" ]; then
|
||||||
|
make build-rc-arm tag=$TAG
|
||||||
|
else
|
||||||
|
make build-release-arm tag=$TAG
|
||||||
|
fi
|
||||||
|
echo "Built & pushed ARM64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
release-amd:
|
||||||
|
name: Build and Release (AMD64)
|
||||||
|
runs-on: [self-hosted, linux, x64, us-east-1]
|
||||||
|
needs: [pre-run]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
needs.pre-run.result == 'success'
|
||||||
|
}}
|
||||||
|
# Job-level timeout to avoid runaway or stuck runs
|
||||||
|
timeout-minutes: 120
|
||||||
|
env:
|
||||||
|
# Target images
|
||||||
|
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
|
- name: Monitor storage space
|
||||||
|
run: |
|
||||||
|
THRESHOLD=75
|
||||||
|
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
||||||
|
echo "Used space: $USED_SPACE%"
|
||||||
|
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
||||||
|
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
||||||
|
echo y | docker system prune -a
|
||||||
|
else
|
||||||
|
echo "Storage space is above the threshold. No action needed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
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: Update version in package.json
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||||
|
cat server/lib/consts.ts
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Check if release candidate
|
||||||
|
id: check-rc
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Build and push Docker images (Docker Hub - AMD64)
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [ "$IS_RC" = "true" ]; then
|
||||||
|
make build-rc-amd tag=$TAG
|
||||||
|
else
|
||||||
|
make build-release-amd tag=$TAG
|
||||||
|
fi
|
||||||
|
echo "Built & pushed AMD64 images to: ${{ env.DOCKERHUB_IMAGE }}:${TAG}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
create-manifest:
|
||||||
|
name: Create Multi-Arch Manifests
|
||||||
|
runs-on: [self-hosted, linux, x64, us-east-1]
|
||||||
|
needs: [release-arm, release-amd]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
needs.release-arm.result == 'success' &&
|
||||||
|
needs.release-amd.result == 'success'
|
||||||
|
}}
|
||||||
|
timeout-minutes: 30
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
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: Check if release candidate
|
||||||
|
id: check-rc
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
|
echo "IS_RC=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "IS_RC=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Create multi-arch manifests
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
if [ "$IS_RC" = "true" ]; then
|
||||||
|
make create-manifests-rc tag=$TAG
|
||||||
|
else
|
||||||
|
make create-manifests tag=$TAG
|
||||||
|
fi
|
||||||
|
echo "Created multi-arch manifests for tag: ${TAG}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
sign-and-package:
|
||||||
|
name: Sign and Package
|
||||||
|
runs-on: [self-hosted, linux, x64, us-east-1]
|
||||||
|
needs: [release-arm, release-amd, create-manifest]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
needs.release-arm.result == 'success' &&
|
||||||
|
needs.release-amd.result == 'success' &&
|
||||||
|
needs.create-manifest.result == 'success'
|
||||||
|
}}
|
||||||
|
# Job-level timeout to avoid runaway or stuck runs
|
||||||
|
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@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Extract tag name
|
||||||
id: get-tag
|
id: get-tag
|
||||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
|
uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.24
|
go-version: 1.24
|
||||||
|
|
||||||
@@ -99,18 +307,11 @@ jobs:
|
|||||||
make go-build-release
|
make go-build-release
|
||||||
|
|
||||||
- name: Upload artifacts from /install/bin
|
- name: Upload artifacts from /install/bin
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||||
with:
|
with:
|
||||||
name: install-bin
|
name: install-bin
|
||||||
path: install/bin/
|
path: install/bin/
|
||||||
|
|
||||||
- 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
|
- name: Install skopeo + jq
|
||||||
# skopeo: copy/inspect images between registries
|
# skopeo: copy/inspect images between registries
|
||||||
# jq: JSON parsing tool used to extract digest values
|
# jq: JSON parsing tool used to extract digest values
|
||||||
@@ -121,21 +322,105 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to GHCR
|
||||||
|
env:
|
||||||
|
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
||||||
run: |
|
run: |
|
||||||
|
mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
|
||||||
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
skopeo login ghcr.io -u "${{ github.actor }}" -p "${{ secrets.GITHUB_TOKEN }}"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Copy tag from Docker Hub to GHCR
|
- name: Copy tags from Docker Hub to GHCR
|
||||||
# Mirror the already-built image (all architectures) to GHCR so we can sign it
|
# Mirror the already-built images (all architectures) to GHCR so we can sign them
|
||||||
|
# Wait a bit for both architectures to be available in Docker Hub manifest
|
||||||
|
env:
|
||||||
|
REGISTRY_AUTH_FILE: ${{ runner.temp }}/containers/auth.json
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
|
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
|
||||||
|
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
|
||||||
|
|
||||||
|
echo "Waiting for multi-arch manifests to be ready..."
|
||||||
|
sleep 30
|
||||||
|
|
||||||
|
# Determine if this is an RC release
|
||||||
|
IS_RC="false"
|
||||||
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
|
IS_RC="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$IS_RC" = "true" ]; then
|
||||||
|
echo "RC release detected - copying version-specific tags only"
|
||||||
|
|
||||||
|
# SQLite OSS
|
||||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
||||||
skopeo copy --all --retry-times 3 \
|
skopeo copy --all --retry-times 3 \
|
||||||
docker://$DOCKERHUB_IMAGE:$TAG \
|
docker://$DOCKERHUB_IMAGE:$TAG \
|
||||||
docker://$GHCR_IMAGE:$TAG
|
docker://$GHCR_IMAGE:$TAG
|
||||||
|
|
||||||
|
# PostgreSQL OSS
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:postgresql-$TAG \
|
||||||
|
docker://$GHCR_IMAGE:postgresql-$TAG
|
||||||
|
|
||||||
|
# SQLite Enterprise
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-${TAG}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:ee-$TAG \
|
||||||
|
docker://$GHCR_IMAGE:ee-$TAG
|
||||||
|
|
||||||
|
# PostgreSQL Enterprise
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:ee-postgresql-$TAG \
|
||||||
|
docker://$GHCR_IMAGE:ee-postgresql-$TAG
|
||||||
|
else
|
||||||
|
echo "Regular release detected - copying all tags (latest, major, minor, full version)"
|
||||||
|
|
||||||
|
# SQLite OSS - all tags
|
||||||
|
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:${TAG_SUFFIX}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:$TAG_SUFFIX \
|
||||||
|
docker://$GHCR_IMAGE:$TAG_SUFFIX
|
||||||
|
done
|
||||||
|
|
||||||
|
# PostgreSQL OSS - all tags
|
||||||
|
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:postgresql-${TAG_SUFFIX}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:postgresql-$TAG_SUFFIX \
|
||||||
|
docker://$GHCR_IMAGE:postgresql-$TAG_SUFFIX
|
||||||
|
done
|
||||||
|
|
||||||
|
# SQLite Enterprise - all tags
|
||||||
|
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-${TAG_SUFFIX}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:ee-$TAG_SUFFIX \
|
||||||
|
docker://$GHCR_IMAGE:ee-$TAG_SUFFIX
|
||||||
|
done
|
||||||
|
|
||||||
|
# PostgreSQL Enterprise - all tags
|
||||||
|
for TAG_SUFFIX in "latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"; do
|
||||||
|
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:ee-postgresql-${TAG_SUFFIX} -> ${{ env.GHCR_IMAGE }}:ee-postgresql-${TAG_SUFFIX}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_IMAGE:ee-postgresql-$TAG_SUFFIX \
|
||||||
|
docker://$GHCR_IMAGE:ee-postgresql-$TAG_SUFFIX
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "All images copied successfully to GHCR!"
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry (for cosign)
|
||||||
|
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
# cosign is used to sign and verify container images (key and keyless)
|
# cosign is used to sign and verify container images (key and keyless)
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
||||||
@@ -155,11 +440,48 @@ jobs:
|
|||||||
issuer="https://token.actions.githubusercontent.com"
|
issuer="https://token.actions.githubusercontent.com"
|
||||||
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
|
id_regex="^https://github.com/${{ github.repository }}/.+" # accept this repo (all workflows/refs)
|
||||||
|
|
||||||
for IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
# Track failures
|
||||||
echo "Processing ${IMAGE}:${TAG}"
|
FAILED_TAGS=()
|
||||||
|
SUCCESSFUL_TAGS=()
|
||||||
|
|
||||||
DIGEST="$(skopeo inspect --retry-times 3 docker://${IMAGE}:${TAG} | jq -r '.Digest')"
|
# Determine if this is an RC release
|
||||||
REF="${IMAGE}@${DIGEST}"
|
IS_RC="false"
|
||||||
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
|
IS_RC="true"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Define image variants to sign
|
||||||
|
if [ "$IS_RC" = "true" ]; then
|
||||||
|
echo "RC release - signing version-specific tags only"
|
||||||
|
IMAGE_TAGS=(
|
||||||
|
"${TAG}"
|
||||||
|
"postgresql-${TAG}"
|
||||||
|
"ee-${TAG}"
|
||||||
|
"ee-postgresql-${TAG}"
|
||||||
|
)
|
||||||
|
else
|
||||||
|
echo "Regular release - signing all tags"
|
||||||
|
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
|
||||||
|
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
|
||||||
|
IMAGE_TAGS=(
|
||||||
|
"latest" "$MAJOR_TAG" "$MINOR_TAG" "$TAG"
|
||||||
|
"postgresql-latest" "postgresql-$MAJOR_TAG" "postgresql-$MINOR_TAG" "postgresql-$TAG"
|
||||||
|
"ee-latest" "ee-$MAJOR_TAG" "ee-$MINOR_TAG" "ee-$TAG"
|
||||||
|
"ee-postgresql-latest" "ee-postgresql-$MAJOR_TAG" "ee-postgresql-$MINOR_TAG" "ee-postgresql-$TAG"
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sign each image variant for both registries
|
||||||
|
for BASE_IMAGE in "${GHCR_IMAGE}" "${DOCKERHUB_IMAGE}"; do
|
||||||
|
for IMAGE_TAG in "${IMAGE_TAGS[@]}"; do
|
||||||
|
echo "Processing ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||||
|
TAG_FAILED=false
|
||||||
|
|
||||||
|
# Wrap the entire tag processing in error handling
|
||||||
|
(
|
||||||
|
set -e
|
||||||
|
DIGEST="$(skopeo inspect --retry-times 3 docker://${BASE_IMAGE}:${IMAGE_TAG} | jq -r '.Digest')"
|
||||||
|
REF="${BASE_IMAGE}@${DIGEST}"
|
||||||
echo "Resolved digest: ${REF}"
|
echo "Resolved digest: ${REF}"
|
||||||
|
|
||||||
echo "==> cosign sign (keyless) --recursive ${REF}"
|
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||||
@@ -168,13 +490,105 @@ jobs:
|
|||||||
echo "==> cosign sign (key) --recursive ${REF}"
|
echo "==> cosign sign (key) --recursive ${REF}"
|
||||||
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
||||||
|
|
||||||
|
# Retry wrapper for verification to handle registry propagation delays
|
||||||
|
retry_verify() {
|
||||||
|
local cmd="$1"
|
||||||
|
local attempts=6
|
||||||
|
local delay=5
|
||||||
|
local i=1
|
||||||
|
until eval "$cmd"; do
|
||||||
|
if [ $i -ge $attempts ]; then
|
||||||
|
echo "Verification failed after $attempts attempts"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
echo "Verification not yet available. Retry $i/$attempts after ${delay}s..."
|
||||||
|
sleep $delay
|
||||||
|
i=$((i+1))
|
||||||
|
delay=$((delay*2))
|
||||||
|
# Cap the delay to avoid very long waits
|
||||||
|
if [ $delay -gt 60 ]; then delay=60; fi
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
echo "==> cosign verify (public key) ${REF}"
|
echo "==> cosign verify (public key) ${REF}"
|
||||||
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
if retry_verify "cosign verify --key env://COSIGN_PUBLIC_KEY '${REF}' -o text"; then
|
||||||
|
VERIFIED_INDEX=true
|
||||||
|
else
|
||||||
|
VERIFIED_INDEX=false
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> cosign verify (keyless policy) ${REF}"
|
echo "==> cosign verify (keyless policy) ${REF}"
|
||||||
cosign verify \
|
if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"; then
|
||||||
--certificate-oidc-issuer "${issuer}" \
|
VERIFIED_INDEX_KEYLESS=true
|
||||||
--certificate-identity-regexp "${id_regex}" \
|
else
|
||||||
"${REF}" -o text
|
VERIFIED_INDEX_KEYLESS=false
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if verification succeeded
|
||||||
|
if [ "${VERIFIED_INDEX}" != "true" ] && [ "${VERIFIED_INDEX_KEYLESS}" != "true" ]; then
|
||||||
|
echo "⚠️ WARNING: Verification not available for ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||||
|
echo "This may be due to registry propagation delays. Continuing anyway."
|
||||||
|
fi
|
||||||
|
) || TAG_FAILED=true
|
||||||
|
|
||||||
|
if [ "$TAG_FAILED" = "true" ]; then
|
||||||
|
echo "⚠️ WARNING: Failed to sign/verify ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||||
|
FAILED_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
|
||||||
|
else
|
||||||
|
echo "✓ Successfully signed and verified ${BASE_IMAGE}:${IMAGE_TAG}"
|
||||||
|
SUCCESSFUL_TAGS+=("${BASE_IMAGE}:${IMAGE_TAG}")
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
done
|
||||||
|
|
||||||
|
# Report summary
|
||||||
|
echo ""
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Sign and Verify Summary"
|
||||||
|
echo "=========================================="
|
||||||
|
echo "Successful: ${#SUCCESSFUL_TAGS[@]}"
|
||||||
|
echo "Failed: ${#FAILED_TAGS[@]}"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [ ${#FAILED_TAGS[@]} -gt 0 ]; then
|
||||||
|
echo "Failed tags:"
|
||||||
|
for tag in "${FAILED_TAGS[@]}"; do
|
||||||
|
echo " - $tag"
|
||||||
|
done
|
||||||
|
echo ""
|
||||||
|
echo "⚠️ WARNING: Some tags failed to sign/verify, but continuing anyway"
|
||||||
|
else
|
||||||
|
echo "✓ All images signed and verified successfully!"
|
||||||
|
fi
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
|
post-run:
|
||||||
|
needs: [pre-run, release-arm, release-amd, create-manifest, sign-and-package]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
always() &&
|
||||||
|
needs.pre-run.result == 'success' &&
|
||||||
|
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure') &&
|
||||||
|
(needs.release-amd.result == 'success' || needs.release-amd.result == 'skipped' || needs.release-amd.result == 'failure') &&
|
||||||
|
(needs.create-manifest.result == 'success' || needs.create-manifest.result == 'skipped' || needs.create-manifest.result == 'failure') &&
|
||||||
|
(needs.sign-and-package.result == 'success' || needs.sign-and-package.result == 'skipped' || needs.sign-and-package.result == 'failure')
|
||||||
|
}}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: write-all
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Stop EC2 instances
|
||||||
|
run: |
|
||||||
|
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||||
|
echo "EC2 instances stopped"
|
||||||
|
|||||||
6
.github/workflows/linting.yml
vendored
@@ -21,12 +21,12 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '24'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|||||||
2
.github/workflows/mirror.yaml
vendored
@@ -45,7 +45,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
|
skopeo list-tags --retry-times 3 docker://"${SOURCE_IMAGE}" \
|
||||||
| jq -r '.Tags[]' | sort -u > src-tags.txt
|
| jq -r '.Tags[]' | grep -v -e '-arm64' -e '-amd64' | sort -u > src-tags.txt
|
||||||
echo "Found source tags: $(wc -l < src-tags.txt)"
|
echo "Found source tags: $(wc -l < src-tags.txt)"
|
||||||
head -n 20 src-tags.txt || true
|
head -n 20 src-tags.txt || true
|
||||||
|
|
||||||
|
|||||||
39
.github/workflows/restart-runners.yml
vendored
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
name: Restart Runners
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 */7 * *'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
ec2-maintenance-prod:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: write-all
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Start EC2 instance
|
||||||
|
run: |
|
||||||
|
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||||
|
echo "EC2 instances started"
|
||||||
|
|
||||||
|
- name: Wait
|
||||||
|
run: sleep 600
|
||||||
|
|
||||||
|
- name: Stop EC2 instance
|
||||||
|
run: |
|
||||||
|
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_AMD_RUNNER }}
|
||||||
|
echo "EC2 instances stopped"
|
||||||
125
.github/workflows/saas.yml
vendored
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
name: SAAS Pipeline
|
||||||
|
|
||||||
|
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
|
||||||
|
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write # for GHCR push
|
||||||
|
id-token: write # for Cosign Keyless (OIDC) Signing
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "[0-9]+.[0-9]+.[0-9]+-s.[0-9]+"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: write-all
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Start EC2 instances
|
||||||
|
run: |
|
||||||
|
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
echo "EC2 instances started"
|
||||||
|
|
||||||
|
|
||||||
|
release-arm:
|
||||||
|
name: Build and Release (ARM64)
|
||||||
|
runs-on: [self-hosted, linux, arm64, us-east-1]
|
||||||
|
needs: [pre-run]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
needs.pre-run.result == 'success'
|
||||||
|
}}
|
||||||
|
# Job-level timeout to avoid runaway or stuck runs
|
||||||
|
timeout-minutes: 120
|
||||||
|
env:
|
||||||
|
# Target images
|
||||||
|
AWS_IMAGE: ${{ secrets.aws_account_id }}.dkr.ecr.us-east-1.amazonaws.com/${{ github.event.repository.name }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
|
- name: Monitor storage space
|
||||||
|
run: |
|
||||||
|
THRESHOLD=75
|
||||||
|
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
||||||
|
echo "Used space: $USED_SPACE%"
|
||||||
|
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
||||||
|
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
||||||
|
echo y | docker system prune -a
|
||||||
|
else
|
||||||
|
echo "Storage space is above the threshold. No action needed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Login to Amazon ECR
|
||||||
|
id: login-ecr
|
||||||
|
uses: aws-actions/amazon-ecr-login@v2
|
||||||
|
|
||||||
|
- name: Extract tag name
|
||||||
|
id: get-tag
|
||||||
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Update version in package.json
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||||
|
cat server/lib/consts.ts
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Build and push Docker images (Docker Hub - ARM64)
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
make build-saas tag=$TAG
|
||||||
|
echo "Built & pushed ARM64 images to: ${{ env.AWS_IMAGE }}:${TAG}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
post-run:
|
||||||
|
needs: [pre-run, release-arm]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
always() &&
|
||||||
|
needs.pre-run.result == 'success' &&
|
||||||
|
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure')
|
||||||
|
}}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: write-all
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v5
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Stop EC2 instances
|
||||||
|
run: |
|
||||||
|
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
echo "EC2 instances stopped"
|
||||||
2
.github/workflows/stale-bot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
|
- uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1
|
||||||
with:
|
with:
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|||||||
29
.github/workflows/test.yml
vendored
@@ -12,13 +12,14 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
|
- name: Install Node
|
||||||
|
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '24'
|
||||||
|
|
||||||
- name: Copy config file
|
- name: Copy config file
|
||||||
run: cp config/config.example.yml config/config.yml
|
run: cp config/config.example.yml config/config.yml
|
||||||
@@ -33,10 +34,10 @@ jobs:
|
|||||||
run: npm run set:oss
|
run: npm run set:oss
|
||||||
|
|
||||||
- name: Generate database migrations
|
- name: Generate database migrations
|
||||||
run: npm run db:sqlite:generate
|
run: npm run db:generate
|
||||||
|
|
||||||
- name: Apply database migrations
|
- name: Apply database migrations
|
||||||
run: npm run db:sqlite:push
|
run: npm run db:push
|
||||||
|
|
||||||
- name: Test with tsc
|
- name: Test with tsc
|
||||||
run: npx tsc --noEmit
|
run: npx tsc --noEmit
|
||||||
@@ -57,8 +58,20 @@ jobs:
|
|||||||
echo "App failed to start"
|
echo "App failed to start"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
|
build-sqlite:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Build Docker image sqlite
|
- name: Build Docker image sqlite
|
||||||
run: make build-sqlite
|
run: make dev-build-sqlite
|
||||||
|
|
||||||
|
build-postgres:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
|
||||||
|
|
||||||
- name: Build Docker image pg
|
- name: Build Docker image pg
|
||||||
run: make build-pg
|
run: make dev-build-pg
|
||||||
|
|||||||
3
.gitignore
vendored
@@ -51,3 +51,6 @@ dynamic/
|
|||||||
scratch/
|
scratch/
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
hydrateSaas.ts
|
hydrateSaas.ts
|
||||||
|
CLAUDE.md
|
||||||
|
drizzle.config.ts
|
||||||
|
server/setup/migrations.ts
|
||||||
|
|||||||
12
.prettierignore
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
.github/
|
||||||
|
bruno/
|
||||||
|
cli/
|
||||||
|
config/
|
||||||
|
messages/
|
||||||
|
next.config.mjs/
|
||||||
|
public/
|
||||||
|
tailwind.config.js/
|
||||||
|
test/
|
||||||
|
**/*.yml
|
||||||
|
**/*.yaml
|
||||||
|
**/*.md
|
||||||
3
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"recommendations": ["esbenp.prettier-vscode"]
|
||||||
|
}
|
||||||
22
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.addMissingImports.ts": "always"
|
||||||
|
},
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[jsonc]": {
|
||||||
|
"editor.defaultFormatter": "vscode.json-language-features"
|
||||||
|
},
|
||||||
|
"[javascript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescript]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[typescriptreact]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"[json]": {
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||||
|
},
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
}
|
||||||
93
Dockerfile
@@ -1,70 +1,75 @@
|
|||||||
FROM node:25-alpine AS builder
|
FROM node:24-alpine AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG BUILD=oss
|
RUN apk add --no-cache python3 make g++
|
||||||
ARG DATABASE=sqlite
|
|
||||||
|
|
||||||
RUN apk add --no-cache curl tzdata python3 make g++
|
|
||||||
|
|
||||||
# COPY package.json package-lock.json ./
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
|
FROM base AS builder-dev
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
ARG BUILD=oss
|
||||||
RUN echo "export const driver: \"pg\" | \"sqlite\" = \"$DATABASE\";" >> server/db/index.ts
|
ARG DATABASE=sqlite
|
||||||
|
|
||||||
RUN echo "export const build = \"$BUILD\" as \"saas\" | \"enterprise\" | \"oss\";" > server/build.ts
|
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \
|
||||||
|
npm run set:$DATABASE && \
|
||||||
|
npm run set:$BUILD && \
|
||||||
|
npm run db:generate && \
|
||||||
|
npm run build && \
|
||||||
|
npm run build:cli && \
|
||||||
|
test -f dist/server.mjs
|
||||||
|
|
||||||
# Copy the appropriate TypeScript configuration based on build type
|
FROM base AS builder
|
||||||
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 npm ci --omit=dev
|
||||||
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
|
FROM node:24-alpine AS runner
|
||||||
|
|
||||||
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:cli
|
|
||||||
|
|
||||||
FROM node:25-alpine AS runner
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Curl used for the health checks
|
RUN apk add --no-cache curl tzdata
|
||||||
# Python and build tools needed for better-sqlite3 native compilation
|
|
||||||
RUN apk add --no-cache curl tzdata python3 make g++
|
|
||||||
|
|
||||||
# COPY package.json package-lock.json ./
|
COPY --from=builder /app/node_modules ./node_modules
|
||||||
COPY package*.json ./
|
COPY --from=builder /app/package.json ./package.json
|
||||||
|
|
||||||
RUN npm ci --omit=dev && npm cache clean --force
|
COPY --from=builder-dev /app/.next/standalone ./
|
||||||
|
COPY --from=builder-dev /app/.next/static ./.next/static
|
||||||
COPY --from=builder /app/.next/standalone ./
|
COPY --from=builder-dev /app/dist ./dist
|
||||||
COPY --from=builder /app/.next/static ./.next/static
|
COPY --from=builder-dev /app/server/migrations ./dist/init
|
||||||
COPY --from=builder /app/dist ./dist
|
|
||||||
COPY --from=builder /app/init ./dist/init
|
|
||||||
|
|
||||||
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
||||||
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
||||||
|
|
||||||
COPY server/db/names.json ./dist/names.json
|
COPY server/db/names.json ./dist/names.json
|
||||||
|
COPY server/db/ios_models.json ./dist/ios_models.json
|
||||||
|
COPY server/db/mac_models.json ./dist/mac_models.json
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
|
|
||||||
|
# OCI Image Labels - Build Args for dynamic values
|
||||||
|
ARG VERSION="dev"
|
||||||
|
ARG REVISION=""
|
||||||
|
ARG CREATED=""
|
||||||
|
ARG LICENSE="AGPL-3.0"
|
||||||
|
|
||||||
|
# Derive title and description based on BUILD type
|
||||||
|
ARG IMAGE_TITLE="Pangolin"
|
||||||
|
ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||||
|
|
||||||
|
# OCI Image Labels
|
||||||
|
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/fosrl/pangolin" \
|
||||||
|
org.opencontainers.image.url="https://github.com/fosrl/pangolin" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.pangolin.net" \
|
||||||
|
org.opencontainers.image.vendor="Fossorial" \
|
||||||
|
org.opencontainers.image.licenses="${LICENSE}" \
|
||||||
|
org.opencontainers.image.title="${IMAGE_TITLE}" \
|
||||||
|
org.opencontainers.image.description="${IMAGE_DESCRIPTION}" \
|
||||||
|
org.opencontainers.image.version="${VERSION}" \
|
||||||
|
org.opencontainers.image.revision="${REVISION}" \
|
||||||
|
org.opencontainers.image.created="${CREATED}"
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
CMD ["npm", "run", "start"]
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
FROM node:22-alpine
|
FROM node:24-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk add --no-cache python3 make g++
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
|
|||||||
454
Makefile
@@ -1,8 +1,32 @@
|
|||||||
.PHONY: build build-pg build-release build-arm build-x86 test clean
|
.PHONY: build build-pg build-release build-release-arm build-release-amd create-manifests build-arm build-x86 test clean
|
||||||
|
|
||||||
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
major_tag := $(shell echo $(tag) | cut -d. -f1)
|
||||||
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
minor_tag := $(shell echo $(tag) | cut -d. -f1,2)
|
||||||
build-release:
|
|
||||||
|
# OCI label variables
|
||||||
|
CREATED := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
REVISION := $(shell git rev-parse HEAD 2>/dev/null || echo "unknown")
|
||||||
|
|
||||||
|
# Common OCI build args for OSS builds
|
||||||
|
OCI_ARGS_OSS = --build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$(REVISION) \
|
||||||
|
--build-arg CREATED=$(CREATED) \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||||
|
|
||||||
|
# Common OCI build args for Enterprise builds
|
||||||
|
OCI_ARGS_EE = --build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$(REVISION) \
|
||||||
|
--build-arg CREATED=$(CREATED) \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||||
|
|
||||||
|
.PHONY: build-release build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||||
|
|
||||||
|
build-release: build-sqlite build-postgresql build-ee-sqlite build-ee-postgresql
|
||||||
|
|
||||||
|
build-sqlite:
|
||||||
@if [ -z "$(tag)" ]; then \
|
@if [ -z "$(tag)" ]; then \
|
||||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
@@ -10,33 +34,55 @@ build-release:
|
|||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
$(OCI_ARGS_OSS) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:latest \
|
--tag fosrl/pangolin:latest \
|
||||||
--tag fosrl/pangolin:$(major_tag) \
|
--tag fosrl/pangolin:$(major_tag) \
|
||||||
--tag fosrl/pangolin:$(minor_tag) \
|
--tag fosrl/pangolin:$(minor_tag) \
|
||||||
--tag fosrl/pangolin:$(tag) \
|
--tag fosrl/pangolin:$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
|
build-postgresql:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
$(OCI_ARGS_OSS) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-latest \
|
--tag fosrl/pangolin:postgresql-latest \
|
||||||
--tag fosrl/pangolin:postgresql-$(major_tag) \
|
--tag fosrl/pangolin:postgresql-$(major_tag) \
|
||||||
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
--tag fosrl/pangolin:postgresql-$(minor_tag) \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
|
build-ee-sqlite:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
$(OCI_ARGS_EE) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-latest \
|
--tag fosrl/pangolin:ee-latest \
|
||||||
--tag fosrl/pangolin:ee-$(major_tag) \
|
--tag fosrl/pangolin:ee-$(major_tag) \
|
||||||
--tag fosrl/pangolin:ee-$(minor_tag) \
|
--tag fosrl/pangolin:ee-$(minor_tag) \
|
||||||
--tag fosrl/pangolin:ee-$(tag) \
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
|
build-ee-postgresql:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
$(OCI_ARGS_EE) \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-latest \
|
--tag fosrl/pangolin:ee-postgresql-latest \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(major_tag) \
|
||||||
@@ -44,47 +90,431 @@ build-release:
|
|||||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
build-rc:
|
build-saas:
|
||||||
@if [ -z "$(tag)" ]; then \
|
@if [ -z "$(tag)" ]; then \
|
||||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=saas \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag $(AWS_IMAGE):$(tag) \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
build-release-arm:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-release-arm tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||||
|
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||||
|
CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:latest-arm64 \
|
||||||
|
--tag fosrl/pangolin:$$MAJOR_TAG-arm64 \
|
||||||
|
--tag fosrl/pangolin:$$MINOR_TAG-arm64 \
|
||||||
|
--tag fosrl/pangolin:$(tag)-arm64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-latest-arm64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-arm64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$$MINOR_TAG-arm64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-latest-arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-$$MAJOR_TAG-arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-$$MINOR_TAG-arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag)-arm64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-latest-arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG-arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
build-release-amd:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-release-amd tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||||
|
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||||
|
CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:latest-amd64 \
|
||||||
|
--tag fosrl/pangolin:$$MAJOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:$$MINOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-latest-amd64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$$MINOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-latest-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-$$MAJOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-$$MINOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-latest-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$$MAJOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$$MINOR_TAG-amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
create-manifests:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make create-manifests tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@MAJOR_TAG=$$(echo $(tag) | cut -d. -f1); \
|
||||||
|
MINOR_TAG=$$(echo $(tag) | cut -d. -f1,2); \
|
||||||
|
echo "Creating multi-arch manifests for sqlite (oss)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag fosrl/pangolin:latest \
|
||||||
|
--tag fosrl/pangolin:$$MAJOR_TAG \
|
||||||
|
--tag fosrl/pangolin:$$MINOR_TAG \
|
||||||
|
--tag fosrl/pangolin:$(tag) \
|
||||||
|
fosrl/pangolin:latest-arm64 \
|
||||||
|
fosrl/pangolin:latest-amd64 && \
|
||||||
|
echo "Creating multi-arch manifests for postgresql (oss)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag fosrl/pangolin:postgresql-latest \
|
||||||
|
--tag fosrl/pangolin:postgresql-$$MAJOR_TAG \
|
||||||
|
--tag fosrl/pangolin:postgresql-$$MINOR_TAG \
|
||||||
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
|
fosrl/pangolin:postgresql-latest-arm64 \
|
||||||
|
fosrl/pangolin:postgresql-latest-amd64 && \
|
||||||
|
echo "Creating multi-arch manifests for sqlite (enterprise)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag fosrl/pangolin:ee-latest \
|
||||||
|
--tag fosrl/pangolin:ee-$$MAJOR_TAG \
|
||||||
|
--tag fosrl/pangolin:ee-$$MINOR_TAG \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
|
fosrl/pangolin:ee-latest-arm64 \
|
||||||
|
fosrl/pangolin:ee-latest-amd64 && \
|
||||||
|
echo "Creating multi-arch manifests for postgresql (enterprise)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--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) \
|
||||||
|
fosrl/pangolin:ee-postgresql-latest-arm64 \
|
||||||
|
fosrl/pangolin:ee-postgresql-latest-amd64 && \
|
||||||
|
echo "All multi-arch manifests created successfully!"
|
||||||
|
|
||||||
|
build-rc:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:$(tag) \
|
--tag fosrl/pangolin:$(tag) \
|
||||||
--push .
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=oss \
|
--build-arg BUILD=oss \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:postgresql-$(tag) \
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
--push .
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=sqlite \
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-$(tag) \
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
--push .
|
--push . && \
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--build-arg BUILD=enterprise \
|
--build-arg BUILD=enterprise \
|
||||||
--build-arg DATABASE=pg \
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
--platform linux/arm64,linux/amd64 \
|
--platform linux/arm64,linux/amd64 \
|
||||||
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
--push .
|
--push .
|
||||||
|
|
||||||
|
build-rc-arm:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-rc-arm tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:$(tag)-arm64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag)-arm64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
build-rc-amd:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make build-rc-amd tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=oss \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:postgresql-$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag)-amd64 \
|
||||||
|
--push . && \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg BUILD=enterprise \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=$(tag) \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg LICENSE="Fossorial Commercial" \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin EE" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Pangolin Enterprise Edition - Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag)-amd64 \
|
||||||
|
--push .
|
||||||
|
|
||||||
|
create-manifests-rc:
|
||||||
|
@if [ -z "$(tag)" ]; then \
|
||||||
|
echo "Error: tag is required. Usage: make create-manifests-rc tag=<tag>"; \
|
||||||
|
exit 1; \
|
||||||
|
fi
|
||||||
|
@echo "Creating multi-arch manifests for RC sqlite (oss)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag fosrl/pangolin:$(tag) \
|
||||||
|
fosrl/pangolin:$(tag)-arm64 \
|
||||||
|
fosrl/pangolin:$(tag)-amd64 && \
|
||||||
|
echo "Creating multi-arch manifests for RC postgresql (oss)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag fosrl/pangolin:postgresql-$(tag) \
|
||||||
|
fosrl/pangolin:postgresql-$(tag)-arm64 \
|
||||||
|
fosrl/pangolin:postgresql-$(tag)-amd64 && \
|
||||||
|
echo "Creating multi-arch manifests for RC sqlite (enterprise)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag fosrl/pangolin:ee-$(tag) \
|
||||||
|
fosrl/pangolin:ee-$(tag)-arm64 \
|
||||||
|
fosrl/pangolin:ee-$(tag)-amd64 && \
|
||||||
|
echo "Creating multi-arch manifests for RC postgresql (enterprise)..." && \
|
||||||
|
docker buildx imagetools create \
|
||||||
|
--tag fosrl/pangolin:ee-postgresql-$(tag) \
|
||||||
|
fosrl/pangolin:ee-postgresql-$(tag)-arm64 \
|
||||||
|
fosrl/pangolin:ee-postgresql-$(tag)-amd64 && \
|
||||||
|
echo "All RC multi-arch manifests created successfully!"
|
||||||
|
|
||||||
build-arm:
|
build-arm:
|
||||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg VERSION=dev \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/arm64 \
|
||||||
|
-t fosrl/pangolin:latest .
|
||||||
|
|
||||||
build-x86:
|
build-x86:
|
||||||
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker buildx build \
|
||||||
|
--build-arg VERSION=dev \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
--platform linux/amd64 \
|
||||||
|
-t fosrl/pangolin:latest .
|
||||||
|
|
||||||
build-sqlite:
|
dev-build-sqlite:
|
||||||
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker build \
|
||||||
|
--build-arg DATABASE=sqlite \
|
||||||
|
--build-arg VERSION=dev \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
-t fosrl/pangolin:latest .
|
||||||
|
|
||||||
build-pg:
|
dev-build-pg:
|
||||||
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
|
@CREATED=$$(date -u +"%Y-%m-%dT%H:%M:%SZ"); \
|
||||||
|
REVISION=$$(git rev-parse HEAD 2>/dev/null || echo "unknown"); \
|
||||||
|
docker build \
|
||||||
|
--build-arg DATABASE=pg \
|
||||||
|
--build-arg VERSION=dev \
|
||||||
|
--build-arg REVISION=$$REVISION \
|
||||||
|
--build-arg CREATED=$$CREATED \
|
||||||
|
--build-arg IMAGE_TITLE="Pangolin" \
|
||||||
|
--build-arg IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere" \
|
||||||
|
-t fosrl/pangolin:postgresql-latest .
|
||||||
|
|
||||||
test:
|
test:
|
||||||
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
||||||
|
|||||||
30
README.md
@@ -31,17 +31,23 @@
|
|||||||
[](https://pangolin.net/slack)
|
[](https://pangolin.net/slack)
|
||||||
[](https://hub.docker.com/r/fosrl/pangolin)
|
[](https://hub.docker.com/r/fosrl/pangolin)
|
||||||

|

|
||||||
[](https://www.youtube.com/@fossorial-app)
|
[](https://www.youtube.com/@pangolin-net)
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://docs.pangolin.net/careers/join-us">
|
||||||
|
<img src="https://img.shields.io/badge/🚀_We're_Hiring!-Join_Our_Team-brightgreen?style=for-the-badge" alt="We're Hiring!" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<strong>
|
<strong>
|
||||||
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
Start testing Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
|
||||||
</strong>
|
</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
Pangolin is a self-hosted tunneled reverse proxy server with identity and 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.
|
Pangolin is an open-source, identity-based remote access platform built on WireGuard that enables secure, seamless connectivity to private and public resources. Pangolin combines reverse proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to any private resources, all with zero-trust security and granular access control.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -60,14 +66,22 @@ Pangolin is a self-hosted tunneled reverse proxy server with identity and contex
|
|||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
Pangolin packages everything you need for seamless application access and exposure into one cohesive platform.
|
|
||||||
|
|
||||||
| <img width=500 /> | <img width=500 /> |
|
| <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> |
|
| **Connect remote networks with sites**<br /><br />Pangolin's lightweight site connectors create secure tunnels from remote networks without requiring public IP addresses or open ports. Sites make any network anywhere available for authorized access. | <img src="public/screenshots/sites.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> |
|
| **Browser-based reverse proxy access**<br /><br />Expose web applications through identity and context-aware tunneled reverse proxies. Pangolin handles routing, load balancing, health checking, and automatic SSL certificates without exposing your network directly to the internet. Users access applications through any web browser with authentication and granular access control. | <img src="public/clip.gif" 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> |
|
| **Client-based private resource access**<br /><br />Access private resources like SSH servers, databases, RDP, and entire network ranges through Pangolin clients. Intelligent NAT traversal enables connections even through restrictive firewalls, while DNS aliases provide friendly names and fast connections to resources across all your sites. | <img src="public/screenshots/private-resources.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> |
|
| **Zero-trust granular access**<br /><br />Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications and services you explicitly define, reducing security risk and attack surface. | <img src="public/screenshots/user-devices.png" width=500 /><tr></tr> |
|
||||||
|
|
||||||
|
## Download Clients
|
||||||
|
|
||||||
|
Download the Pangolin client for your platform:
|
||||||
|
|
||||||
|
- [Mac](https://pangolin.net/downloads/mac)
|
||||||
|
- [Windows](https://pangolin.net/downloads/windows)
|
||||||
|
- [Linux](https://pangolin.net/downloads/linux)
|
||||||
|
- [iOS](https://pangolin.net/downloads/ios)
|
||||||
|
- [Android](https://pangolin.net/downloads/android)
|
||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
|
|||||||
72
blueprint.py
@@ -1,72 +0,0 @@
|
|||||||
import requests
|
|
||||||
import yaml
|
|
||||||
import json
|
|
||||||
import base64
|
|
||||||
|
|
||||||
# The file path for the YAML file to be read
|
|
||||||
# You can change this to the path of your YAML file
|
|
||||||
YAML_FILE_PATH = 'blueprint.yaml'
|
|
||||||
|
|
||||||
# The API endpoint and headers from the curl request
|
|
||||||
API_URL = 'http://api.pangolin.net/v1/org/test/blueprint'
|
|
||||||
HEADERS = {
|
|
||||||
'accept': '*/*',
|
|
||||||
'Authorization': 'Bearer <your_token_here>',
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
}
|
|
||||||
|
|
||||||
def convert_and_send(file_path, url, headers):
|
|
||||||
"""
|
|
||||||
Reads a YAML file, converts its content to a JSON payload,
|
|
||||||
and sends it via a PUT request to a specified URL.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Read the YAML file content
|
|
||||||
with open(file_path, 'r') as file:
|
|
||||||
yaml_content = file.read()
|
|
||||||
|
|
||||||
# Parse the YAML string to a Python dictionary
|
|
||||||
# This will be used to ensure the YAML is valid before sending
|
|
||||||
parsed_yaml = yaml.safe_load(yaml_content)
|
|
||||||
|
|
||||||
# convert the parsed YAML to a JSON string
|
|
||||||
json_payload = json.dumps(parsed_yaml)
|
|
||||||
print("Converted JSON payload:")
|
|
||||||
print(json_payload)
|
|
||||||
|
|
||||||
# Encode the JSON string to Base64
|
|
||||||
encoded_json = base64.b64encode(json_payload.encode('utf-8')).decode('utf-8')
|
|
||||||
|
|
||||||
# Create the final payload with the base64 encoded data
|
|
||||||
final_payload = {
|
|
||||||
"blueprint": encoded_json
|
|
||||||
}
|
|
||||||
|
|
||||||
print("Sending the following Base64 encoded JSON payload:")
|
|
||||||
print(final_payload)
|
|
||||||
print("-" * 20)
|
|
||||||
|
|
||||||
# Make the PUT request with the base64 encoded payload
|
|
||||||
response = requests.put(url, headers=headers, json=final_payload)
|
|
||||||
|
|
||||||
# Print the API response for debugging
|
|
||||||
print(f"API Response Status Code: {response.status_code}")
|
|
||||||
print("API Response Content:")
|
|
||||||
print(response.text)
|
|
||||||
|
|
||||||
# Raise an exception for bad status codes (4xx or 5xx)
|
|
||||||
response.raise_for_status()
|
|
||||||
|
|
||||||
except FileNotFoundError:
|
|
||||||
print(f"Error: The file '{file_path}' was not found.")
|
|
||||||
except yaml.YAMLError as e:
|
|
||||||
print(f"Error parsing YAML file: {e}")
|
|
||||||
except requests.exceptions.RequestException as e:
|
|
||||||
print(f"An error occurred during the API request: {e}")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"An unexpected error occurred: {e}")
|
|
||||||
|
|
||||||
# Run the function
|
|
||||||
if __name__ == "__main__":
|
|
||||||
convert_and_send(YAML_FILE_PATH, API_URL, HEADERS)
|
|
||||||
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
client-resources:
|
|
||||||
client-resource-nice-id-uno:
|
|
||||||
name: this is my resource
|
|
||||||
protocol: tcp
|
|
||||||
proxy-port: 3001
|
|
||||||
hostname: localhost
|
|
||||||
internal-port: 3000
|
|
||||||
site: lively-yosemite-toad
|
|
||||||
client-resource-nice-id-duce:
|
|
||||||
name: this is my resource
|
|
||||||
protocol: udp
|
|
||||||
proxy-port: 3000
|
|
||||||
hostname: localhost
|
|
||||||
internal-port: 3000
|
|
||||||
site: lively-yosemite-toad
|
|
||||||
|
|
||||||
proxy-resources:
|
|
||||||
resource-nice-id-uno:
|
|
||||||
name: this is my resource
|
|
||||||
protocol: http
|
|
||||||
full-domain: duce.test.example.com
|
|
||||||
host-header: example.com
|
|
||||||
tls-server-name: example.com
|
|
||||||
# auth:
|
|
||||||
# pincode: 123456
|
|
||||||
# password: sadfasdfadsf
|
|
||||||
# sso-enabled: true
|
|
||||||
# sso-roles:
|
|
||||||
# - Member
|
|
||||||
# sso-users:
|
|
||||||
# - owen@pangolin.net
|
|
||||||
# whitelist-users:
|
|
||||||
# - owen@pangolin.net
|
|
||||||
# auto-login-idp: 1
|
|
||||||
headers:
|
|
||||||
- name: X-Example-Header
|
|
||||||
value: example-value
|
|
||||||
- name: X-Another-Header
|
|
||||||
value: another-value
|
|
||||||
rules:
|
|
||||||
- action: allow
|
|
||||||
match: ip
|
|
||||||
value: 1.1.1.1
|
|
||||||
- action: deny
|
|
||||||
match: cidr
|
|
||||||
value: 2.2.2.2/32
|
|
||||||
- action: pass
|
|
||||||
match: path
|
|
||||||
value: /admin
|
|
||||||
targets:
|
|
||||||
- site: lively-yosemite-toad
|
|
||||||
path: /path
|
|
||||||
pathMatchType: prefix
|
|
||||||
hostname: localhost
|
|
||||||
method: http
|
|
||||||
port: 8000
|
|
||||||
- site: slim-alpine-chipmunk
|
|
||||||
hostname: localhost
|
|
||||||
path: /yoman
|
|
||||||
pathMatchType: exact
|
|
||||||
method: http
|
|
||||||
port: 8001
|
|
||||||
resource-nice-id-duce:
|
|
||||||
name: this is other resource
|
|
||||||
protocol: tcp
|
|
||||||
proxy-port: 3000
|
|
||||||
targets:
|
|
||||||
- site: lively-yosemite-toad
|
|
||||||
hostname: localhost
|
|
||||||
port: 3000
|
|
||||||
36
cli/commands/clearExitNodes.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, exitNodes } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
type ClearExitNodesArgs = { };
|
||||||
|
|
||||||
|
export const clearExitNodes: CommandModule<
|
||||||
|
{},
|
||||||
|
ClearExitNodesArgs
|
||||||
|
> = {
|
||||||
|
command: "clear-exit-nodes",
|
||||||
|
describe:
|
||||||
|
"Clear all exit nodes from the database",
|
||||||
|
// no args
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs;
|
||||||
|
},
|
||||||
|
handler: async (argv: {}) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
console.log(`Clearing all exit nodes from the database`);
|
||||||
|
|
||||||
|
// Delete all exit nodes
|
||||||
|
const deletedCount = await db
|
||||||
|
.delete(exitNodes)
|
||||||
|
.where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)) .returning();; // delete all
|
||||||
|
|
||||||
|
console.log(`Deleted ${deletedCount.length} exit node(s) from the database`);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
36
cli/commands/clearLicenseKeys.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, licenseKey } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
|
type ClearLicenseKeysArgs = { };
|
||||||
|
|
||||||
|
export const clearLicenseKeys: CommandModule<
|
||||||
|
{},
|
||||||
|
ClearLicenseKeysArgs
|
||||||
|
> = {
|
||||||
|
command: "clear-license-keys",
|
||||||
|
describe:
|
||||||
|
"Clear all license keys from the database",
|
||||||
|
// no args
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs;
|
||||||
|
},
|
||||||
|
handler: async (argv: {}) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
console.log(`Clearing all license keys from the database`);
|
||||||
|
|
||||||
|
// Delete all license keys
|
||||||
|
const deletedCount = await db
|
||||||
|
.delete(licenseKey)
|
||||||
|
.where(eq(licenseKey.licenseKeyId, licenseKey.licenseKeyId)) .returning();; // delete all
|
||||||
|
|
||||||
|
console.log(`Deleted ${deletedCount.length} license key(s) from the database`);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
123
cli/commands/deleteClient.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, clients, olms, currentFingerprint, userClients, approvals } from "@server/db";
|
||||||
|
import { eq, and, inArray } from "drizzle-orm";
|
||||||
|
|
||||||
|
type DeleteClientArgs = {
|
||||||
|
orgId: string;
|
||||||
|
niceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteClient: CommandModule<{}, DeleteClientArgs> = {
|
||||||
|
command: "delete-client",
|
||||||
|
describe:
|
||||||
|
"Delete a client and all associated data (OLMs, current fingerprint, userClients, approvals). Snapshots are preserved.",
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs
|
||||||
|
.option("orgId", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "The organization ID"
|
||||||
|
})
|
||||||
|
.option("niceId", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "The client niceId (identifier)"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv: { orgId: string; niceId: string }) => {
|
||||||
|
try {
|
||||||
|
const { orgId, niceId } = argv;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Deleting client with orgId: ${orgId}, niceId: ${niceId}...`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the client
|
||||||
|
const [client] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(and(eq(clients.orgId, orgId), eq(clients.niceId, niceId)))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client) {
|
||||||
|
console.error(
|
||||||
|
`Error: Client with orgId "${orgId}" and niceId "${niceId}" not found.`
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientId = client.clientId;
|
||||||
|
console.log(`Found client with clientId: ${clientId}`);
|
||||||
|
|
||||||
|
// Find all OLMs associated with this client
|
||||||
|
const associatedOlms = await db
|
||||||
|
.select()
|
||||||
|
.from(olms)
|
||||||
|
.where(eq(olms.clientId, clientId));
|
||||||
|
|
||||||
|
console.log(`Found ${associatedOlms.length} OLM(s) associated with this client`);
|
||||||
|
|
||||||
|
// Delete in a transaction to ensure atomicity
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
// Delete currentFingerprint entries for the associated OLMs
|
||||||
|
// Note: We delete these explicitly before deleting OLMs to ensure
|
||||||
|
// we have control, even though cascade would handle it
|
||||||
|
let fingerprintCount = 0;
|
||||||
|
if (associatedOlms.length > 0) {
|
||||||
|
const olmIds = associatedOlms.map((olm) => olm.olmId);
|
||||||
|
const deletedFingerprints = await trx
|
||||||
|
.delete(currentFingerprint)
|
||||||
|
.where(inArray(currentFingerprint.olmId, olmIds))
|
||||||
|
.returning();
|
||||||
|
fingerprintCount = deletedFingerprints.length;
|
||||||
|
}
|
||||||
|
console.log(`Deleted ${fingerprintCount} current fingerprint(s)`);
|
||||||
|
|
||||||
|
// Delete OLMs
|
||||||
|
// Note: OLMs have onDelete: "set null" for clientId, so we need to delete them explicitly
|
||||||
|
const deletedOlms = await trx
|
||||||
|
.delete(olms)
|
||||||
|
.where(eq(olms.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted ${deletedOlms.length} OLM(s)`);
|
||||||
|
|
||||||
|
// Delete approvals
|
||||||
|
// Note: Approvals have onDelete: "cascade" but we delete explicitly for clarity
|
||||||
|
const deletedApprovals = await trx
|
||||||
|
.delete(approvals)
|
||||||
|
.where(eq(approvals.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted ${deletedApprovals.length} approval(s)`);
|
||||||
|
|
||||||
|
// Delete userClients
|
||||||
|
// Note: userClients have onDelete: "cascade" but we delete explicitly for clarity
|
||||||
|
const deletedUserClients = await trx
|
||||||
|
.delete(userClients)
|
||||||
|
.where(eq(userClients.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted ${deletedUserClients.length} userClient association(s)`);
|
||||||
|
|
||||||
|
// Finally, delete the client itself
|
||||||
|
const deletedClients = await trx
|
||||||
|
.delete(clients)
|
||||||
|
.where(eq(clients.clientId, clientId))
|
||||||
|
.returning();
|
||||||
|
console.log(`Deleted client: ${deletedClients[0]?.name || niceId}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("\nClient deletion completed successfully!");
|
||||||
|
console.log("\nSummary:");
|
||||||
|
console.log(` - Client: ${niceId} (clientId: ${clientId})`);
|
||||||
|
console.log(` - Olm(s): ${associatedOlms.length}`);
|
||||||
|
console.log(` - Current fingerprints: deleted`);
|
||||||
|
console.log(` - Approvals: deleted`);
|
||||||
|
console.log(` - UserClients: deleted`);
|
||||||
|
console.log(` - Snapshots: preserved (not deleted)`);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error deleting client:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
284
cli/commands/rotateServerSecret.ts
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, idpOidcConfig, licenseKey } from "@server/db";
|
||||||
|
import { encrypt, decrypt } from "@server/lib/crypto";
|
||||||
|
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import fs from "fs";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
type RotateServerSecretArgs = {
|
||||||
|
"old-secret": string;
|
||||||
|
"new-secret": string;
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const rotateServerSecret: CommandModule<
|
||||||
|
{},
|
||||||
|
RotateServerSecretArgs
|
||||||
|
> = {
|
||||||
|
command: "rotate-server-secret",
|
||||||
|
describe:
|
||||||
|
"Rotate the server secret by decrypting all encrypted values with the old secret and re-encrypting with a new secret",
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs
|
||||||
|
.option("old-secret", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "The current server secret (for verification)"
|
||||||
|
})
|
||||||
|
.option("new-secret", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "The new server secret to use"
|
||||||
|
})
|
||||||
|
.option("force", {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
describe:
|
||||||
|
"Force rotation even if the old secret doesn't match the config file. " +
|
||||||
|
"Use this if you know the old secret is correct but the config file is out of sync. " +
|
||||||
|
"WARNING: This will attempt to decrypt all values with the provided old secret. " +
|
||||||
|
"If the old secret is incorrect, the rotation will fail or corrupt data."
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv: {
|
||||||
|
"old-secret": string;
|
||||||
|
"new-secret": string;
|
||||||
|
force?: boolean;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
// Determine which config file exists
|
||||||
|
const configPath = fs.existsSync(configFilePath1)
|
||||||
|
? configFilePath1
|
||||||
|
: fs.existsSync(configFilePath2)
|
||||||
|
? configFilePath2
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!configPath) {
|
||||||
|
console.error(
|
||||||
|
"Error: Config file not found. Expected config.yml or config.yaml in the config directory."
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read current config
|
||||||
|
const configContent = fs.readFileSync(configPath, "utf8");
|
||||||
|
const config = yaml.load(configContent) as any;
|
||||||
|
|
||||||
|
if (!config?.server?.secret) {
|
||||||
|
console.error(
|
||||||
|
"Error: No server secret found in config file. Cannot rotate."
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configSecret = config.server.secret;
|
||||||
|
const oldSecret = argv["old-secret"];
|
||||||
|
const newSecret = argv["new-secret"];
|
||||||
|
const force = argv.force || false;
|
||||||
|
|
||||||
|
// Verify that the provided old secret matches the one in config
|
||||||
|
if (configSecret !== oldSecret) {
|
||||||
|
if (!force) {
|
||||||
|
console.error(
|
||||||
|
"Error: The provided old secret does not match the secret in the config file."
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"\nIf you are certain the old secret is correct and the config file is out of sync,"
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"you can use the --force flag to bypass this check."
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"\nWARNING: Using --force with an incorrect old secret will cause the rotation to fail"
|
||||||
|
);
|
||||||
|
console.error(
|
||||||
|
"or corrupt encrypted data. Only use --force if you are absolutely certain."
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"\nWARNING: Using --force flag. Bypassing old secret verification."
|
||||||
|
);
|
||||||
|
console.warn(
|
||||||
|
"The provided old secret does not match the config file, but proceeding anyway."
|
||||||
|
);
|
||||||
|
console.warn(
|
||||||
|
"If the old secret is incorrect, this operation will fail or corrupt data.\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate new secret
|
||||||
|
if (newSecret.length < 8) {
|
||||||
|
console.error(
|
||||||
|
"Error: New secret must be at least 8 characters long"
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldSecret === newSecret) {
|
||||||
|
console.error("Error: New secret must be different from old secret");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Starting server secret rotation...");
|
||||||
|
console.log("This will decrypt and re-encrypt all encrypted values in the database.");
|
||||||
|
|
||||||
|
// Read all data first
|
||||||
|
console.log("\nReading encrypted data from database...");
|
||||||
|
const idpConfigs = await db.select().from(idpOidcConfig);
|
||||||
|
const licenseKeys = await db.select().from(licenseKey);
|
||||||
|
|
||||||
|
console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
|
||||||
|
console.log(`Found ${licenseKeys.length} license key(s)`);
|
||||||
|
|
||||||
|
// Prepare all decrypted and re-encrypted values
|
||||||
|
console.log("\nDecrypting and re-encrypting values...");
|
||||||
|
|
||||||
|
type IdpUpdate = {
|
||||||
|
idpOauthConfigId: number;
|
||||||
|
encryptedClientId: string;
|
||||||
|
encryptedClientSecret: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type LicenseKeyUpdate = {
|
||||||
|
oldLicenseKeyId: string;
|
||||||
|
newLicenseKeyId: string;
|
||||||
|
encryptedToken: string;
|
||||||
|
encryptedInstanceId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const idpUpdates: IdpUpdate[] = [];
|
||||||
|
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
|
||||||
|
|
||||||
|
// Process idpOidcConfig entries
|
||||||
|
for (const idpConfig of idpConfigs) {
|
||||||
|
try {
|
||||||
|
// Decrypt with old secret
|
||||||
|
const decryptedClientId = decrypt(idpConfig.clientId, oldSecret);
|
||||||
|
const decryptedClientSecret = decrypt(
|
||||||
|
idpConfig.clientSecret,
|
||||||
|
oldSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
// Re-encrypt with new secret
|
||||||
|
const encryptedClientId = encrypt(decryptedClientId, newSecret);
|
||||||
|
const encryptedClientSecret = encrypt(
|
||||||
|
decryptedClientSecret,
|
||||||
|
newSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
idpUpdates.push({
|
||||||
|
idpOauthConfigId: idpConfig.idpOauthConfigId,
|
||||||
|
encryptedClientId,
|
||||||
|
encryptedClientSecret
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error processing IdP config ${idpConfig.idpOauthConfigId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process licenseKey entries
|
||||||
|
for (const key of licenseKeys) {
|
||||||
|
try {
|
||||||
|
// Decrypt with old secret
|
||||||
|
const decryptedLicenseKeyId = decrypt(key.licenseKeyId, oldSecret);
|
||||||
|
const decryptedToken = decrypt(key.token, oldSecret);
|
||||||
|
const decryptedInstanceId = decrypt(key.instanceId, oldSecret);
|
||||||
|
|
||||||
|
// Re-encrypt with new secret
|
||||||
|
const encryptedLicenseKeyId = encrypt(
|
||||||
|
decryptedLicenseKeyId,
|
||||||
|
newSecret
|
||||||
|
);
|
||||||
|
const encryptedToken = encrypt(decryptedToken, newSecret);
|
||||||
|
const encryptedInstanceId = encrypt(
|
||||||
|
decryptedInstanceId,
|
||||||
|
newSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
licenseKeyUpdates.push({
|
||||||
|
oldLicenseKeyId: key.licenseKeyId,
|
||||||
|
newLicenseKeyId: encryptedLicenseKeyId,
|
||||||
|
encryptedToken,
|
||||||
|
encryptedInstanceId
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Error processing license key ${key.licenseKeyId}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform all database updates in a single transaction
|
||||||
|
console.log("\nUpdating database in transaction...");
|
||||||
|
await db.transaction(async (trx) => {
|
||||||
|
// Update idpOidcConfig entries
|
||||||
|
for (const update of idpUpdates) {
|
||||||
|
await trx
|
||||||
|
.update(idpOidcConfig)
|
||||||
|
.set({
|
||||||
|
clientId: update.encryptedClientId,
|
||||||
|
clientSecret: update.encryptedClientSecret
|
||||||
|
})
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
idpOidcConfig.idpOauthConfigId,
|
||||||
|
update.idpOauthConfigId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update licenseKey entries (delete old, insert new)
|
||||||
|
for (const update of licenseKeyUpdates) {
|
||||||
|
// Delete old entry
|
||||||
|
await trx
|
||||||
|
.delete(licenseKey)
|
||||||
|
.where(eq(licenseKey.licenseKeyId, update.oldLicenseKeyId));
|
||||||
|
|
||||||
|
// Insert new entry with re-encrypted values
|
||||||
|
await trx.insert(licenseKey).values({
|
||||||
|
licenseKeyId: update.newLicenseKeyId,
|
||||||
|
token: update.encryptedToken,
|
||||||
|
instanceId: update.encryptedInstanceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
|
||||||
|
console.log(`Rotated ${licenseKeyUpdates.length} license key(s)`);
|
||||||
|
|
||||||
|
// Update config file with new secret
|
||||||
|
console.log("\nUpdating config file...");
|
||||||
|
config.server.secret = newSecret;
|
||||||
|
const newConfigContent = yaml.dump(config, {
|
||||||
|
indent: 2,
|
||||||
|
lineWidth: -1
|
||||||
|
});
|
||||||
|
fs.writeFileSync(configPath, newConfigContent, "utf8");
|
||||||
|
|
||||||
|
console.log(`Updated config file: ${configPath}`);
|
||||||
|
|
||||||
|
console.log("\nServer secret rotation completed successfully!");
|
||||||
|
console.log(`\nSummary:`);
|
||||||
|
console.log(` - OIDC IdP configurations: ${idpUpdates.length}`);
|
||||||
|
console.log(` - License keys: ${licenseKeyUpdates.length}`);
|
||||||
|
console.log(
|
||||||
|
`\n IMPORTANT: Restart the server for the new secret to take effect.`
|
||||||
|
);
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error rotating server secret:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -4,10 +4,18 @@ import yargs from "yargs";
|
|||||||
import { hideBin } from "yargs/helpers";
|
import { hideBin } from "yargs/helpers";
|
||||||
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
|
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
|
||||||
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
||||||
|
import { clearExitNodes } from "./commands/clearExitNodes";
|
||||||
|
import { rotateServerSecret } from "./commands/rotateServerSecret";
|
||||||
|
import { clearLicenseKeys } from "./commands/clearLicenseKeys";
|
||||||
|
import { deleteClient } from "./commands/deleteClient";
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
yargs(hideBin(process.argv))
|
||||||
.scriptName("pangctl")
|
.scriptName("pangctl")
|
||||||
.command(setAdminCredentials)
|
.command(setAdminCredentials)
|
||||||
.command(resetUserSecurityKeys)
|
.command(resetUserSecurityKeys)
|
||||||
|
.command(clearExitNodes)
|
||||||
|
.command(rotateServerSecret)
|
||||||
|
.command(clearLicenseKeys)
|
||||||
|
.command(deleteClient)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.pangolin.net/self-host/advanced/config-file
|
# https://docs.pangolin.net/
|
||||||
|
|
||||||
|
gerbil:
|
||||||
|
start_port: 51820
|
||||||
|
base_endpoint: "{{.DashboardDomain}}"
|
||||||
|
|
||||||
app:
|
app:
|
||||||
dashboard_url: http://localhost:3002
|
dashboard_url: "https://{{.DashboardDomain}}"
|
||||||
log_level: debug
|
log_level: "info"
|
||||||
|
telemetry:
|
||||||
|
anonymous_usage: true
|
||||||
|
|
||||||
domains:
|
domains:
|
||||||
domain1:
|
domain1:
|
||||||
base_domain: example.com
|
base_domain: "{{.BaseDomain}}"
|
||||||
|
|
||||||
server:
|
server:
|
||||||
secret: my_secret_key
|
secret: "{{.Secret}}"
|
||||||
|
cors:
|
||||||
gerbil:
|
origins: ["https://{{.DashboardDomain}}"]
|
||||||
base_endpoint: example.com
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
|
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||||
orgs:
|
credentials: false
|
||||||
block_size: 24
|
|
||||||
subnet_group: 100.90.137.0/20
|
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: false
|
require_email_verification: false
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: true
|
disable_user_create_org: false
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
enable_integration_api: true
|
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
|
badger:
|
||||||
|
plugin:
|
||||||
|
badger:
|
||||||
|
disableForwardAuth: true
|
||||||
redirect-to-https:
|
redirect-to-https:
|
||||||
redirectScheme:
|
redirectScheme:
|
||||||
scheme: https
|
scheme: https
|
||||||
@@ -13,14 +17,16 @@ http:
|
|||||||
- web
|
- web
|
||||||
middlewares:
|
middlewares:
|
||||||
- redirect-to-https
|
- redirect-to-https
|
||||||
|
- badger
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
# Next.js router (handles everything except API and WebSocket paths)
|
||||||
next-router:
|
next-router:
|
||||||
rule: "Host(`{{.DashboardDomain}}`)"
|
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
||||||
service: next-service
|
service: next-service
|
||||||
priority: 10
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -28,9 +34,10 @@ http:
|
|||||||
api-router:
|
api-router:
|
||||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
||||||
service: api-service
|
service: api-service
|
||||||
priority: 100
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -44,3 +51,12 @@ http:
|
|||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://pangolin:3000" # API/WebSocket server
|
- url: "http://pangolin:3000" # API/WebSocket server
|
||||||
|
|
||||||
|
tcp:
|
||||||
|
serversTransports:
|
||||||
|
pp-transport-v1:
|
||||||
|
proxyProtocol:
|
||||||
|
version: 1
|
||||||
|
pp-transport-v2:
|
||||||
|
proxyProtocol:
|
||||||
|
version: 2
|
||||||
|
|||||||
@@ -3,32 +3,52 @@ api:
|
|||||||
dashboard: true
|
dashboard: true
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
|
http:
|
||||||
|
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||||
|
pollInterval: "5s"
|
||||||
file:
|
file:
|
||||||
directory: "/var/dynamic"
|
filename: "/etc/traefik/dynamic_config.yml"
|
||||||
watch: true
|
|
||||||
|
|
||||||
experimental:
|
experimental:
|
||||||
plugins:
|
plugins:
|
||||||
badger:
|
badger:
|
||||||
moduleName: "github.com/fosrl/badger"
|
moduleName: "github.com/fosrl/badger"
|
||||||
version: "v1.2.0"
|
version: "{{.BadgerVersion}}"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: "DEBUG"
|
level: "INFO"
|
||||||
format: "common"
|
format: "common"
|
||||||
maxSize: 100
|
maxSize: 100
|
||||||
maxBackups: 3
|
maxBackups: 3
|
||||||
maxAge: 3
|
maxAge: 3
|
||||||
compress: true
|
compress: true
|
||||||
|
|
||||||
|
certificatesResolvers:
|
||||||
|
letsencrypt:
|
||||||
|
acme:
|
||||||
|
httpChallenge:
|
||||||
|
entryPoint: web
|
||||||
|
email: "{{.LetsEncryptEmail}}"
|
||||||
|
storage: "/letsencrypt/acme.json"
|
||||||
|
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
web:
|
web:
|
||||||
address: ":80"
|
address: ":80"
|
||||||
websecure:
|
websecure:
|
||||||
address: ":9443"
|
address: ":443"
|
||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
|
http:
|
||||||
|
tls:
|
||||||
|
certResolver: "letsencrypt"
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSlash: true
|
||||||
|
allowEncodedQuestionMark: true
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|
||||||
|
ping:
|
||||||
|
entryPoint: "web"
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ services:
|
|||||||
POSTGRES_DB: postgres # Default database name
|
POSTGRES_DB: postgres # Default database name
|
||||||
POSTGRES_USER: postgres # Default user
|
POSTGRES_USER: postgres # Default user
|
||||||
POSTGRES_PASSWORD: password # Default password (change for production!)
|
POSTGRES_PASSWORD: password # Default password (change for production!)
|
||||||
volumes:
|
# volumes:
|
||||||
- ./config/postgres:/var/lib/postgresql/data
|
# - ./config/postgres:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- "5432:5432" # Map host port 5432 to container port 5432
|
- "5432:5432" # Map host port 5432 to container port 5432
|
||||||
restart: no
|
restart: no
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
const schema = [
|
const schema = [path.join("server", "db", "pg", "schema")];
|
||||||
path.join("server", "db", "pg", "schema"),
|
|
||||||
];
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "postgresql",
|
dialect: "postgresql",
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { APP_PATH } from "@server/lib/consts";
|
|||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
const schema = [
|
const schema = [path.join("server", "db", "sqlite", "schema")];
|
||||||
path.join("server", "db", "sqlite", "schema"),
|
|
||||||
];
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "sqlite",
|
dialect: "sqlite",
|
||||||
|
|||||||
101
esbuild.mjs
@@ -6,6 +6,12 @@ import path from "path";
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
// import { glob } from "glob";
|
// import { glob } from "glob";
|
||||||
|
|
||||||
|
// Read default build type from server/build.ts
|
||||||
|
let build = "oss";
|
||||||
|
const buildFile = fs.readFileSync(path.resolve("server/build.ts"), "utf8");
|
||||||
|
const m = buildFile.match(/export\s+const\s+build\s*=\s*["'](oss|saas|enterprise)["']/);
|
||||||
|
if (m) build = m[1];
|
||||||
|
|
||||||
const banner = `
|
const banner = `
|
||||||
// patch __dirname
|
// patch __dirname
|
||||||
// import { fileURLToPath } from "url";
|
// import { fileURLToPath } from "url";
|
||||||
@@ -24,20 +30,20 @@ const argv = yargs(hideBin(process.argv))
|
|||||||
alias: "e",
|
alias: "e",
|
||||||
describe: "Entry point file",
|
describe: "Entry point file",
|
||||||
type: "string",
|
type: "string",
|
||||||
demandOption: true,
|
demandOption: true
|
||||||
})
|
})
|
||||||
.option("out", {
|
.option("out", {
|
||||||
alias: "o",
|
alias: "o",
|
||||||
describe: "Output file path",
|
describe: "Output file path",
|
||||||
type: "string",
|
type: "string",
|
||||||
demandOption: true,
|
demandOption: true
|
||||||
})
|
})
|
||||||
.option("build", {
|
.option("build", {
|
||||||
alias: "b",
|
alias: "b",
|
||||||
describe: "Build type (oss, saas, enterprise)",
|
describe: "Build type (oss, saas, enterprise)",
|
||||||
type: "string",
|
type: "string",
|
||||||
choices: ["oss", "saas", "enterprise"],
|
choices: ["oss", "saas", "enterprise"],
|
||||||
default: "oss",
|
default: build
|
||||||
})
|
})
|
||||||
.help()
|
.help()
|
||||||
.alias("help", "h").argv;
|
.alias("help", "h").argv;
|
||||||
@@ -66,7 +72,9 @@ function privateImportGuardPlugin() {
|
|||||||
|
|
||||||
// Check if the importing file is NOT in server/private
|
// Check if the importing file is NOT in server/private
|
||||||
const normalizedImporter = path.normalize(importingFile);
|
const normalizedImporter = path.normalize(importingFile);
|
||||||
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
const isInServerPrivate = normalizedImporter.includes(
|
||||||
|
path.normalize("server/private")
|
||||||
|
);
|
||||||
|
|
||||||
if (!isInServerPrivate) {
|
if (!isInServerPrivate) {
|
||||||
const violation = {
|
const violation = {
|
||||||
@@ -79,8 +87,8 @@ function privateImportGuardPlugin() {
|
|||||||
console.log(`PRIVATE IMPORT VIOLATION:`);
|
console.log(`PRIVATE IMPORT VIOLATION:`);
|
||||||
console.log(` File: ${importingFile}`);
|
console.log(` File: ${importingFile}`);
|
||||||
console.log(` Import: ${args.path}`);
|
console.log(` Import: ${args.path}`);
|
||||||
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
console.log(` Resolve dir: ${args.resolveDir || "N/A"}`);
|
||||||
console.log('');
|
console.log("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return null to let the default resolver handle it
|
// Return null to let the default resolver handle it
|
||||||
@@ -89,16 +97,20 @@ function privateImportGuardPlugin() {
|
|||||||
|
|
||||||
build.onEnd((result) => {
|
build.onEnd((result) => {
|
||||||
if (violations.length > 0) {
|
if (violations.length > 0) {
|
||||||
console.log(`\nSUMMARY: Found ${violations.length} private import violation(s):`);
|
console.log(
|
||||||
|
`\nSUMMARY: Found ${violations.length} private import violation(s):`
|
||||||
|
);
|
||||||
violations.forEach((v, i) => {
|
violations.forEach((v, i) => {
|
||||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
console.log(
|
||||||
|
` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
console.log('');
|
console.log("");
|
||||||
|
|
||||||
result.errors.push({
|
result.errors.push({
|
||||||
text: `Private import violations detected: ${violations.length} violation(s) found`,
|
text: `Private import violations detected: ${violations.length} violation(s) found`,
|
||||||
location: null,
|
location: null,
|
||||||
notes: violations.map(v => ({
|
notes: violations.map((v) => ({
|
||||||
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||||
location: null
|
location: null
|
||||||
}))
|
}))
|
||||||
@@ -121,7 +133,9 @@ function dynamicImportGuardPlugin() {
|
|||||||
|
|
||||||
// Check if the importing file is NOT in server/private
|
// Check if the importing file is NOT in server/private
|
||||||
const normalizedImporter = path.normalize(importingFile);
|
const normalizedImporter = path.normalize(importingFile);
|
||||||
const isInServerPrivate = normalizedImporter.includes(path.normalize("server/private"));
|
const isInServerPrivate = normalizedImporter.includes(
|
||||||
|
path.normalize("server/private")
|
||||||
|
);
|
||||||
|
|
||||||
if (isInServerPrivate) {
|
if (isInServerPrivate) {
|
||||||
const violation = {
|
const violation = {
|
||||||
@@ -134,8 +148,8 @@ function dynamicImportGuardPlugin() {
|
|||||||
console.log(`DYNAMIC IMPORT VIOLATION:`);
|
console.log(`DYNAMIC IMPORT VIOLATION:`);
|
||||||
console.log(` File: ${importingFile}`);
|
console.log(` File: ${importingFile}`);
|
||||||
console.log(` Import: ${args.path}`);
|
console.log(` Import: ${args.path}`);
|
||||||
console.log(` Resolve dir: ${args.resolveDir || 'N/A'}`);
|
console.log(` Resolve dir: ${args.resolveDir || "N/A"}`);
|
||||||
console.log('');
|
console.log("");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return null to let the default resolver handle it
|
// Return null to let the default resolver handle it
|
||||||
@@ -144,16 +158,20 @@ function dynamicImportGuardPlugin() {
|
|||||||
|
|
||||||
build.onEnd((result) => {
|
build.onEnd((result) => {
|
||||||
if (violations.length > 0) {
|
if (violations.length > 0) {
|
||||||
console.log(`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`);
|
console.log(
|
||||||
|
`\nSUMMARY: Found ${violations.length} dynamic import violation(s):`
|
||||||
|
);
|
||||||
violations.forEach((v, i) => {
|
violations.forEach((v, i) => {
|
||||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`);
|
console.log(
|
||||||
|
` ${i + 1}. ${path.relative(process.cwd(), v.file)} imports ${v.importPath}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
console.log('');
|
console.log("");
|
||||||
|
|
||||||
result.errors.push({
|
result.errors.push({
|
||||||
text: `Dynamic import violations detected: ${violations.length} violation(s) found`,
|
text: `Dynamic import violations detected: ${violations.length} violation(s) found`,
|
||||||
location: null,
|
location: null,
|
||||||
notes: violations.map(v => ({
|
notes: violations.map((v) => ({
|
||||||
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
text: `${path.relative(process.cwd(), v.file)} imports ${v.importPath}`,
|
||||||
location: null
|
location: null
|
||||||
}))
|
}))
|
||||||
@@ -172,21 +190,28 @@ function dynamicImportSwitcherPlugin(buildValue) {
|
|||||||
const switches = [];
|
const switches = [];
|
||||||
|
|
||||||
build.onStart(() => {
|
build.onStart(() => {
|
||||||
console.log(`Dynamic import switcher using build type: ${buildValue}`);
|
console.log(
|
||||||
|
`Dynamic import switcher using build type: ${buildValue}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
build.onResolve({ filter: /^#dynamic\// }, (args) => {
|
build.onResolve({ filter: /^#dynamic\// }, (args) => {
|
||||||
// Extract the path after #dynamic/
|
// Extract the path after #dynamic/
|
||||||
const dynamicPath = args.path.replace(/^#dynamic\//, '');
|
const dynamicPath = args.path.replace(/^#dynamic\//, "");
|
||||||
|
|
||||||
// Determine the replacement based on build type
|
// Determine the replacement based on build type
|
||||||
let replacement;
|
let replacement;
|
||||||
if (buildValue === "oss") {
|
if (buildValue === "oss") {
|
||||||
replacement = `#open/${dynamicPath}`;
|
replacement = `#open/${dynamicPath}`;
|
||||||
} else if (buildValue === "saas" || buildValue === "enterprise") {
|
} 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
|
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 {
|
} else {
|
||||||
console.warn(`Unknown build type '${buildValue}', defaulting to #open/`);
|
console.warn(
|
||||||
|
`Unknown build type '${buildValue}', defaulting to #open/`
|
||||||
|
);
|
||||||
replacement = `#open/${dynamicPath}`;
|
replacement = `#open/${dynamicPath}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,8 +226,10 @@ function dynamicImportSwitcherPlugin(buildValue) {
|
|||||||
console.log(`DYNAMIC IMPORT SWITCH:`);
|
console.log(`DYNAMIC IMPORT SWITCH:`);
|
||||||
console.log(` File: ${args.importer}`);
|
console.log(` File: ${args.importer}`);
|
||||||
console.log(` Original: ${args.path}`);
|
console.log(` Original: ${args.path}`);
|
||||||
console.log(` Switched to: ${replacement} (build: ${buildValue})`);
|
console.log(
|
||||||
console.log('');
|
` Switched to: ${replacement} (build: ${buildValue})`
|
||||||
|
);
|
||||||
|
console.log("");
|
||||||
|
|
||||||
// Rewrite the import path and let the normal resolution continue
|
// Rewrite the import path and let the normal resolution continue
|
||||||
return build.resolve(replacement, {
|
return build.resolve(replacement, {
|
||||||
@@ -215,12 +242,18 @@ function dynamicImportSwitcherPlugin(buildValue) {
|
|||||||
|
|
||||||
build.onEnd((result) => {
|
build.onEnd((result) => {
|
||||||
if (switches.length > 0) {
|
if (switches.length > 0) {
|
||||||
console.log(`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`);
|
console.log(
|
||||||
|
`\nDYNAMIC IMPORT SUMMARY: Switched ${switches.length} import(s) for build type '${buildValue}':`
|
||||||
|
);
|
||||||
switches.forEach((s, i) => {
|
switches.forEach((s, i) => {
|
||||||
console.log(` ${i + 1}. ${path.relative(process.cwd(), s.file)}`);
|
console.log(
|
||||||
console.log(` ${s.originalPath} → ${s.replacementPath}`);
|
` ${i + 1}. ${path.relative(process.cwd(), s.file)}`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
` ${s.originalPath} → ${s.replacementPath}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
console.log('');
|
console.log("");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -235,7 +268,7 @@ esbuild
|
|||||||
format: "esm",
|
format: "esm",
|
||||||
minify: false,
|
minify: false,
|
||||||
banner: {
|
banner: {
|
||||||
js: banner,
|
js: banner
|
||||||
},
|
},
|
||||||
platform: "node",
|
platform: "node",
|
||||||
external: ["body-parser"],
|
external: ["body-parser"],
|
||||||
@@ -244,20 +277,22 @@ esbuild
|
|||||||
dynamicImportGuardPlugin(),
|
dynamicImportGuardPlugin(),
|
||||||
dynamicImportSwitcherPlugin(argv.build),
|
dynamicImportSwitcherPlugin(argv.build),
|
||||||
nodeExternalsPlugin({
|
nodeExternalsPlugin({
|
||||||
packagePath: getPackagePaths(),
|
packagePath: getPackagePaths()
|
||||||
}),
|
})
|
||||||
],
|
],
|
||||||
sourcemap: "inline",
|
sourcemap: "inline",
|
||||||
target: "node22",
|
target: "node24"
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
// Check if there were any errors in the build result
|
// Check if there were any errors in the build result
|
||||||
if (result.errors && result.errors.length > 0) {
|
if (result.errors && result.errors.length > 0) {
|
||||||
console.error(`Build failed with ${result.errors.length} error(s):`);
|
console.error(
|
||||||
|
`Build failed with ${result.errors.length} error(s):`
|
||||||
|
);
|
||||||
result.errors.forEach((error, i) => {
|
result.errors.forEach((error, i) => {
|
||||||
console.error(`${i + 1}. ${error.text}`);
|
console.error(`${i + 1}. ${error.text}`);
|
||||||
if (error.notes) {
|
if (error.notes) {
|
||||||
error.notes.forEach(note => {
|
error.notes.forEach((note) => {
|
||||||
console.error(` - ${note.text}`);
|
console.error(` - ${note.text}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import tseslint from 'typescript-eslint';
|
import tseslint from "typescript-eslint";
|
||||||
|
|
||||||
export default tseslint.config({
|
export default tseslint.config({
|
||||||
files: ["**/*.{ts,tsx,js,jsx}"],
|
files: ["**/*.{ts,tsx,js,jsx}"],
|
||||||
@@ -13,7 +13,7 @@ export default tseslint.config({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"semi": "error",
|
semi: "error",
|
||||||
"prefer-const": "warn"
|
"prefer-const": "warn"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -9,10 +9,15 @@ services:
|
|||||||
PARSERS: crowdsecurity/whitelists
|
PARSERS: crowdsecurity/whitelists
|
||||||
ENROLL_TAGS: docker
|
ENROLL_TAGS: docker
|
||||||
healthcheck:
|
healthcheck:
|
||||||
|
test:
|
||||||
|
- CMD
|
||||||
|
- cscli
|
||||||
|
- lapi
|
||||||
|
- status
|
||||||
interval: 10s
|
interval: 10s
|
||||||
retries: 15
|
timeout: 5s
|
||||||
timeout: 10s
|
retries: 3
|
||||||
test: ["CMD", "cscli", "capi", "status"]
|
start_period: 30s
|
||||||
labels:
|
labels:
|
||||||
- "traefik.enable=false" # Disable traefik for crowdsec
|
- "traefik.enable=false" # Disable traefik for crowdsec
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
|
badger:
|
||||||
|
plugin:
|
||||||
|
badger:
|
||||||
|
disableForwardAuth: true
|
||||||
redirect-to-https:
|
redirect-to-https:
|
||||||
redirectScheme:
|
redirectScheme:
|
||||||
scheme: https
|
scheme: https
|
||||||
@@ -63,6 +67,7 @@ http:
|
|||||||
- web
|
- web
|
||||||
middlewares:
|
middlewares:
|
||||||
- redirect-to-https
|
- redirect-to-https
|
||||||
|
- badger
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
# Next.js router (handles everything except API and WebSocket paths)
|
||||||
next-router:
|
next-router:
|
||||||
@@ -72,6 +77,7 @@ http:
|
|||||||
- websecure
|
- websecure
|
||||||
middlewares:
|
middlewares:
|
||||||
- security-headers # Add security headers middleware
|
- security-headers # Add security headers middleware
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -83,6 +89,7 @@ http:
|
|||||||
- websecure
|
- websecure
|
||||||
middlewares:
|
middlewares:
|
||||||
- security-headers # Add security headers middleware
|
- security-headers # Add security headers middleware
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -94,6 +101,7 @@ http:
|
|||||||
- websecure
|
- websecure
|
||||||
middlewares:
|
middlewares:
|
||||||
- security-headers # Add security headers middleware
|
- security-headers # Add security headers middleware
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -107,3 +115,12 @@ http:
|
|||||||
loadBalancer:
|
loadBalancer:
|
||||||
servers:
|
servers:
|
||||||
- url: "http://pangolin:3000" # API/WebSocket server
|
- url: "http://pangolin:3000" # API/WebSocket server
|
||||||
|
|
||||||
|
tcp:
|
||||||
|
serversTransports:
|
||||||
|
pp-transport-v1:
|
||||||
|
proxyProtocol:
|
||||||
|
version: 1
|
||||||
|
pp-transport-v2:
|
||||||
|
proxyProtocol:
|
||||||
|
version: 2
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
name: pangolin
|
name: pangolin
|
||||||
services:
|
services:
|
||||||
pangolin:
|
pangolin:
|
||||||
image: docker.io/fosrl/pangolin:{{.PangolinVersion}}
|
image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}}
|
||||||
container_name: pangolin
|
container_name: pangolin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
|
badger:
|
||||||
|
plugin:
|
||||||
|
badger:
|
||||||
|
disableForwardAuth: true
|
||||||
redirect-to-https:
|
redirect-to-https:
|
||||||
redirectScheme:
|
redirectScheme:
|
||||||
scheme: https
|
scheme: https
|
||||||
@@ -13,6 +17,7 @@ http:
|
|||||||
- web
|
- web
|
||||||
middlewares:
|
middlewares:
|
||||||
- redirect-to-https
|
- redirect-to-https
|
||||||
|
- badger
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
# Next.js router (handles everything except API and WebSocket paths)
|
||||||
next-router:
|
next-router:
|
||||||
@@ -20,6 +25,8 @@ http:
|
|||||||
service: next-service
|
service: next-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -29,6 +36,8 @@ http:
|
|||||||
service: api-service
|
service: api-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -38,6 +47,8 @@ http:
|
|||||||
service: api-service
|
service: api-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,9 @@ entryPoints:
|
|||||||
http:
|
http:
|
||||||
tls:
|
tls:
|
||||||
certResolver: "letsencrypt"
|
certResolver: "letsencrypt"
|
||||||
|
encodedCharacters:
|
||||||
|
allowEncodedSlash: true
|
||||||
|
allowEncodedQuestionMark: true
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ func installDocker() error {
|
|||||||
case strings.Contains(osRelease, "ID=ubuntu"):
|
case strings.Contains(osRelease, "ID=ubuntu"):
|
||||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
apt-get install -y apt-transport-https ca-certificates curl &&
|
apt-get install -y apt-transport-https ca-certificates curl gpg &&
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
@@ -82,7 +82,7 @@ func installDocker() error {
|
|||||||
case strings.Contains(osRelease, "ID=debian"):
|
case strings.Contains(osRelease, "ID=debian"):
|
||||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
apt-get install -y apt-transport-https ca-certificates curl &&
|
apt-get install -y apt-transport-https ca-certificates curl gpg &&
|
||||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||||
apt-get update &&
|
apt-get update &&
|
||||||
@@ -210,6 +210,47 @@ func isDockerRunning() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isPodmanRunning() bool {
|
||||||
|
cmd := exec.Command("podman", "info")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectContainerType detects whether the system is currently using Docker or Podman
|
||||||
|
// by checking which container runtime is running and has containers
|
||||||
|
func detectContainerType() SupportedContainer {
|
||||||
|
// Check if we have running containers with podman
|
||||||
|
if isPodmanRunning() {
|
||||||
|
cmd := exec.Command("podman", "ps", "-q")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||||
|
return Podman
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have running containers with docker
|
||||||
|
if isDockerRunning() {
|
||||||
|
cmd := exec.Command("docker", "ps", "-q")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||||
|
return Docker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no containers are running, check which one is installed and running
|
||||||
|
if isPodmanRunning() && isPodmanInstalled() {
|
||||||
|
return Podman
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDockerRunning() && isDockerInstalled() {
|
||||||
|
return Docker
|
||||||
|
}
|
||||||
|
|
||||||
|
return Undefined
|
||||||
|
}
|
||||||
|
|
||||||
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
||||||
func executeDockerComposeCommandWithArgs(args ...string) error {
|
func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ func installCrowdsec(config Config) error {
|
|||||||
|
|
||||||
if checkIfTextInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK") {
|
if checkIfTextInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK") {
|
||||||
fmt.Println("Failed to replace bouncer key! Please retrieve the key and replace it in the config/traefik/dynamic_config.yml file using the following command:")
|
fmt.Println("Failed to replace bouncer key! Please retrieve the key and replace it in the config/traefik/dynamic_config.yml file using the following command:")
|
||||||
fmt.Println(" docker exec crowdsec cscli bouncers add traefik-bouncer")
|
fmt.Printf(" %s exec crowdsec cscli bouncers add traefik-bouncer\n", config.InstallationContainerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -117,7 +117,7 @@ func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute the command to get the API key
|
// Execute the command to get the API key
|
||||||
cmd := exec.Command("docker", "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
|
cmd := exec.Command(string(containerType), "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ module installer
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.37.0
|
golang.org/x/term v0.39.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.38.0 // indirect
|
require golang.org/x/sys v0.40.0 // indirect
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -54,13 +54,31 @@ func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
|||||||
if defaultValue {
|
if defaultValue {
|
||||||
defaultStr = "yes"
|
defaultStr = "yes"
|
||||||
}
|
}
|
||||||
|
for {
|
||||||
input := readString(reader, prompt+" (yes/no)", defaultStr)
|
input := readString(reader, prompt+" (yes/no)", defaultStr)
|
||||||
return strings.ToLower(input) == "yes"
|
lower := strings.ToLower(input)
|
||||||
|
if lower == "yes" {
|
||||||
|
return true
|
||||||
|
} else if lower == "no" {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
fmt.Println("Please enter 'yes' or 'no'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
|
func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
|
||||||
|
for {
|
||||||
input := readStringNoDefault(reader, prompt+" (yes/no)")
|
input := readStringNoDefault(reader, prompt+" (yes/no)")
|
||||||
return strings.ToLower(input) == "yes"
|
lower := strings.ToLower(input)
|
||||||
|
if lower == "yes" {
|
||||||
|
return true
|
||||||
|
} else if lower == "no" {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
fmt.Println("Please enter 'yes' or 'no'.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math/rand"
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -49,6 +50,7 @@ type Config struct {
|
|||||||
DoCrowdsecInstall bool
|
DoCrowdsecInstall bool
|
||||||
EnableGeoblocking bool
|
EnableGeoblocking bool
|
||||||
Secret string
|
Secret string
|
||||||
|
IsEnterprise bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedContainer string
|
type SupportedContainer string
|
||||||
@@ -179,7 +181,7 @@ func main() {
|
|||||||
fmt.Println("You can try downloading it manually later if needed.")
|
fmt.Println("You can try downloading it manually later if needed.")
|
||||||
}
|
}
|
||||||
// Now you need to update your config file accordingly to enable geoblocking
|
// Now you need to update your config file accordingly to enable geoblocking
|
||||||
fmt.Println("Please remember to update your config/config.yml file to enable geoblocking! \n")
|
fmt.Print("Please remember to update your config/config.yml file to enable geoblocking! \n\n")
|
||||||
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
|
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
|
||||||
fmt.Println("Add the following line under the 'server' section:")
|
fmt.Println("Add the following line under the 'server' section:")
|
||||||
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
|
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
|
||||||
@@ -228,7 +230,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Try to detect container type from existing installation
|
||||||
|
detectedType := detectContainerType()
|
||||||
|
if detectedType == Undefined {
|
||||||
|
// If detection fails, prompt the user
|
||||||
|
fmt.Println("Unable to detect container type from existing installation.")
|
||||||
config.InstallationContainerType = podmanOrDocker(reader)
|
config.InstallationContainerType = podmanOrDocker(reader)
|
||||||
|
} else {
|
||||||
|
config.InstallationContainerType = detectedType
|
||||||
|
fmt.Printf("Detected container type: %s\n", config.InstallationContainerType)
|
||||||
|
}
|
||||||
|
|
||||||
config.DoCrowdsecInstall = true
|
config.DoCrowdsecInstall = true
|
||||||
err := installCrowdsec(config)
|
err := installCrowdsec(config)
|
||||||
@@ -242,7 +253,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !alreadyInstalled {
|
if !alreadyInstalled || config.DoCrowdsecInstall {
|
||||||
// Setup Token Section
|
// Setup Token Section
|
||||||
fmt.Println("\n=== Setup Token ===")
|
fmt.Println("\n=== Setup Token ===")
|
||||||
|
|
||||||
@@ -285,10 +296,10 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
if err := exec.Command("bash", "-c", "cat /etc/sysctl.d/99-podman.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start=' || cat /etc/sysctl.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
||||||
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
||||||
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
||||||
approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true)
|
approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system\". Approve?", true)
|
||||||
if approved {
|
if approved {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
fmt.Println("You need to run the installer as root for such a configuration.")
|
fmt.Println("You need to run the installer as root for such a configuration.")
|
||||||
@@ -299,8 +310,8 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
// container low-range ports as unprivileged ports.
|
// container low-range ports as unprivileged ports.
|
||||||
// Linux only.
|
// Linux only.
|
||||||
|
|
||||||
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil {
|
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system"); err != nil {
|
||||||
fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err)
|
fmt.Printf("Error configuring unprivileged ports: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -339,6 +350,8 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== Basic Configuration ===")
|
fmt.Println("\n=== Basic Configuration ===")
|
||||||
|
|
||||||
|
config.IsEnterprise = readBoolNoDefault(reader, "Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.")
|
||||||
|
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
|
|
||||||
// Set default dashboard domain after base domain is collected
|
// Set default dashboard domain after base domain is collected
|
||||||
@@ -359,7 +372,7 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
config.EmailNoReply = readString(reader, "Enter no-reply email address (often the same as SMTP username)", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
@@ -371,6 +384,10 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
fmt.Println("Error: Let's Encrypt email is required")
|
fmt.Println("Error: Let's Encrypt email is required")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if config.EnableEmail && config.EmailNoReply == "" {
|
||||||
|
fmt.Println("Error: No-reply email address is required when email is enabled")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
// Advanced configuration
|
// Advanced configuration
|
||||||
|
|
||||||
@@ -576,17 +593,12 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai
|
|||||||
}
|
}
|
||||||
|
|
||||||
func generateRandomSecretKey() string {
|
func generateRandomSecretKey() string {
|
||||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
secret := make([]byte, 32)
|
||||||
const length = 32
|
_, err := rand.Read(secret)
|
||||||
|
if err != nil {
|
||||||
var seededRand *rand.Rand = rand.New(
|
panic(fmt.Sprintf("Failed to generate random secret key: %v", err))
|
||||||
rand.NewSource(time.Now().UnixNano()))
|
|
||||||
|
|
||||||
b := make([]byte, length)
|
|
||||||
for i := range b {
|
|
||||||
b[i] = charset[seededRand.Intn(len(charset))]
|
|
||||||
}
|
}
|
||||||
return string(b)
|
return base64.StdEncoding.EncodeToString(secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getPublicIP() string {
|
func getPublicIP() string {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"setupCreate": "Create the organization, site, and resources",
|
"setupCreate": "Create the organization, site, and resources",
|
||||||
|
"headerAuthCompatibilityInfo": "Enable this to force a 401 Unauthorized response when an authentication token is missing. This is required for browsers or specific HTTP libraries that do not send credentials without a server challenge.",
|
||||||
|
"headerAuthCompatibility": "Extended compatibility",
|
||||||
"setupNewOrg": "New Organization",
|
"setupNewOrg": "New Organization",
|
||||||
"setupCreateOrg": "Create Organization",
|
"setupCreateOrg": "Create Organization",
|
||||||
"setupCreateResources": "Create Resources",
|
"setupCreateResources": "Create Resources",
|
||||||
@@ -16,6 +18,8 @@
|
|||||||
"componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.",
|
"componentsMember": "You're a member of {count, plural, =0 {no organization} one {one organization} other {# organizations}}.",
|
||||||
"componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.",
|
"componentsInvalidKey": "Invalid or expired license keys detected. Follow license terms to continue using all features.",
|
||||||
"dismiss": "Dismiss",
|
"dismiss": "Dismiss",
|
||||||
|
"subscriptionViolationMessage": "You're beyond your limits for your current plan. Correct the problem by removing sites, users, or other resources to stay within your plan.",
|
||||||
|
"subscriptionViolationViewBilling": "View billing",
|
||||||
"componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.",
|
"componentsLicenseViolation": "License Violation: This server is using {usedSites} sites which exceeds its licensed limit of {maxSites} sites. Follow license terms to continue using all features.",
|
||||||
"componentsSupporterMessage": "Thank you for supporting Pangolin as a {tier}!",
|
"componentsSupporterMessage": "Thank you for supporting Pangolin as a {tier}!",
|
||||||
"inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.",
|
"inviteErrorNotValid": "We're sorry, but it looks like the invite you're trying to access has not been accepted or is no longer valid.",
|
||||||
@@ -33,7 +37,7 @@
|
|||||||
"password": "Password",
|
"password": "Password",
|
||||||
"confirmPassword": "Confirm Password",
|
"confirmPassword": "Confirm Password",
|
||||||
"createAccount": "Create Account",
|
"createAccount": "Create Account",
|
||||||
"viewSettings": "View settings",
|
"viewSettings": "View Settings",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
@@ -51,6 +55,12 @@
|
|||||||
"siteQuestionRemove": "Are you sure you want to remove the site from the organization?",
|
"siteQuestionRemove": "Are you sure you want to remove the site from the organization?",
|
||||||
"siteManageSites": "Manage Sites",
|
"siteManageSites": "Manage Sites",
|
||||||
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
"siteDescription": "Create and manage sites to enable connectivity to private networks",
|
||||||
|
"sitesBannerTitle": "Connect Any Network",
|
||||||
|
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
|
||||||
|
"sitesBannerButtonText": "Install Site Connector",
|
||||||
|
"approvalsBannerTitle": "Approve or Deny Device Access",
|
||||||
|
"approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.",
|
||||||
|
"approvalsBannerButtonText": "Learn More",
|
||||||
"siteCreate": "Create Site",
|
"siteCreate": "Create Site",
|
||||||
"siteCreateDescription2": "Follow the steps below to create and connect a new site",
|
"siteCreateDescription2": "Follow the steps below to create and connect a new site",
|
||||||
"siteCreateDescription": "Create a new site to start connecting resources",
|
"siteCreateDescription": "Create a new site to start connecting resources",
|
||||||
@@ -71,8 +81,8 @@
|
|||||||
"siteConfirmCopy": "I have copied the config",
|
"siteConfirmCopy": "I have copied the config",
|
||||||
"searchSitesProgress": "Search sites...",
|
"searchSitesProgress": "Search sites...",
|
||||||
"siteAdd": "Add Site",
|
"siteAdd": "Add Site",
|
||||||
"siteInstallNewt": "Install Newt",
|
"siteInstallNewt": "Install Site",
|
||||||
"siteInstallNewtDescription": "Get Newt running on your system",
|
"siteInstallNewtDescription": "Install the site connector for your system",
|
||||||
"WgConfiguration": "WireGuard Configuration",
|
"WgConfiguration": "WireGuard Configuration",
|
||||||
"WgConfigurationDescription": "Use the following configuration to connect to the network",
|
"WgConfigurationDescription": "Use the following configuration to connect to the network",
|
||||||
"operatingSystem": "Operating System",
|
"operatingSystem": "Operating System",
|
||||||
@@ -100,6 +110,7 @@
|
|||||||
"siteTunnelDescription": "Determine how you want to connect to the site",
|
"siteTunnelDescription": "Determine how you want to connect to the site",
|
||||||
"siteNewtCredentials": "Credentials",
|
"siteNewtCredentials": "Credentials",
|
||||||
"siteNewtCredentialsDescription": "This is how the site will authenticate with the server",
|
"siteNewtCredentialsDescription": "This is how the site will authenticate with the server",
|
||||||
|
"remoteNodeCredentialsDescription": "This is how the remote node will authenticate with the server",
|
||||||
"siteCredentialsSave": "Save the Credentials",
|
"siteCredentialsSave": "Save the Credentials",
|
||||||
"siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
"siteCredentialsSaveDescription": "You will only be able to see this once. Make sure to copy it to a secure place.",
|
||||||
"siteInfo": "Site Information",
|
"siteInfo": "Site Information",
|
||||||
@@ -144,10 +155,14 @@
|
|||||||
"expires": "Expires",
|
"expires": "Expires",
|
||||||
"never": "Never",
|
"never": "Never",
|
||||||
"shareErrorSelectResource": "Please select a resource",
|
"shareErrorSelectResource": "Please select a resource",
|
||||||
"proxyResourceTitle": "Manage Proxy Resources",
|
"proxyResourceTitle": "Manage Public Resources",
|
||||||
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
|
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
|
||||||
"clientResourceTitle": "Manage Client Resources",
|
"proxyResourcesBannerTitle": "Web-based Public Access",
|
||||||
|
"proxyResourcesBannerDescription": "Public resources are HTTPS or TCP/UDP proxies accessible to anyone on the internet through a web browser. Unlike private resources, they do not require client-side software and can include identity and context-aware access policies.",
|
||||||
|
"clientResourceTitle": "Manage Private Resources",
|
||||||
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
|
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
|
||||||
|
"privateResourcesBannerTitle": "Zero-Trust Private Access",
|
||||||
|
"privateResourcesBannerDescription": "Private resources use zero-trust security, ensuring users and machines can only access resources you explicitly grant. Connect user devices or machine clients to access these resources over a secure virtual private network.",
|
||||||
"resourcesSearch": "Search resources...",
|
"resourcesSearch": "Search resources...",
|
||||||
"resourceAdd": "Add Resource",
|
"resourceAdd": "Add Resource",
|
||||||
"resourceErrorDelte": "Error deleting resource",
|
"resourceErrorDelte": "Error deleting resource",
|
||||||
@@ -157,9 +172,9 @@
|
|||||||
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
|
"resourceMessageRemove": "Once removed, the resource will no longer be accessible. All targets associated with the resource will also be removed.",
|
||||||
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
|
"resourceQuestionRemove": "Are you sure you want to remove the resource from the organization?",
|
||||||
"resourceHTTP": "HTTPS Resource",
|
"resourceHTTP": "HTTPS Resource",
|
||||||
"resourceHTTPDescription": "Proxy requests to the app over HTTPS using a subdomain or base domain.",
|
"resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.",
|
||||||
"resourceRaw": "Raw TCP/UDP Resource",
|
"resourceRaw": "Raw TCP/UDP Resource",
|
||||||
"resourceRawDescription": "Proxy requests to the app over TCP/UDP using a port number. This only works when sites are connected to nodes.",
|
"resourceRawDescription": "Proxy requests over raw TCP/UDP using a port number.",
|
||||||
"resourceCreate": "Create Resource",
|
"resourceCreate": "Create Resource",
|
||||||
"resourceCreateDescription": "Follow the steps below to create a new resource",
|
"resourceCreateDescription": "Follow the steps below to create a new resource",
|
||||||
"resourceSeeAll": "See All Resources",
|
"resourceSeeAll": "See All Resources",
|
||||||
@@ -181,11 +196,12 @@
|
|||||||
"baseDomain": "Base Domain",
|
"baseDomain": "Base Domain",
|
||||||
"subdomnainDescription": "The subdomain where the resource will be accessible.",
|
"subdomnainDescription": "The subdomain where the resource will be accessible.",
|
||||||
"resourceRawSettings": "TCP/UDP Settings",
|
"resourceRawSettings": "TCP/UDP Settings",
|
||||||
"resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP. You map the resource to a port on the host Pangolin server, so you can access the resource from server-public-ip:mapped-port.",
|
"resourceRawSettingsDescription": "Configure how the resource will be accessed over TCP/UDP",
|
||||||
"protocol": "Protocol",
|
"protocol": "Protocol",
|
||||||
"protocolSelect": "Select a protocol",
|
"protocolSelect": "Select a protocol",
|
||||||
"resourcePortNumber": "Port Number",
|
"resourcePortNumber": "Port Number",
|
||||||
"resourcePortNumberDescription": "The external port number to proxy requests.",
|
"resourcePortNumberDescription": "The external port number to proxy requests.",
|
||||||
|
"back": "Back",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"resourceConfig": "Configuration Snippets",
|
"resourceConfig": "Configuration Snippets",
|
||||||
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource",
|
"resourceConfigDescription": "Copy and paste these configuration snippets to set up the TCP/UDP resource",
|
||||||
@@ -231,6 +247,17 @@
|
|||||||
"orgErrorDeleteMessage": "An error occurred while deleting the organization.",
|
"orgErrorDeleteMessage": "An error occurred while deleting the organization.",
|
||||||
"orgDeleted": "Organization deleted",
|
"orgDeleted": "Organization deleted",
|
||||||
"orgDeletedMessage": "The organization and its data has been deleted.",
|
"orgDeletedMessage": "The organization and its data has been deleted.",
|
||||||
|
"deleteAccount": "Delete Account",
|
||||||
|
"deleteAccountDescription": "Permanently delete your account, all organizations you own, and all data within those organizations. This cannot be undone.",
|
||||||
|
"deleteAccountButton": "Delete Account",
|
||||||
|
"deleteAccountConfirmTitle": "Delete Account",
|
||||||
|
"deleteAccountConfirmMessage": "This will permanently wipe your account, all organizations you own, and all data within those organizations. This cannot be undone.",
|
||||||
|
"deleteAccountConfirmString": "delete account",
|
||||||
|
"deleteAccountSuccess": "Account Deleted",
|
||||||
|
"deleteAccountSuccessMessage": "Your account has been deleted.",
|
||||||
|
"deleteAccountError": "Failed to delete account",
|
||||||
|
"deleteAccountPreviewAccount": "Your Account",
|
||||||
|
"deleteAccountPreviewOrgs": "Organizations you own (and all their data)",
|
||||||
"orgMissing": "Organization ID Missing",
|
"orgMissing": "Organization ID Missing",
|
||||||
"orgMissingMessage": "Unable to regenerate invitation without an organization ID.",
|
"orgMissingMessage": "Unable to regenerate invitation without an organization ID.",
|
||||||
"accessUsersManage": "Manage Users",
|
"accessUsersManage": "Manage Users",
|
||||||
@@ -247,6 +274,8 @@
|
|||||||
"accessRolesSearch": "Search roles...",
|
"accessRolesSearch": "Search roles...",
|
||||||
"accessRolesAdd": "Add Role",
|
"accessRolesAdd": "Add Role",
|
||||||
"accessRoleDelete": "Delete Role",
|
"accessRoleDelete": "Delete Role",
|
||||||
|
"accessApprovalsManage": "Manage Approvals",
|
||||||
|
"accessApprovalsDescription": "View and manage pending approvals for access to this organization",
|
||||||
"description": "Description",
|
"description": "Description",
|
||||||
"inviteTitle": "Open Invitations",
|
"inviteTitle": "Open Invitations",
|
||||||
"inviteDescription": "Manage invitations for other users to join the organization",
|
"inviteDescription": "Manage invitations for other users to join the organization",
|
||||||
@@ -419,7 +448,7 @@
|
|||||||
"userErrorExistsDescription": "This user is already a member of the organization.",
|
"userErrorExistsDescription": "This user is already a member of the organization.",
|
||||||
"inviteError": "Failed to invite user",
|
"inviteError": "Failed to invite user",
|
||||||
"inviteErrorDescription": "An error occurred while inviting the user",
|
"inviteErrorDescription": "An error occurred while inviting the user",
|
||||||
"userInvited": "User invited",
|
"userInvited": "User Invited",
|
||||||
"userInvitedDescription": "The user has been successfully invited.",
|
"userInvitedDescription": "The user has been successfully invited.",
|
||||||
"userErrorCreate": "Failed to create user",
|
"userErrorCreate": "Failed to create user",
|
||||||
"userErrorCreateDescription": "An error occurred while creating the user",
|
"userErrorCreateDescription": "An error occurred while creating the user",
|
||||||
@@ -440,6 +469,20 @@
|
|||||||
"selectDuration": "Select duration",
|
"selectDuration": "Select duration",
|
||||||
"selectResource": "Select Resource",
|
"selectResource": "Select Resource",
|
||||||
"filterByResource": "Filter By Resource",
|
"filterByResource": "Filter By Resource",
|
||||||
|
"selectApprovalState": "Select Approval State",
|
||||||
|
"filterByApprovalState": "Filter By Approval State",
|
||||||
|
"approvalListEmpty": "No approvals",
|
||||||
|
"approvalState": "Approval State",
|
||||||
|
"approvalLoadMore": "Load more",
|
||||||
|
"loadingApprovals": "Loading Approvals",
|
||||||
|
"approve": "Approve",
|
||||||
|
"approved": "Approved",
|
||||||
|
"denied": "Denied",
|
||||||
|
"deniedApproval": "Denied Approval",
|
||||||
|
"all": "All",
|
||||||
|
"deny": "Deny",
|
||||||
|
"viewDetails": "View Details",
|
||||||
|
"requestingNewDeviceApproval": "requested a new device",
|
||||||
"resetFilters": "Reset Filters",
|
"resetFilters": "Reset Filters",
|
||||||
"totalBlocked": "Requests Blocked By Pangolin",
|
"totalBlocked": "Requests Blocked By Pangolin",
|
||||||
"totalRequests": "Total Requests",
|
"totalRequests": "Total Requests",
|
||||||
@@ -499,7 +542,7 @@
|
|||||||
"proxyUpdatedDescription": "Proxy settings have been updated successfully",
|
"proxyUpdatedDescription": "Proxy settings have been updated successfully",
|
||||||
"proxyErrorUpdate": "Failed to update proxy settings",
|
"proxyErrorUpdate": "Failed to update proxy settings",
|
||||||
"proxyErrorUpdateDescription": "An error occurred while updating proxy settings",
|
"proxyErrorUpdateDescription": "An error occurred while updating proxy settings",
|
||||||
"targetAddr": "IP / Hostname",
|
"targetAddr": "Host",
|
||||||
"targetPort": "Port",
|
"targetPort": "Port",
|
||||||
"targetProtocol": "Protocol",
|
"targetProtocol": "Protocol",
|
||||||
"targetTlsSettings": "Secure Connection Configuration",
|
"targetTlsSettings": "Secure Connection Configuration",
|
||||||
@@ -687,7 +730,7 @@
|
|||||||
"resourceRoleDescription": "Admins can always access this resource.",
|
"resourceRoleDescription": "Admins can always access this resource.",
|
||||||
"resourceUsersRoles": "Access Controls",
|
"resourceUsersRoles": "Access Controls",
|
||||||
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
"resourceUsersRolesDescription": "Configure which users and roles can visit this resource",
|
||||||
"resourceUsersRolesSubmit": "Save Users & Roles",
|
"resourceUsersRolesSubmit": "Save Access Controls",
|
||||||
"resourceWhitelistSave": "Saved successfully",
|
"resourceWhitelistSave": "Saved successfully",
|
||||||
"resourceWhitelistSaveDescription": "Whitelist settings have been saved",
|
"resourceWhitelistSaveDescription": "Whitelist settings have been saved",
|
||||||
"ssoUse": "Use Platform SSO",
|
"ssoUse": "Use Platform SSO",
|
||||||
@@ -719,16 +762,28 @@
|
|||||||
"countries": "Countries",
|
"countries": "Countries",
|
||||||
"accessRoleCreate": "Create Role",
|
"accessRoleCreate": "Create Role",
|
||||||
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
|
"accessRoleCreateDescription": "Create a new role to group users and manage their permissions.",
|
||||||
|
"accessRoleEdit": "Edit Role",
|
||||||
|
"accessRoleEditDescription": "Edit role information.",
|
||||||
"accessRoleCreateSubmit": "Create Role",
|
"accessRoleCreateSubmit": "Create Role",
|
||||||
"accessRoleCreated": "Role created",
|
"accessRoleCreated": "Role created",
|
||||||
"accessRoleCreatedDescription": "The role has been successfully created.",
|
"accessRoleCreatedDescription": "The role has been successfully created.",
|
||||||
"accessRoleErrorCreate": "Failed to create role",
|
"accessRoleErrorCreate": "Failed to create role",
|
||||||
"accessRoleErrorCreateDescription": "An error occurred while creating the role.",
|
"accessRoleErrorCreateDescription": "An error occurred while creating the role.",
|
||||||
|
"accessRoleUpdateSubmit": "Update Role",
|
||||||
|
"accessRoleUpdated": "Role updated",
|
||||||
|
"accessRoleUpdatedDescription": "The role has been successfully updated.",
|
||||||
|
"accessApprovalUpdated": "Approval processed",
|
||||||
|
"accessApprovalApprovedDescription": "Set Approval Request decision to approved.",
|
||||||
|
"accessApprovalDeniedDescription": "Set Approval Request decision to denied.",
|
||||||
|
"accessRoleErrorUpdate": "Failed to update role",
|
||||||
|
"accessRoleErrorUpdateDescription": "An error occurred while updating the role.",
|
||||||
|
"accessApprovalErrorUpdate": "Failed to process approval",
|
||||||
|
"accessApprovalErrorUpdateDescription": "An error occurred while processing the approval.",
|
||||||
"accessRoleErrorNewRequired": "New role is required",
|
"accessRoleErrorNewRequired": "New role is required",
|
||||||
"accessRoleErrorRemove": "Failed to remove role",
|
"accessRoleErrorRemove": "Failed to remove role",
|
||||||
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
|
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
|
||||||
"accessRoleName": "Role Name",
|
"accessRoleName": "Role Name",
|
||||||
"accessRoleQuestionRemove": "You're about to delete the {name} role. You cannot undo this action.",
|
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
|
||||||
"accessRoleRemove": "Remove Role",
|
"accessRoleRemove": "Remove Role",
|
||||||
"accessRoleRemoveDescription": "Remove a role from the organization",
|
"accessRoleRemoveDescription": "Remove a role from the organization",
|
||||||
"accessRoleRemoveSubmit": "Remove Role",
|
"accessRoleRemoveSubmit": "Remove Role",
|
||||||
@@ -750,6 +805,9 @@
|
|||||||
"sitestCountIncrease": "Increase site count",
|
"sitestCountIncrease": "Increase site count",
|
||||||
"idpManage": "Manage Identity Providers",
|
"idpManage": "Manage Identity Providers",
|
||||||
"idpManageDescription": "View and manage identity providers in the system",
|
"idpManageDescription": "View and manage identity providers in the system",
|
||||||
|
"idpGlobalModeBanner": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To enable IdPs per organization, edit the server config and set IdP mode to org. <configDocsLink>See the docs</configDocsLink>. If you want to continue using global IdPs and make this disappear from the organization settings, explicitly set the mode to global in the config.",
|
||||||
|
"idpGlobalModeBannerUpgradeRequired": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To use identity providers per organization, you must upgrade to the Enterprise edition.",
|
||||||
|
"idpGlobalModeBannerLicenseRequired": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To use identity providers per organization, an Enterprise license is required.",
|
||||||
"idpDeletedDescription": "Identity provider deleted successfully",
|
"idpDeletedDescription": "Identity provider deleted successfully",
|
||||||
"idpOidc": "OAuth2/OIDC",
|
"idpOidc": "OAuth2/OIDC",
|
||||||
"idpQuestionRemove": "Are you sure you want to permanently delete the identity provider?",
|
"idpQuestionRemove": "Are you sure you want to permanently delete the identity provider?",
|
||||||
@@ -840,6 +898,7 @@
|
|||||||
"orgPolicyConfig": "Configure access for an organization",
|
"orgPolicyConfig": "Configure access for an organization",
|
||||||
"idpUpdatedDescription": "Identity provider updated successfully",
|
"idpUpdatedDescription": "Identity provider updated successfully",
|
||||||
"redirectUrl": "Redirect URL",
|
"redirectUrl": "Redirect URL",
|
||||||
|
"orgIdpRedirectUrls": "Redirect URLs",
|
||||||
"redirectUrlAbout": "About Redirect URL",
|
"redirectUrlAbout": "About Redirect URL",
|
||||||
"redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in the identity provider's settings.",
|
"redirectUrlAboutDescription": "This is the URL to which users will be redirected after authentication. You need to configure this URL in the identity provider's settings.",
|
||||||
"pangolinAuth": "Auth - Pangolin",
|
"pangolinAuth": "Auth - Pangolin",
|
||||||
@@ -863,7 +922,7 @@
|
|||||||
"inviteAlready": "Looks like you've been invited!",
|
"inviteAlready": "Looks like you've been invited!",
|
||||||
"inviteAlreadyDescription": "To accept the invite, you must log in or create an account.",
|
"inviteAlreadyDescription": "To accept the invite, you must log in or create an account.",
|
||||||
"signupQuestion": "Already have an account?",
|
"signupQuestion": "Already have an account?",
|
||||||
"login": "Log in",
|
"login": "Log In",
|
||||||
"resourceNotFound": "Resource Not Found",
|
"resourceNotFound": "Resource Not Found",
|
||||||
"resourceNotFoundDescription": "The resource you're trying to access does not exist.",
|
"resourceNotFoundDescription": "The resource you're trying to access does not exist.",
|
||||||
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
|
"pincodeRequirementsLength": "PIN must be exactly 6 digits",
|
||||||
@@ -924,6 +983,10 @@
|
|||||||
"passwordResetSent": "We'll send a password reset code to this email address.",
|
"passwordResetSent": "We'll send a password reset code to this email address.",
|
||||||
"passwordResetCode": "Reset Code",
|
"passwordResetCode": "Reset Code",
|
||||||
"passwordResetCodeDescription": "Check your email for the reset code.",
|
"passwordResetCodeDescription": "Check your email for the reset code.",
|
||||||
|
"generatePasswordResetCode": "Generate Password Reset Code",
|
||||||
|
"passwordResetCodeGenerated": "Password Reset Code Generated",
|
||||||
|
"passwordResetCodeGeneratedDescription": "Share this code with the user. They can use it to reset their password.",
|
||||||
|
"passwordResetUrl": "Reset URL",
|
||||||
"passwordNew": "New Password",
|
"passwordNew": "New Password",
|
||||||
"passwordNewConfirm": "Confirm New Password",
|
"passwordNewConfirm": "Confirm New Password",
|
||||||
"changePassword": "Change Password",
|
"changePassword": "Change Password",
|
||||||
@@ -939,10 +1002,13 @@
|
|||||||
"passwordExpiryDescription": "This organization requires you to change your password every {maxDays} days.",
|
"passwordExpiryDescription": "This organization requires you to change your password every {maxDays} days.",
|
||||||
"changePasswordNow": "Change Password Now",
|
"changePasswordNow": "Change Password Now",
|
||||||
"pincodeAuth": "Authenticator Code",
|
"pincodeAuth": "Authenticator Code",
|
||||||
"pincodeSubmit2": "Submit Code",
|
"pincodeSubmit2": "Submit code",
|
||||||
"passwordResetSubmit": "Request Reset",
|
"passwordResetSubmit": "Request Reset",
|
||||||
|
"passwordResetAlreadyHaveCode": "Enter Code",
|
||||||
|
"passwordResetSmtpRequired": "Please contact your administrator",
|
||||||
|
"passwordResetSmtpRequiredDescription": "A password reset code is required to reset your password. Please contact your administrator for assistance.",
|
||||||
"passwordBack": "Back to Password",
|
"passwordBack": "Back to Password",
|
||||||
"loginBack": "Go back to log in",
|
"loginBack": "Go back to main login page",
|
||||||
"signup": "Sign up",
|
"signup": "Sign up",
|
||||||
"loginStart": "Log in to get started",
|
"loginStart": "Log in to get started",
|
||||||
"idpOidcTokenValidating": "Validating OIDC token",
|
"idpOidcTokenValidating": "Validating OIDC token",
|
||||||
@@ -965,6 +1031,7 @@
|
|||||||
"pangolinSetup": "Setup - Pangolin",
|
"pangolinSetup": "Setup - Pangolin",
|
||||||
"orgNameRequired": "Organization name is required",
|
"orgNameRequired": "Organization name is required",
|
||||||
"orgIdRequired": "Organization ID is required",
|
"orgIdRequired": "Organization ID is required",
|
||||||
|
"orgIdMaxLength": "Organization ID must be at most 32 characters",
|
||||||
"orgErrorCreate": "An error occurred while creating org",
|
"orgErrorCreate": "An error occurred while creating org",
|
||||||
"pageNotFound": "Page Not Found",
|
"pageNotFound": "Page Not Found",
|
||||||
"pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.",
|
"pageNotFoundDescription": "Oops! The page you're looking for doesn't exist.",
|
||||||
@@ -1028,6 +1095,7 @@
|
|||||||
"updateOrgUser": "Update Org User",
|
"updateOrgUser": "Update Org User",
|
||||||
"createOrgUser": "Create Org User",
|
"createOrgUser": "Create Org User",
|
||||||
"actionUpdateOrg": "Update Organization",
|
"actionUpdateOrg": "Update Organization",
|
||||||
|
"actionRemoveInvitation": "Remove Invitation",
|
||||||
"actionUpdateUser": "Update User",
|
"actionUpdateUser": "Update User",
|
||||||
"actionGetUser": "Get User",
|
"actionGetUser": "Get User",
|
||||||
"actionGetOrgUser": "Get Organization User",
|
"actionGetOrgUser": "Get Organization User",
|
||||||
@@ -1037,6 +1105,8 @@
|
|||||||
"actionGetSite": "Get Site",
|
"actionGetSite": "Get Site",
|
||||||
"actionListSites": "List Sites",
|
"actionListSites": "List Sites",
|
||||||
"actionApplyBlueprint": "Apply Blueprint",
|
"actionApplyBlueprint": "Apply Blueprint",
|
||||||
|
"actionListBlueprints": "List Blueprints",
|
||||||
|
"actionGetBlueprint": "Get Blueprint",
|
||||||
"setupToken": "Setup Token",
|
"setupToken": "Setup Token",
|
||||||
"setupTokenDescription": "Enter the setup token from the server console.",
|
"setupTokenDescription": "Enter the setup token from the server console.",
|
||||||
"setupTokenRequired": "Setup token is required",
|
"setupTokenRequired": "Setup token is required",
|
||||||
@@ -1097,6 +1167,10 @@
|
|||||||
"actionUpdateIdpOrg": "Update IDP Org",
|
"actionUpdateIdpOrg": "Update IDP Org",
|
||||||
"actionCreateClient": "Create Client",
|
"actionCreateClient": "Create Client",
|
||||||
"actionDeleteClient": "Delete Client",
|
"actionDeleteClient": "Delete Client",
|
||||||
|
"actionArchiveClient": "Archive Client",
|
||||||
|
"actionUnarchiveClient": "Unarchive Client",
|
||||||
|
"actionBlockClient": "Block Client",
|
||||||
|
"actionUnblockClient": "Unblock Client",
|
||||||
"actionUpdateClient": "Update Client",
|
"actionUpdateClient": "Update Client",
|
||||||
"actionListClients": "List Clients",
|
"actionListClients": "List Clients",
|
||||||
"actionGetClient": "Get Client",
|
"actionGetClient": "Get Client",
|
||||||
@@ -1110,17 +1184,18 @@
|
|||||||
"actionViewLogs": "View Logs",
|
"actionViewLogs": "View Logs",
|
||||||
"noneSelected": "None selected",
|
"noneSelected": "None selected",
|
||||||
"orgNotFound2": "No organizations found.",
|
"orgNotFound2": "No organizations found.",
|
||||||
"searchProgress": "Search...",
|
"searchPlaceholder": "Search...",
|
||||||
|
"emptySearchOptions": "No options found",
|
||||||
"create": "Create",
|
"create": "Create",
|
||||||
"orgs": "Organizations",
|
"orgs": "Organizations",
|
||||||
"loginError": "An error occurred while logging in",
|
"loginError": "An unexpected error occurred. Please try again.",
|
||||||
"loginRequiredForDevice": "Login is required to authenticate your device.",
|
"loginRequiredForDevice": "Login is required for your device.",
|
||||||
"passwordForgot": "Forgot your password?",
|
"passwordForgot": "Forgot your password?",
|
||||||
"otpAuth": "Two-Factor Authentication",
|
"otpAuth": "Two-Factor Authentication",
|
||||||
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
|
"otpAuthDescription": "Enter the code from your authenticator app or one of your single-use backup codes.",
|
||||||
"otpAuthSubmit": "Submit Code",
|
"otpAuthSubmit": "Submit Code",
|
||||||
"idpContinue": "Or continue with",
|
"idpContinue": "Or continue with",
|
||||||
"otpAuthBack": "Back to Log In",
|
"otpAuthBack": "Back to Password",
|
||||||
"navbar": "Navigation Menu",
|
"navbar": "Navigation Menu",
|
||||||
"navbarDescription": "Main navigation menu for the application",
|
"navbarDescription": "Main navigation menu for the application",
|
||||||
"navbarDocsLink": "Documentation",
|
"navbarDocsLink": "Documentation",
|
||||||
@@ -1168,9 +1243,10 @@
|
|||||||
"sidebarOverview": "Overview",
|
"sidebarOverview": "Overview",
|
||||||
"sidebarHome": "Home",
|
"sidebarHome": "Home",
|
||||||
"sidebarSites": "Sites",
|
"sidebarSites": "Sites",
|
||||||
|
"sidebarApprovals": "Approval Requests",
|
||||||
"sidebarResources": "Resources",
|
"sidebarResources": "Resources",
|
||||||
"sidebarProxyResources": "Proxy Resources",
|
"sidebarProxyResources": "Public",
|
||||||
"sidebarClientResources": "Client Resources",
|
"sidebarClientResources": "Private",
|
||||||
"sidebarAccessControl": "Access Control",
|
"sidebarAccessControl": "Access Control",
|
||||||
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
"sidebarLogsAndAnalytics": "Logs & Analytics",
|
||||||
"sidebarUsers": "Users",
|
"sidebarUsers": "Users",
|
||||||
@@ -1185,13 +1261,14 @@
|
|||||||
"sidebarLicense": "License",
|
"sidebarLicense": "License",
|
||||||
"sidebarClients": "Clients",
|
"sidebarClients": "Clients",
|
||||||
"sidebarUserDevices": "User Devices",
|
"sidebarUserDevices": "User Devices",
|
||||||
"sidebarMachineClients": "Machine Clients",
|
"sidebarMachineClients": "Machines",
|
||||||
"sidebarDomains": "Domains",
|
"sidebarDomains": "Domains",
|
||||||
"sidebarGeneral": "General",
|
"sidebarGeneral": "Manage",
|
||||||
"sidebarLogAndAnalytics": "Log & Analytics",
|
"sidebarLogAndAnalytics": "Log & Analytics",
|
||||||
"sidebarBluePrints": "Blueprints",
|
"sidebarBluePrints": "Blueprints",
|
||||||
"sidebarOrganization": "Organization",
|
"sidebarOrganization": "Organization",
|
||||||
"sidebarLogsAnalytics": "Request Analytics",
|
"sidebarBillingAndLicenses": "Billing & Licenses",
|
||||||
|
"sidebarLogsAnalytics": "Analytics",
|
||||||
"blueprints": "Blueprints",
|
"blueprints": "Blueprints",
|
||||||
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
"blueprintsDescription": "Apply declarative configurations and view previous runs",
|
||||||
"blueprintAdd": "Add Blueprint",
|
"blueprintAdd": "Add Blueprint",
|
||||||
@@ -1256,6 +1333,7 @@
|
|||||||
"setupErrorCreateAdmin": "An error occurred while creating the server admin account.",
|
"setupErrorCreateAdmin": "An error occurred while creating the server admin account.",
|
||||||
"certificateStatus": "Certificate Status",
|
"certificateStatus": "Certificate Status",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
|
"loadingAnalytics": "Loading Analytics",
|
||||||
"restart": "Restart",
|
"restart": "Restart",
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
"domainsDescription": "Create and manage domains available in the organization",
|
"domainsDescription": "Create and manage domains available in the organization",
|
||||||
@@ -1283,6 +1361,7 @@
|
|||||||
"refreshError": "Failed to refresh data",
|
"refreshError": "Failed to refresh data",
|
||||||
"verified": "Verified",
|
"verified": "Verified",
|
||||||
"pending": "Pending",
|
"pending": "Pending",
|
||||||
|
"pendingApproval": "Pending Approval",
|
||||||
"sidebarBilling": "Billing",
|
"sidebarBilling": "Billing",
|
||||||
"billing": "Billing",
|
"billing": "Billing",
|
||||||
"orgBillingDescription": "Manage billing information and subscriptions",
|
"orgBillingDescription": "Manage billing information and subscriptions",
|
||||||
@@ -1301,8 +1380,11 @@
|
|||||||
"accountSetupSuccess": "Account setup completed! Welcome to Pangolin!",
|
"accountSetupSuccess": "Account setup completed! Welcome to Pangolin!",
|
||||||
"documentation": "Documentation",
|
"documentation": "Documentation",
|
||||||
"saveAllSettings": "Save All Settings",
|
"saveAllSettings": "Save All Settings",
|
||||||
|
"saveResourceTargets": "Save Targets",
|
||||||
|
"saveResourceHttp": "Save Proxy Settings",
|
||||||
|
"saveProxyProtocol": "Save Proxy protocol settings",
|
||||||
"settingsUpdated": "Settings updated",
|
"settingsUpdated": "Settings updated",
|
||||||
"settingsUpdatedDescription": "All settings have been updated successfully",
|
"settingsUpdatedDescription": "Settings updated successfully",
|
||||||
"settingsErrorUpdate": "Failed to update settings",
|
"settingsErrorUpdate": "Failed to update settings",
|
||||||
"settingsErrorUpdateDescription": "An error occurred while updating settings",
|
"settingsErrorUpdateDescription": "An error occurred while updating settings",
|
||||||
"sidebarCollapse": "Collapse",
|
"sidebarCollapse": "Collapse",
|
||||||
@@ -1313,9 +1395,9 @@
|
|||||||
"productUpdateTitle": "Product Updates",
|
"productUpdateTitle": "Product Updates",
|
||||||
"productUpdateEmpty": "No updates",
|
"productUpdateEmpty": "No updates",
|
||||||
"dismissAll": "Dismiss all",
|
"dismissAll": "Dismiss all",
|
||||||
"pangolinUpdateAvailable": "New version available",
|
"pangolinUpdateAvailable": "Update Available",
|
||||||
"pangolinUpdateAvailableInfo": "Version {version} is ready to install",
|
"pangolinUpdateAvailableInfo": "Version {version} is ready to install",
|
||||||
"pangolinUpdateAvailableReleaseNotes": "View release notes",
|
"pangolinUpdateAvailableReleaseNotes": "View Release Notes",
|
||||||
"newtUpdateAvailable": "Update Available",
|
"newtUpdateAvailable": "Update Available",
|
||||||
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
|
"newtUpdateAvailableInfo": "A new version of Newt is available. Please update to the latest version for the best experience.",
|
||||||
"domainPickerEnterDomain": "Domain",
|
"domainPickerEnterDomain": "Domain",
|
||||||
@@ -1344,10 +1426,11 @@
|
|||||||
"billingUsageLimitsOverview": "Usage Limits Overview",
|
"billingUsageLimitsOverview": "Usage Limits Overview",
|
||||||
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
|
"billingMonitorUsage": "Monitor your usage against configured limits. If you need limits increased please contact us support@pangolin.net.",
|
||||||
"billingDataUsage": "Data Usage",
|
"billingDataUsage": "Data Usage",
|
||||||
"billingOnlineTime": "Site Online Time",
|
"billingSites": "Sites",
|
||||||
"billingUsers": "Active Users",
|
"billingUsers": "Users",
|
||||||
"billingDomains": "Active Domains",
|
"billingDomains": "Domains",
|
||||||
"billingRemoteExitNodes": "Active Self-hosted Nodes",
|
"billingOrganizations": "Orgs",
|
||||||
|
"billingRemoteExitNodes": "Remote Nodes",
|
||||||
"billingNoLimitConfigured": "No limit configured",
|
"billingNoLimitConfigured": "No limit configured",
|
||||||
"billingEstimatedPeriod": "Estimated Billing Period",
|
"billingEstimatedPeriod": "Estimated Billing Period",
|
||||||
"billingIncludedUsage": "Included Usage",
|
"billingIncludedUsage": "Included Usage",
|
||||||
@@ -1372,15 +1455,24 @@
|
|||||||
"billingFailedToGetPortalUrl": "Failed to get portal URL",
|
"billingFailedToGetPortalUrl": "Failed to get portal URL",
|
||||||
"billingPortalError": "Portal Error",
|
"billingPortalError": "Portal Error",
|
||||||
"billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.",
|
"billingDataUsageInfo": "You're charged for all data transferred through your secure tunnels when connected to the cloud. This includes both incoming and outgoing traffic across all your sites. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Data is not charged when using nodes.",
|
||||||
"billingOnlineTimeInfo": "You're charged based on how long your sites stay connected to the cloud. For example, 44,640 minutes equals one site running 24/7 for a full month. When you reach your limit, your sites will disconnect until you upgrade your plan or reduce usage. Time is not charged when using nodes.",
|
"billingSInfo": "How many sites you can use",
|
||||||
"billingUsersInfo": "You're charged for each user in the organization. Billing is calculated daily based on the number of active user accounts in your org.",
|
"billingUsersInfo": "How many users you can use",
|
||||||
"billingDomainInfo": "You're charged for each domain in the organization. Billing is calculated daily based on the number of active domain accounts in your org.",
|
"billingDomainInfo": "How many domains you can use",
|
||||||
"billingRemoteExitNodesInfo": "You're charged for each managed Node in the organization. Billing is calculated daily based on the number of active managed Nodes in your org.",
|
"billingRemoteExitNodesInfo": "How many remote nodes you can use",
|
||||||
|
"billingLicenseKeys": "License Keys",
|
||||||
|
"billingLicenseKeysDescription": "Manage your license key subscriptions",
|
||||||
|
"billingLicenseSubscription": "License Subscription",
|
||||||
|
"billingInactive": "Inactive",
|
||||||
|
"billingLicenseItem": "License Item",
|
||||||
|
"billingQuantity": "Quantity",
|
||||||
|
"billingTotal": "total",
|
||||||
|
"billingModifyLicenses": "Modify License Subscription",
|
||||||
"domainNotFound": "Domain Not Found",
|
"domainNotFound": "Domain Not Found",
|
||||||
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
|
"domainNotFoundDescription": "This resource is disabled because the domain no longer exists our system. Please set a new domain for this resource.",
|
||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
"createNewOrgDescription": "Create a new organization",
|
"createNewOrgDescription": "Create a new organization",
|
||||||
"organization": "Organization",
|
"organization": "Organization",
|
||||||
|
"primary": "Primary",
|
||||||
"port": "Port",
|
"port": "Port",
|
||||||
"securityKeyManage": "Manage Security Keys",
|
"securityKeyManage": "Manage Security Keys",
|
||||||
"securityKeyDescription": "Add or remove security keys for passwordless authentication",
|
"securityKeyDescription": "Add or remove security keys for passwordless authentication",
|
||||||
@@ -1396,7 +1488,7 @@
|
|||||||
"securityKeyRemoveSuccess": "Security key removed successfully",
|
"securityKeyRemoveSuccess": "Security key removed successfully",
|
||||||
"securityKeyRemoveError": "Failed to remove security key",
|
"securityKeyRemoveError": "Failed to remove security key",
|
||||||
"securityKeyLoadError": "Failed to load security keys",
|
"securityKeyLoadError": "Failed to load security keys",
|
||||||
"securityKeyLogin": "Continue with security key",
|
"securityKeyLogin": "Use Security Key",
|
||||||
"securityKeyAuthError": "Failed to authenticate with security key",
|
"securityKeyAuthError": "Failed to authenticate with security key",
|
||||||
"securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.",
|
"securityKeyRecommendation": "Register a backup security key on another device to ensure you always have access to your account.",
|
||||||
"registering": "Registering...",
|
"registering": "Registering...",
|
||||||
@@ -1452,11 +1544,37 @@
|
|||||||
"resourcePortRequired": "Port number is required for non-HTTP resources",
|
"resourcePortRequired": "Port number is required for non-HTTP resources",
|
||||||
"resourcePortNotAllowed": "Port number should not be set for HTTP resources",
|
"resourcePortNotAllowed": "Port number should not be set for HTTP resources",
|
||||||
"billingPricingCalculatorLink": "Pricing Calculator",
|
"billingPricingCalculatorLink": "Pricing Calculator",
|
||||||
|
"billingYourPlan": "Your Plan",
|
||||||
|
"billingViewOrModifyPlan": "View or modify your current plan",
|
||||||
|
"billingViewPlanDetails": "View Plan Details",
|
||||||
|
"billingUsageAndLimits": "Usage and Limits",
|
||||||
|
"billingViewUsageAndLimits": "View your plan's limits and current usage",
|
||||||
|
"billingCurrentUsage": "Current Usage",
|
||||||
|
"billingMaximumLimits": "Maximum Limits",
|
||||||
|
"billingRemoteNodes": "Remote Nodes",
|
||||||
|
"billingUnlimited": "Unlimited",
|
||||||
|
"billingPaidLicenseKeys": "Paid License Keys",
|
||||||
|
"billingManageLicenseSubscription": "Manage your subscription for paid self-hosted license keys",
|
||||||
|
"billingCurrentKeys": "Current Keys",
|
||||||
|
"billingModifyCurrentPlan": "Modify Current Plan",
|
||||||
|
"billingConfirmUpgrade": "Confirm Upgrade",
|
||||||
|
"billingConfirmDowngrade": "Confirm Downgrade",
|
||||||
|
"billingConfirmUpgradeDescription": "You are about to upgrade your plan. Review the new limits and pricing below.",
|
||||||
|
"billingConfirmDowngradeDescription": "You are about to downgrade your plan. Review the new limits and pricing below.",
|
||||||
|
"billingPlanIncludes": "Plan Includes",
|
||||||
|
"billingProcessing": "Processing...",
|
||||||
|
"billingConfirmUpgradeButton": "Confirm Upgrade",
|
||||||
|
"billingConfirmDowngradeButton": "Confirm Downgrade",
|
||||||
|
"billingLimitViolationWarning": "Usage Exceeds New Plan Limits",
|
||||||
|
"billingLimitViolationDescription": "Your current usage exceeds the limits of this plan. After downgrading, all actions will be disabled until you reduce usage within the new limits. Please review the features below that are currently over the limits. Limits in violation:",
|
||||||
|
"billingFeatureLossWarning": "Feature Availability Notice",
|
||||||
|
"billingFeatureLossDescription": "By downgrading, features not available in the new plan will be automatically disabled. Some settings and configurations may be lost. Please review the pricing matrix to understand which features will no longer be available.",
|
||||||
|
"billingUsageExceedsLimit": "Current usage ({current}) exceeds limit ({limit})",
|
||||||
"signUpTerms": {
|
"signUpTerms": {
|
||||||
"IAgreeToThe": "I agree to the",
|
"IAgreeToThe": "I agree to the",
|
||||||
"termsOfService": "terms of service",
|
"termsOfService": "terms of service",
|
||||||
"and": "and",
|
"and": "and",
|
||||||
"privacyPolicy": "privacy policy"
|
"privacyPolicy": "privacy policy."
|
||||||
},
|
},
|
||||||
"signUpMarketing": {
|
"signUpMarketing": {
|
||||||
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
|
"keepMeInTheLoop": "Keep me in the loop with news, updates, and new features by email."
|
||||||
@@ -1476,8 +1594,8 @@
|
|||||||
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
|
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
|
||||||
"selectSites": "Select sites",
|
"selectSites": "Select sites",
|
||||||
"sitesDescription": "The client will have connectivity to the selected sites",
|
"sitesDescription": "The client will have connectivity to the selected sites",
|
||||||
"clientInstallOlm": "Install Olm",
|
"clientInstallOlm": "Install Machine Client",
|
||||||
"clientInstallOlmDescription": "Get Olm running on your system",
|
"clientInstallOlmDescription": "Install the machine client for your system",
|
||||||
"clientOlmCredentials": "Credentials",
|
"clientOlmCredentials": "Credentials",
|
||||||
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
|
"clientOlmCredentialsDescription": "This is how the client will authenticate with the server",
|
||||||
"olmEndpoint": "Endpoint",
|
"olmEndpoint": "Endpoint",
|
||||||
@@ -1501,6 +1619,7 @@
|
|||||||
"addNewTarget": "Add New Target",
|
"addNewTarget": "Add New Target",
|
||||||
"targetsList": "Targets List",
|
"targetsList": "Targets List",
|
||||||
"advancedMode": "Advanced Mode",
|
"advancedMode": "Advanced Mode",
|
||||||
|
"advancedSettings": "Advanced Settings",
|
||||||
"targetErrorDuplicateTargetFound": "Duplicate target found",
|
"targetErrorDuplicateTargetFound": "Duplicate target found",
|
||||||
"healthCheckHealthy": "Healthy",
|
"healthCheckHealthy": "Healthy",
|
||||||
"healthCheckUnhealthy": "Unhealthy",
|
"healthCheckUnhealthy": "Unhealthy",
|
||||||
@@ -1522,6 +1641,8 @@
|
|||||||
"IntervalSeconds": "Healthy Interval",
|
"IntervalSeconds": "Healthy Interval",
|
||||||
"timeoutSeconds": "Timeout (sec)",
|
"timeoutSeconds": "Timeout (sec)",
|
||||||
"timeIsInSeconds": "Time is in seconds",
|
"timeIsInSeconds": "Time is in seconds",
|
||||||
|
"requireDeviceApproval": "Require Device Approvals",
|
||||||
|
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
|
||||||
"retryAttempts": "Retry Attempts",
|
"retryAttempts": "Retry Attempts",
|
||||||
"expectedResponseCodes": "Expected Response Codes",
|
"expectedResponseCodes": "Expected Response Codes",
|
||||||
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
"expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.",
|
||||||
@@ -1562,6 +1683,8 @@
|
|||||||
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
|
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
|
||||||
"resourcesTableDestination": "Destination",
|
"resourcesTableDestination": "Destination",
|
||||||
"resourcesTableAlias": "Alias",
|
"resourcesTableAlias": "Alias",
|
||||||
|
"resourcesTableAliasAddress": "Alias Address",
|
||||||
|
"resourcesTableAliasAddressInfo": "This address is part of the organization's utility subnet. It's used to resolve alias records using internal DNS resolution.",
|
||||||
"resourcesTableClients": "Clients",
|
"resourcesTableClients": "Clients",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
"resourcesTableAndOnlyAccessibleInternally": "and are only accessible internally when connected with a client.",
|
||||||
"resourcesTableNoTargets": "No targets",
|
"resourcesTableNoTargets": "No targets",
|
||||||
@@ -1570,7 +1693,7 @@
|
|||||||
"resourcesTableOffline": "Offline",
|
"resourcesTableOffline": "Offline",
|
||||||
"resourcesTableUnknown": "Unknown",
|
"resourcesTableUnknown": "Unknown",
|
||||||
"resourcesTableNotMonitored": "Not monitored",
|
"resourcesTableNotMonitored": "Not monitored",
|
||||||
"editInternalResourceDialogEditClientResource": "Edit Client Resource",
|
"editInternalResourceDialogEditClientResource": "Edit Private Resource",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "Update the resource configuration and access controls for {resourceName}",
|
"editInternalResourceDialogUpdateResourceProperties": "Update the resource configuration and access controls for {resourceName}",
|
||||||
"editInternalResourceDialogResourceProperties": "Resource Properties",
|
"editInternalResourceDialogResourceProperties": "Resource Properties",
|
||||||
"editInternalResourceDialogName": "Name",
|
"editInternalResourceDialogName": "Name",
|
||||||
@@ -1604,14 +1727,13 @@
|
|||||||
"createInternalResourceDialogNoSitesAvailable": "No Sites Available",
|
"createInternalResourceDialogNoSitesAvailable": "No Sites Available",
|
||||||
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
|
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
|
||||||
"createInternalResourceDialogClose": "Close",
|
"createInternalResourceDialogClose": "Close",
|
||||||
"createInternalResourceDialogCreateClientResource": "Create Client Resource",
|
"createInternalResourceDialogCreateClientResource": "Create Private Resource",
|
||||||
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will only be accessible to clients connected to the organization",
|
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will only be accessible to clients connected to the organization",
|
||||||
"createInternalResourceDialogResourceProperties": "Resource Properties",
|
"createInternalResourceDialogResourceProperties": "Resource Properties",
|
||||||
"createInternalResourceDialogName": "Name",
|
"createInternalResourceDialogName": "Name",
|
||||||
"createInternalResourceDialogSite": "Site",
|
"createInternalResourceDialogSite": "Site",
|
||||||
"createInternalResourceDialogSelectSite": "Select site...",
|
"selectSite": "Select site...",
|
||||||
"createInternalResourceDialogSearchSites": "Search sites...",
|
"noSitesFound": "No sites found.",
|
||||||
"createInternalResourceDialogNoSitesFound": "No sites found.",
|
|
||||||
"createInternalResourceDialogProtocol": "Protocol",
|
"createInternalResourceDialogProtocol": "Protocol",
|
||||||
"createInternalResourceDialogTcp": "TCP",
|
"createInternalResourceDialogTcp": "TCP",
|
||||||
"createInternalResourceDialogUdp": "UDP",
|
"createInternalResourceDialogUdp": "UDP",
|
||||||
@@ -1651,7 +1773,7 @@
|
|||||||
"siteAddressDescription": "The internal address of the site. Must fall within the organization's subnet.",
|
"siteAddressDescription": "The internal address of the site. Must fall within the organization's subnet.",
|
||||||
"siteNameDescription": "The display name of the site that can be changed later.",
|
"siteNameDescription": "The display name of the site that can be changed later.",
|
||||||
"autoLoginExternalIdp": "Auto Login with External IDP",
|
"autoLoginExternalIdp": "Auto Login with External IDP",
|
||||||
"autoLoginExternalIdpDescription": "Immediately redirect the user to the external IDP for authentication.",
|
"autoLoginExternalIdpDescription": "Immediately redirect the user to the external identity provider for authentication.",
|
||||||
"selectIdp": "Select IDP",
|
"selectIdp": "Select IDP",
|
||||||
"selectIdpPlaceholder": "Choose an IDP...",
|
"selectIdpPlaceholder": "Choose an IDP...",
|
||||||
"selectIdpRequired": "Please select an IDP when auto login is enabled.",
|
"selectIdpRequired": "Please select an IDP when auto login is enabled.",
|
||||||
@@ -1663,7 +1785,7 @@
|
|||||||
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||||
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
|
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
|
||||||
"remoteExitNodeDescription": "Self-host one or more remote nodes to extend network connectivity and reduce reliance on the cloud",
|
"remoteExitNodeDescription": "Self-host your own remote relay and proxy server nodes",
|
||||||
"remoteExitNodes": "Nodes",
|
"remoteExitNodes": "Nodes",
|
||||||
"searchRemoteExitNodes": "Search nodes...",
|
"searchRemoteExitNodes": "Search nodes...",
|
||||||
"remoteExitNodeAdd": "Add Node",
|
"remoteExitNodeAdd": "Add Node",
|
||||||
@@ -1673,20 +1795,22 @@
|
|||||||
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
||||||
"remoteExitNodeDelete": "Delete Node",
|
"remoteExitNodeDelete": "Delete Node",
|
||||||
"sidebarRemoteExitNodes": "Remote Nodes",
|
"sidebarRemoteExitNodes": "Remote Nodes",
|
||||||
|
"remoteExitNodeId": "ID",
|
||||||
|
"remoteExitNodeSecretKey": "Secret",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Create Node",
|
"title": "Create Remote Node",
|
||||||
"description": "Create a new node to extend network connectivity",
|
"description": "Create a new self-hosted remote relay and proxy server node",
|
||||||
"viewAllButton": "View All Nodes",
|
"viewAllButton": "View All Nodes",
|
||||||
"strategy": {
|
"strategy": {
|
||||||
"title": "Creation Strategy",
|
"title": "Creation Strategy",
|
||||||
"description": "Choose this to manually configure the node or generate new credentials.",
|
"description": "Select how you want to create the remote node",
|
||||||
"adopt": {
|
"adopt": {
|
||||||
"title": "Adopt Node",
|
"title": "Adopt Node",
|
||||||
"description": "Choose this if you already have the credentials for the node."
|
"description": "Choose this if you already have the credentials for the node."
|
||||||
},
|
},
|
||||||
"generate": {
|
"generate": {
|
||||||
"title": "Generate Keys",
|
"title": "Generate Keys",
|
||||||
"description": "Choose this if you want to generate new keys for the node"
|
"description": "Choose this if you want to generate new keys for the node."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"adopt": {
|
"adopt": {
|
||||||
@@ -1799,9 +1923,33 @@
|
|||||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
||||||
"subnet": "Subnet",
|
"subnet": "Subnet",
|
||||||
"subnetDescription": "The subnet for this organization's network configuration.",
|
"subnetDescription": "The subnet for this organization's network configuration.",
|
||||||
"authPage": "Auth Page",
|
"customDomain": "Custom Domain",
|
||||||
"authPageDescription": "Configure the auth page for the organization",
|
"authPage": "Authentication Pages",
|
||||||
|
"authPageDescription": "Set a custom domain for the organization's authentication pages",
|
||||||
"authPageDomain": "Auth Page Domain",
|
"authPageDomain": "Auth Page Domain",
|
||||||
|
"authPageBranding": "Custom Branding",
|
||||||
|
"authPageBrandingDescription": "Configure the branding that appears on authentication pages for this organization",
|
||||||
|
"authPageBrandingUpdated": "Auth page Branding updated successfully",
|
||||||
|
"authPageBrandingRemoved": "Auth page Branding removed successfully",
|
||||||
|
"authPageBrandingRemoveTitle": "Remove Auth Page Branding",
|
||||||
|
"authPageBrandingQuestionRemove": "Are you sure you want to remove the branding for Auth Pages ?",
|
||||||
|
"authPageBrandingDeleteConfirm": "Confirm Delete Branding",
|
||||||
|
"brandingLogoURL": "Logo URL",
|
||||||
|
"brandingLogoURLOrPath": "Logo URL or Path",
|
||||||
|
"brandingLogoPathDescription": "Enter a URL or a local path.",
|
||||||
|
"brandingLogoURLDescription": "Enter a publicly accessible URL to your logo image.",
|
||||||
|
"brandingPrimaryColor": "Primary Color",
|
||||||
|
"brandingLogoWidth": "Width (px)",
|
||||||
|
"brandingLogoHeight": "Height (px)",
|
||||||
|
"brandingOrgTitle": "Title for Organization Auth Page",
|
||||||
|
"brandingOrgDescription": "{orgName} will be replaced with the organization's name",
|
||||||
|
"brandingOrgSubtitle": "Subtitle for Organization Auth Page",
|
||||||
|
"brandingResourceTitle": "Title for Resource Auth Page",
|
||||||
|
"brandingResourceSubtitle": "Subtitle for Resource Auth Page",
|
||||||
|
"brandingResourceDescription": "{resourceName} will be replaced with the organization's name",
|
||||||
|
"saveAuthPageDomain": "Save Domain",
|
||||||
|
"saveAuthPageBranding": "Save Branding",
|
||||||
|
"removeAuthPageBranding": "Remove Branding",
|
||||||
"noDomainSet": "No domain set",
|
"noDomainSet": "No domain set",
|
||||||
"changeDomain": "Change Domain",
|
"changeDomain": "Change Domain",
|
||||||
"selectDomain": "Select Domain",
|
"selectDomain": "Select Domain",
|
||||||
@@ -1810,7 +1958,7 @@
|
|||||||
"setAuthPageDomain": "Set Auth Page Domain",
|
"setAuthPageDomain": "Set Auth Page Domain",
|
||||||
"failedToFetchCertificate": "Failed to fetch certificate",
|
"failedToFetchCertificate": "Failed to fetch certificate",
|
||||||
"failedToRestartCertificate": "Failed to restart certificate",
|
"failedToRestartCertificate": "Failed to restart certificate",
|
||||||
"addDomainToEnableCustomAuthPages": "Add a domain to enable custom authentication pages for the organization",
|
"addDomainToEnableCustomAuthPages": "Users will be able to access the organization's login page and complete resource authentication using this domain.",
|
||||||
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
|
"selectDomainForOrgAuthPage": "Select a domain for the organization's authentication page",
|
||||||
"domainPickerProvidedDomain": "Provided Domain",
|
"domainPickerProvidedDomain": "Provided Domain",
|
||||||
"domainPickerFreeProvidedDomain": "Free Provided Domain",
|
"domainPickerFreeProvidedDomain": "Free Provided Domain",
|
||||||
@@ -1825,11 +1973,27 @@
|
|||||||
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.",
|
"domainPickerInvalidSubdomainCannotMakeValid": "\"{sub}\" could not be made valid for {domain}.",
|
||||||
"domainPickerSubdomainSanitized": "Subdomain sanitized",
|
"domainPickerSubdomainSanitized": "Subdomain sanitized",
|
||||||
"domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"",
|
"domainPickerSubdomainCorrected": "\"{sub}\" was corrected to \"{sanitized}\"",
|
||||||
"orgAuthSignInTitle": "Sign in to the organization",
|
"orgAuthSignInTitle": "Organization Sign In",
|
||||||
"orgAuthChooseIdpDescription": "Choose your identity provider to continue",
|
"orgAuthChooseIdpDescription": "Choose your identity provider to continue",
|
||||||
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
|
"orgAuthNoIdpConfigured": "This organization doesn't have any identity providers configured. You can log in with your Pangolin identity instead.",
|
||||||
"orgAuthSignInWithPangolin": "Sign in with Pangolin",
|
"orgAuthSignInWithPangolin": "Sign in with Pangolin",
|
||||||
|
"orgAuthSignInToOrg": "Sign in to an organization",
|
||||||
|
"orgAuthSelectOrgTitle": "Organization Sign In",
|
||||||
|
"orgAuthSelectOrgDescription": "Enter your organization ID to continue",
|
||||||
|
"orgAuthOrgIdPlaceholder": "your-organization",
|
||||||
|
"orgAuthOrgIdHelp": "Enter your organization's unique identifier",
|
||||||
|
"orgAuthSelectOrgHelp": "After entering your organization ID, you'll be taken to your organization's sign-in page where you can use SSO or your organization credentials.",
|
||||||
|
"orgAuthRememberOrgId": "Remember this organization ID",
|
||||||
|
"orgAuthBackToSignIn": "Back to standard sign in",
|
||||||
|
"orgAuthNoAccount": "Don't have an account?",
|
||||||
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
|
"subscriptionRequiredToUse": "A subscription is required to use this feature.",
|
||||||
|
"mustUpgradeToUse": "You must upgrade your subscription to use this feature.",
|
||||||
|
"subscriptionRequiredTierToUse": "This feature requires <tierLink>{tier}</tierLink> or higher.",
|
||||||
|
"upgradeToTierToUse": "Upgrade to <tierLink>{tier}</tierLink> or higher to use this feature.",
|
||||||
|
"subscriptionTierTier1": "Home",
|
||||||
|
"subscriptionTierTier2": "Team",
|
||||||
|
"subscriptionTierTier3": "Business",
|
||||||
|
"subscriptionTierEnterprise": "Enterprise",
|
||||||
"idpDisabled": "Identity providers are disabled.",
|
"idpDisabled": "Identity providers are disabled.",
|
||||||
"orgAuthPageDisabled": "Organization auth page is disabled.",
|
"orgAuthPageDisabled": "Organization auth page is disabled.",
|
||||||
"domainRestartedDescription": "Domain verification restarted successfully",
|
"domainRestartedDescription": "Domain verification restarted successfully",
|
||||||
@@ -1843,6 +2007,8 @@
|
|||||||
"enableTwoFactorAuthentication": "Enable two-factor authentication",
|
"enableTwoFactorAuthentication": "Enable two-factor authentication",
|
||||||
"completeSecuritySteps": "Complete Security Steps",
|
"completeSecuritySteps": "Complete Security Steps",
|
||||||
"securitySettings": "Security Settings",
|
"securitySettings": "Security Settings",
|
||||||
|
"dangerSection": "Danger Zone",
|
||||||
|
"dangerSectionDescription": "Permanently delete all data associated with this organization",
|
||||||
"securitySettingsDescription": "Configure security policies for the organization",
|
"securitySettingsDescription": "Configure security policies for the organization",
|
||||||
"requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users",
|
"requireTwoFactorForAllUsers": "Require Two-Factor Authentication for All Users",
|
||||||
"requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.",
|
"requireTwoFactorDescription": "When enabled, all internal users in this organization must have two-factor authentication enabled to access the organization.",
|
||||||
@@ -1880,7 +2046,7 @@
|
|||||||
"securityPolicyChangeWarningText": "This will affect all users in the organization",
|
"securityPolicyChangeWarningText": "This will affect all users in the organization",
|
||||||
"authPageErrorUpdateMessage": "An error occurred while updating the auth page settings",
|
"authPageErrorUpdateMessage": "An error occurred while updating the auth page settings",
|
||||||
"authPageErrorUpdate": "Unable to update auth page",
|
"authPageErrorUpdate": "Unable to update auth page",
|
||||||
"authPageUpdated": "Auth page updated successfully",
|
"authPageDomainUpdated": "Auth page Domain updated successfully",
|
||||||
"healthCheckNotAvailable": "Local",
|
"healthCheckNotAvailable": "Local",
|
||||||
"rewritePath": "Rewrite Path",
|
"rewritePath": "Rewrite Path",
|
||||||
"rewritePathDescription": "Optionally rewrite the path before forwarding to the target.",
|
"rewritePathDescription": "Optionally rewrite the path before forwarding to the target.",
|
||||||
@@ -1908,8 +2074,15 @@
|
|||||||
"beta": "Beta",
|
"beta": "Beta",
|
||||||
"manageUserDevices": "User Devices",
|
"manageUserDevices": "User Devices",
|
||||||
"manageUserDevicesDescription": "View and manage devices that users use to privately connect to resources",
|
"manageUserDevicesDescription": "View and manage devices that users use to privately connect to resources",
|
||||||
|
"downloadClientBannerTitle": "Download Pangolin Client",
|
||||||
|
"downloadClientBannerDescription": "Download the Pangolin client for your system to connect to the Pangolin network and access resources privately.",
|
||||||
"manageMachineClients": "Manage Machine Clients",
|
"manageMachineClients": "Manage Machine Clients",
|
||||||
"manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources",
|
"manageMachineClientsDescription": "Create and manage clients that servers and systems use to privately connect to resources",
|
||||||
|
"machineClientsBannerTitle": "Servers & Automated Systems",
|
||||||
|
"machineClientsBannerDescription": "Machine clients are for servers and automated systems that are not associated with a specific user. They authenticate with an ID and secret, and can run with Pangolin CLI, Olm CLI, or Olm as a container.",
|
||||||
|
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||||
|
"machineClientsBannerOlmCLI": "Olm CLI",
|
||||||
|
"machineClientsBannerOlmContainer": "Container",
|
||||||
"clientsTableUserClients": "User",
|
"clientsTableUserClients": "User",
|
||||||
"clientsTableMachineClients": "Machine",
|
"clientsTableMachineClients": "Machine",
|
||||||
"licenseTableValidUntil": "Valid Until",
|
"licenseTableValidUntil": "Valid Until",
|
||||||
@@ -2008,6 +2181,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"newPricingLicenseForm": {
|
||||||
|
"title": "Get a license",
|
||||||
|
"description": "Choose a plan and tell us how you plan to use Pangolin.",
|
||||||
|
"chooseTier": "Choose your plan",
|
||||||
|
"viewPricingLink": "See pricing, features, and limits",
|
||||||
|
"tiers": {
|
||||||
|
"starter": {
|
||||||
|
"title": "Starter",
|
||||||
|
"description": "Enterprise features, 25 users, 25 sites, and community support."
|
||||||
|
},
|
||||||
|
"scale": {
|
||||||
|
"title": "Scale",
|
||||||
|
"description": "Enterprise features, 50 users, 50 sites, and priority support."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"personalUseOnly": "Personal use only (free license — no checkout)",
|
||||||
|
"buttons": {
|
||||||
|
"continueToCheckout": "Continue to Checkout"
|
||||||
|
},
|
||||||
|
"toasts": {
|
||||||
|
"checkoutError": {
|
||||||
|
"title": "Checkout error",
|
||||||
|
"description": "Could not start checkout. Please try again."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"priority": "Priority",
|
"priority": "Priority",
|
||||||
"priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.",
|
"priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.",
|
||||||
"instanceName": "Instance Name",
|
"instanceName": "Instance Name",
|
||||||
@@ -2046,20 +2245,22 @@
|
|||||||
"pathRewriteStripLabel": "strip",
|
"pathRewriteStripLabel": "strip",
|
||||||
"sidebarEnableEnterpriseLicense": "Enable Enterprise License",
|
"sidebarEnableEnterpriseLicense": "Enable Enterprise License",
|
||||||
"cannotbeUndone": "This can not be undone.",
|
"cannotbeUndone": "This can not be undone.",
|
||||||
"toConfirm": "to confirm",
|
"toConfirm": "to confirm.",
|
||||||
"deleteClientQuestion": "Are you sure you want to remove the client from the site and organization?",
|
"deleteClientQuestion": "Are you sure you want to remove the client from the site and organization?",
|
||||||
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.",
|
"clientMessageRemove": "Once removed, the client will no longer be able to connect to the site.",
|
||||||
"sidebarLogs": "Logs",
|
"sidebarLogs": "Logs",
|
||||||
"request": "Request",
|
"request": "Request",
|
||||||
"requests": "Requests",
|
"requests": "Requests",
|
||||||
"logs": "Logs",
|
"logs": "Logs",
|
||||||
"logsSettingsDescription": "Monitor logs collected from this orginization",
|
"logsSettingsDescription": "Monitor logs collected from this organization",
|
||||||
"searchLogs": "Search logs...",
|
"searchLogs": "Search logs...",
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
"actor": "Actor",
|
"actor": "Actor",
|
||||||
"timestamp": "Timestamp",
|
"timestamp": "Timestamp",
|
||||||
"accessLogs": "Access Logs",
|
"accessLogs": "Access Logs",
|
||||||
"exportCsv": "Export CSV",
|
"exportCsv": "Export CSV",
|
||||||
|
"exportError": "Unknown error when exporting CSV",
|
||||||
|
"exportCsvTooltip": "Within Time Range",
|
||||||
"actorId": "Actor ID",
|
"actorId": "Actor ID",
|
||||||
"allowedByRule": "Allowed by Rule",
|
"allowedByRule": "Allowed by Rule",
|
||||||
"allowedNoAuth": "Allowed No Auth",
|
"allowedNoAuth": "Allowed No Auth",
|
||||||
@@ -2101,9 +2302,11 @@
|
|||||||
"logRetention30Days": "30 days",
|
"logRetention30Days": "30 days",
|
||||||
"logRetention90Days": "90 days",
|
"logRetention90Days": "90 days",
|
||||||
"logRetentionForever": "Forever",
|
"logRetentionForever": "Forever",
|
||||||
|
"logRetentionEndOfFollowingYear": "End of following year",
|
||||||
"actionLogsDescription": "View a history of actions performed in this organization",
|
"actionLogsDescription": "View a history of actions performed in this organization",
|
||||||
"accessLogsDescription": "View access auth requests for resources in this organization",
|
"accessLogsDescription": "View access auth requests for resources in this organization",
|
||||||
"licenseRequiredToUse": "An Enterprise license is required to use this feature.",
|
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
||||||
|
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>.",
|
||||||
"certResolver": "Certificate Resolver",
|
"certResolver": "Certificate Resolver",
|
||||||
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
"certResolverDescription": "Select the certificate resolver to use for this resource.",
|
||||||
"selectCertResolver": "Select Certificate Resolver",
|
"selectCertResolver": "Select Certificate Resolver",
|
||||||
@@ -2112,7 +2315,7 @@
|
|||||||
"unverified": "Unverified",
|
"unverified": "Unverified",
|
||||||
"domainSetting": "Domain Settings",
|
"domainSetting": "Domain Settings",
|
||||||
"domainSettingDescription": "Configure settings for the domain",
|
"domainSettingDescription": "Configure settings for the domain",
|
||||||
"preferWildcardCertDescription": "Attempt to generate a wildcard certificate (require a properly configured certificate resolver).",
|
"preferWildcardCertDescription": "Attempt to generate a wildcard certificate (requires a properly configured certificate resolver).",
|
||||||
"recordName": "Record Name",
|
"recordName": "Record Name",
|
||||||
"auto": "Auto",
|
"auto": "Auto",
|
||||||
"TTL": "TTL",
|
"TTL": "TTL",
|
||||||
@@ -2164,6 +2367,8 @@
|
|||||||
"deviceCodeInvalidFormat": "Code must be 9 characters (e.g., A1AJ-N5JD)",
|
"deviceCodeInvalidFormat": "Code must be 9 characters (e.g., A1AJ-N5JD)",
|
||||||
"deviceCodeInvalidOrExpired": "Invalid or expired code",
|
"deviceCodeInvalidOrExpired": "Invalid or expired code",
|
||||||
"deviceCodeVerifyFailed": "Failed to verify device code",
|
"deviceCodeVerifyFailed": "Failed to verify device code",
|
||||||
|
"deviceCodeValidating": "Validating device code...",
|
||||||
|
"deviceCodeVerifying": "Verifying device authorization...",
|
||||||
"signedInAs": "Signed in as",
|
"signedInAs": "Signed in as",
|
||||||
"deviceCodeEnterPrompt": "Enter the code displayed on the device",
|
"deviceCodeEnterPrompt": "Enter the code displayed on the device",
|
||||||
"continue": "Continue",
|
"continue": "Continue",
|
||||||
@@ -2176,7 +2381,7 @@
|
|||||||
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
|
"deviceOrganizationsAccess": "Access to all organizations your account has access to",
|
||||||
"deviceAuthorize": "Authorize {applicationName}",
|
"deviceAuthorize": "Authorize {applicationName}",
|
||||||
"deviceConnected": "Device Connected!",
|
"deviceConnected": "Device Connected!",
|
||||||
"deviceAuthorizedMessage": "Device is authorized to access your account.",
|
"deviceAuthorizedMessage": "Device is authorized to access your account. Please return to the client application.",
|
||||||
"pangolinCloud": "Pangolin Cloud",
|
"pangolinCloud": "Pangolin Cloud",
|
||||||
"viewDevices": "View Devices",
|
"viewDevices": "View Devices",
|
||||||
"viewDevicesDescription": "Manage your connected devices",
|
"viewDevicesDescription": "Manage your connected devices",
|
||||||
@@ -2213,8 +2418,8 @@
|
|||||||
"regenerate": "Regenerate",
|
"regenerate": "Regenerate",
|
||||||
"credentials": "Credentials",
|
"credentials": "Credentials",
|
||||||
"savecredentials": "Save Credentials",
|
"savecredentials": "Save Credentials",
|
||||||
"regeneratecredentials": "Re-key",
|
"regenerateCredentialsButton": "Regenerate Credentials",
|
||||||
"regenerateCredentials": "Regenerate and save your credentials",
|
"regenerateCredentials": "Regenerate Credentials",
|
||||||
"generatedcredentials": "Generated Credentials",
|
"generatedcredentials": "Generated Credentials",
|
||||||
"copyandsavethesecredentials": "Copy and save these credentials",
|
"copyandsavethesecredentials": "Copy and save these credentials",
|
||||||
"copyandsavethesecredentialsdescription": "These credentials will not be shown again after you leave this page. Save them securely now.",
|
"copyandsavethesecredentialsdescription": "These credentials will not be shown again after you leave this page. Save them securely now.",
|
||||||
@@ -2222,13 +2427,12 @@
|
|||||||
"credentialsSavedDescription": "Credentials have been regenerated and saved successfully.",
|
"credentialsSavedDescription": "Credentials have been regenerated and saved successfully.",
|
||||||
"credentialsSaveError": "Credentials Save Error",
|
"credentialsSaveError": "Credentials Save Error",
|
||||||
"credentialsSaveErrorDescription": "An error occurred while regenerating and saving the credentials.",
|
"credentialsSaveErrorDescription": "An error occurred while regenerating and saving the credentials.",
|
||||||
"regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones. Make sure to update any configurations that use these credentials.",
|
"regenerateCredentialsWarning": "Regenerating credentials will invalidate the previous ones and cause a disconnection. Make sure to update any configurations that use these credentials.",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"regenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials?",
|
"regenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials?",
|
||||||
"endpoint": "Endpoint",
|
"endpoint": "Endpoint",
|
||||||
"Id": "Id",
|
"Id": "Id",
|
||||||
"SecretKey": "Secret Key",
|
"SecretKey": "Secret Key",
|
||||||
"featureDisabledTooltip": "This feature is only available in the enterprise plan and require a license to use it.",
|
|
||||||
"niceId": "Nice ID",
|
"niceId": "Nice ID",
|
||||||
"niceIdUpdated": "Nice ID Updated",
|
"niceIdUpdated": "Nice ID Updated",
|
||||||
"niceIdUpdatedSuccessfully": "Nice ID Updated Successfully",
|
"niceIdUpdatedSuccessfully": "Nice ID Updated Successfully",
|
||||||
@@ -2239,6 +2443,7 @@
|
|||||||
"identifier": "Identifier",
|
"identifier": "Identifier",
|
||||||
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
|
"deviceLoginUseDifferentAccount": "Not you? Use a different account.",
|
||||||
"deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account.",
|
"deviceLoginDeviceRequestingAccessToAccount": "A device is requesting access to this account.",
|
||||||
|
"loginSelectAuthenticationMethod": "Select an authentication method to continue.",
|
||||||
"noData": "No Data",
|
"noData": "No Data",
|
||||||
"machineClients": "Machine Clients",
|
"machineClients": "Machine Clients",
|
||||||
"install": "Install",
|
"install": "Install",
|
||||||
@@ -2247,5 +2452,185 @@
|
|||||||
"clientAddress": "Client Address (Advanced)",
|
"clientAddress": "Client Address (Advanced)",
|
||||||
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
|
"setupFailedToFetchSubnet": "Failed to fetch default subnet",
|
||||||
"setupSubnetAdvanced": "Subnet (Advanced)",
|
"setupSubnetAdvanced": "Subnet (Advanced)",
|
||||||
"setupSubnetDescription": "The subnet for this organization's internal network."
|
"setupSubnetDescription": "The subnet for this organization's internal network.",
|
||||||
|
"setupUtilitySubnet": "Utility Subnet (Advanced)",
|
||||||
|
"setupUtilitySubnetDescription": "The subnet for this organization's alias addresses and DNS server.",
|
||||||
|
"siteRegenerateAndDisconnect": "Regenerate and Disconnect",
|
||||||
|
"siteRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this site?",
|
||||||
|
"siteRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the site. The site will need to be restarted with the new credentials.",
|
||||||
|
"siteRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this site?",
|
||||||
|
"siteRegenerateCredentialsWarning": "This will regenerate the credentials. The site will stay connected until you manually restart it and use the new credentials.",
|
||||||
|
"clientRegenerateAndDisconnect": "Regenerate and Disconnect",
|
||||||
|
"clientRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this client?",
|
||||||
|
"clientRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the client. The client will need to be restarted with the new credentials.",
|
||||||
|
"clientRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this client?",
|
||||||
|
"clientRegenerateCredentialsWarning": "This will regenerate the credentials. The client will stay connected until you manually restart it and use the new credentials.",
|
||||||
|
"remoteExitNodeRegenerateAndDisconnect": "Regenerate and Disconnect",
|
||||||
|
"remoteExitNodeRegenerateAndDisconnectConfirmation": "Are you sure you want to regenerate the credentials and disconnect this remote exit node?",
|
||||||
|
"remoteExitNodeRegenerateAndDisconnectWarning": "This will regenerate the credentials and immediately disconnect the remote exit node. The remote exit node will need to be restarted with the new credentials.",
|
||||||
|
"remoteExitNodeRegenerateCredentialsConfirmation": "Are you sure you want to regenerate the credentials for this remote exit node?",
|
||||||
|
"remoteExitNodeRegenerateCredentialsWarning": "This will regenerate the credentials. The remote exit node will stay connected until you manually restart it and use the new credentials.",
|
||||||
|
"agent": "Agent",
|
||||||
|
"personalUseOnly": "Personal Use Only",
|
||||||
|
"loginPageLicenseWatermark": "This instance is licensed for personal use only.",
|
||||||
|
"instanceIsUnlicensed": "This instance is unlicensed.",
|
||||||
|
"portRestrictions": "Port Restrictions",
|
||||||
|
"allPorts": "All",
|
||||||
|
"custom": "Custom",
|
||||||
|
"allPortsAllowed": "All Ports Allowed",
|
||||||
|
"allPortsBlocked": "All Ports Blocked",
|
||||||
|
"tcpPortsDescription": "Specify which TCP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 80,443,8000-9000).",
|
||||||
|
"udpPortsDescription": "Specify which UDP ports are allowed for this resource. Use '*' for all ports, leave empty to block all, or enter a comma-separated list of ports and ranges (e.g., 53,123,500-600).",
|
||||||
|
"organizationLoginPageTitle": "Organization Login Page",
|
||||||
|
"organizationLoginPageDescription": "Customize the login page for this organization",
|
||||||
|
"resourceLoginPageTitle": "Resource Login Page",
|
||||||
|
"resourceLoginPageDescription": "Customize the login page for individual resources",
|
||||||
|
"enterConfirmation": "Enter confirmation",
|
||||||
|
"blueprintViewDetails": "Details",
|
||||||
|
"defaultIdentityProvider": "Default Identity Provider",
|
||||||
|
"defaultIdentityProviderDescription": "When a default identity provider is selected, the user will be automatically redirected to the provider for authentication.",
|
||||||
|
"editInternalResourceDialogNetworkSettings": "Network Settings",
|
||||||
|
"editInternalResourceDialogAccessPolicy": "Access Policy",
|
||||||
|
"editInternalResourceDialogAddRoles": "Add Roles",
|
||||||
|
"editInternalResourceDialogAddUsers": "Add Users",
|
||||||
|
"editInternalResourceDialogAddClients": "Add Clients",
|
||||||
|
"editInternalResourceDialogDestinationLabel": "Destination",
|
||||||
|
"editInternalResourceDialogDestinationDescription": "Specify the destination address for the internal resource. This can be a hostname, IP address, or CIDR range depending on the selected mode. Optionally set an internal DNS alias for easier identification.",
|
||||||
|
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
|
||||||
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
|
"editInternalResourceDialogIcmp": "ICMP",
|
||||||
|
"editInternalResourceDialogAccessControl": "Access Control",
|
||||||
|
"editInternalResourceDialogAccessControlDescription": "Control which roles, users, and machine clients have access to this resource when connected. Admins always have access.",
|
||||||
|
"editInternalResourceDialogPortRangeValidationError": "Port range must be \"*\" for all ports, or a comma-separated list of ports and ranges (e.g., \"80,443,8000-9000\"). Ports must be between 1 and 65535.",
|
||||||
|
"orgAuthWhatsThis": "Where can I find my organization ID?",
|
||||||
|
"learnMore": "Learn more",
|
||||||
|
"backToHome": "Go back to home",
|
||||||
|
"needToSignInToOrg": "Need to use your organization's identity provider?",
|
||||||
|
"maintenanceMode": "Maintenance Mode",
|
||||||
|
"maintenanceModeDescription": "Display a maintenance page to visitors",
|
||||||
|
"maintenanceModeType": "Maintenance Mode Type",
|
||||||
|
"showMaintenancePage": "Show a maintenance page to visitors",
|
||||||
|
"enableMaintenanceMode": "Enable Maintenance Mode",
|
||||||
|
"automatic": "Automatic",
|
||||||
|
"automaticModeDescription": " Show maintenance page only when all backend targets are down or unhealthy. Your resource continues working normally as long as at least one target is healthy.",
|
||||||
|
"forced": "Forced",
|
||||||
|
"forcedModeDescription": "Always show the maintenance page regardless of backend health. Use this for planned maintenance when you want to prevent all access.",
|
||||||
|
"warning:" : "Warning:",
|
||||||
|
"forcedeModeWarning": "All traffic will be directed to the maintenance page. Your backend resources will not receive any requests.",
|
||||||
|
"pageTitle": "Page Title",
|
||||||
|
"pageTitleDescription": "The main heading displayed on the maintenance page",
|
||||||
|
"maintenancePageMessage": "Maintenance Message",
|
||||||
|
"maintenancePageMessagePlaceholder": "We'll be back soon! Our site is currently undergoing scheduled maintenance.",
|
||||||
|
"maintenancePageMessageDescription": "Detailed message explaining the maintenance",
|
||||||
|
"maintenancePageTimeTitle": "Estimated Completion Time (Optional)",
|
||||||
|
"maintenanceTime": "e.g., 2 hours, Nov 1 at 5:00 PM",
|
||||||
|
"maintenanceEstimatedTimeDescription": "When you expect maintenance to be completed",
|
||||||
|
"editDomain": "Edit Domain",
|
||||||
|
"editDomainDescription": "Select a domain for your resource",
|
||||||
|
"maintenanceModeDisabledTooltip": "This feature requires a valid license to enable.",
|
||||||
|
"maintenanceScreenTitle": "Service Temporarily Unavailable",
|
||||||
|
"maintenanceScreenMessage": "We are currently experiencing technical difficulties. Please check back soon.",
|
||||||
|
"maintenanceScreenEstimatedCompletion": "Estimated Completion:",
|
||||||
|
"createInternalResourceDialogDestinationRequired": "Destination is required",
|
||||||
|
"available": "Available",
|
||||||
|
"archived": "Archived",
|
||||||
|
"noArchivedDevices": "No archived devices found",
|
||||||
|
"deviceArchived": "Device archived",
|
||||||
|
"deviceArchivedDescription": "The device has been successfully archived.",
|
||||||
|
"errorArchivingDevice": "Error archiving device",
|
||||||
|
"failedToArchiveDevice": "Failed to archive device",
|
||||||
|
"deviceQuestionArchive": "Are you sure you want to archive this device?",
|
||||||
|
"deviceMessageArchive": "The device will be archived and removed from your active devices list.",
|
||||||
|
"deviceArchiveConfirm": "Archive Device",
|
||||||
|
"archiveDevice": "Archive Device",
|
||||||
|
"archive": "Archive",
|
||||||
|
"deviceUnarchived": "Device unarchived",
|
||||||
|
"deviceUnarchivedDescription": "The device has been successfully unarchived.",
|
||||||
|
"errorUnarchivingDevice": "Error unarchiving device",
|
||||||
|
"failedToUnarchiveDevice": "Failed to unarchive device",
|
||||||
|
"unarchive": "Unarchive",
|
||||||
|
"archiveClient": "Archive Client",
|
||||||
|
"archiveClientQuestion": "Are you sure you want to archive this client?",
|
||||||
|
"archiveClientMessage": "The client will be archived and removed from your active clients list.",
|
||||||
|
"archiveClientConfirm": "Archive Client",
|
||||||
|
"blockClient": "Block Client",
|
||||||
|
"blockClientQuestion": "Are you sure you want to block this client?",
|
||||||
|
"blockClientMessage": "The device will be forced to disconnect if currently connected. You can unblock the device later.",
|
||||||
|
"blockClientConfirm": "Block Client",
|
||||||
|
"active": "Active",
|
||||||
|
"usernameOrEmail": "Username or Email",
|
||||||
|
"selectYourOrganization": "Select your organization",
|
||||||
|
"signInTo": "Log in in to",
|
||||||
|
"signInWithPassword": "Continue with Password",
|
||||||
|
"noAuthMethodsAvailable": "No authentication methods available for this organization.",
|
||||||
|
"enterPassword": "Enter your password",
|
||||||
|
"enterMfaCode": "Enter the code from your authenticator app",
|
||||||
|
"securityKeyRequired": "Please use your security key to sign in.",
|
||||||
|
"needToUseAnotherAccount": "Need to use a different account?",
|
||||||
|
"loginLegalDisclaimer": "By clicking the buttons below, you acknowledge you have read, understand, and agree to the <termsOfService>Terms of Service</termsOfService> and <privacyPolicy>Privacy Policy</privacyPolicy>.",
|
||||||
|
"termsOfService": "Terms of Service",
|
||||||
|
"privacyPolicy": "Privacy Policy",
|
||||||
|
"userNotFoundWithUsername": "No user found with that username.",
|
||||||
|
"verify": "Verify",
|
||||||
|
"signIn": "Sign In",
|
||||||
|
"forgotPassword": "Forgot password?",
|
||||||
|
"orgSignInTip": "If you've logged in before, you can enter your username or email above to authenticate with your organization's identity provider instead. It's easier!",
|
||||||
|
"continueAnyway": "Continue anyway",
|
||||||
|
"dontShowAgain": "Don't show again",
|
||||||
|
"orgSignInNotice": "Did you know?",
|
||||||
|
"signupOrgNotice": "Trying to sign in?",
|
||||||
|
"signupOrgTip": "Are you trying to sign in through your organization's identity provider?",
|
||||||
|
"signupOrgLink": "Sign in or sign up with your organization instead",
|
||||||
|
"verifyEmailLogInWithDifferentAccount": "Use a Different Account",
|
||||||
|
"logIn": "Log In",
|
||||||
|
"deviceInformation": "Device Information",
|
||||||
|
"deviceInformationDescription": "Information about the device and agent",
|
||||||
|
"deviceSecurity": "Device Security",
|
||||||
|
"deviceSecurityDescription": "Device security posture information",
|
||||||
|
"platform": "Platform",
|
||||||
|
"macosVersion": "macOS Version",
|
||||||
|
"windowsVersion": "Windows Version",
|
||||||
|
"iosVersion": "iOS Version",
|
||||||
|
"androidVersion": "Android Version",
|
||||||
|
"osVersion": "OS Version",
|
||||||
|
"kernelVersion": "Kernel Version",
|
||||||
|
"deviceModel": "Device Model",
|
||||||
|
"serialNumber": "Serial Number",
|
||||||
|
"hostname": "Hostname",
|
||||||
|
"firstSeen": "First Seen",
|
||||||
|
"lastSeen": "Last Seen",
|
||||||
|
"biometricsEnabled": "Biometrics Enabled",
|
||||||
|
"diskEncrypted": "Disk Encrypted",
|
||||||
|
"firewallEnabled": "Firewall Enabled",
|
||||||
|
"autoUpdatesEnabled": "Auto Updates Enabled",
|
||||||
|
"tpmAvailable": "TPM Available",
|
||||||
|
"windowsAntivirusEnabled": "Antivirus Enabled",
|
||||||
|
"macosSipEnabled": "System Integrity Protection (SIP)",
|
||||||
|
"macosGatekeeperEnabled": "Gatekeeper",
|
||||||
|
"macosFirewallStealthMode": "Firewall Stealth Mode",
|
||||||
|
"linuxAppArmorEnabled": "AppArmor",
|
||||||
|
"linuxSELinuxEnabled": "SELinux",
|
||||||
|
"deviceSettingsDescription": "View device information and settings",
|
||||||
|
"devicePendingApprovalDescription": "This device is waiting for approval",
|
||||||
|
"deviceBlockedDescription": "This device is currently blocked. It won't be able to connect to any resources unless unblocked.",
|
||||||
|
"unblockClient": "Unblock Client",
|
||||||
|
"unblockClientDescription": "The device has been unblocked",
|
||||||
|
"unarchiveClient": "Unarchive Client",
|
||||||
|
"unarchiveClientDescription": "The device has been unarchived",
|
||||||
|
"block": "Block",
|
||||||
|
"unblock": "Unblock",
|
||||||
|
"deviceActions": "Device Actions",
|
||||||
|
"deviceActionsDescription": "Manage device status and access",
|
||||||
|
"devicePendingApprovalBannerDescription": "This device is pending approval. It won't be able to connect to resources until approved.",
|
||||||
|
"connected": "Connected",
|
||||||
|
"disconnected": "Disconnected",
|
||||||
|
"approvalsEmptyStateTitle": "Device Approvals Not Enabled",
|
||||||
|
"approvalsEmptyStateDescription": "Enable device approvals for roles to require admin approval before users can connect new devices.",
|
||||||
|
"approvalsEmptyStateStep1Title": "Go to Roles",
|
||||||
|
"approvalsEmptyStateStep1Description": "Navigate to your organization's roles settings to configure device approvals.",
|
||||||
|
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
|
||||||
|
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
|
||||||
|
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
|
||||||
|
"approvalsEmptyStateButtonText": "Manage Roles"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"setupCreate": "創建您的第一個組織、網站和資源",
|
"setupCreate": "創建您的第一個組織、網站和資源",
|
||||||
|
"headerAuthCompatibilityInfo": "啟用此選項以在缺少驗證令牌時強制回傳 401 未授權回應。這對於不會在沒有伺服器挑戰的情況下發送憑證的瀏覽器或特定 HTTP 函式庫是必需的。",
|
||||||
|
"headerAuthCompatibility": "擴展相容性",
|
||||||
"setupNewOrg": "新建組織",
|
"setupNewOrg": "新建組織",
|
||||||
"setupCreateOrg": "創建組織",
|
"setupCreateOrg": "創建組織",
|
||||||
"setupCreateResources": "創建資源",
|
"setupCreateResources": "創建資源",
|
||||||
@@ -51,6 +53,9 @@
|
|||||||
"siteQuestionRemove": "您確定要從組織中刪除該站點嗎?",
|
"siteQuestionRemove": "您確定要從組織中刪除該站點嗎?",
|
||||||
"siteManageSites": "管理站點",
|
"siteManageSites": "管理站點",
|
||||||
"siteDescription": "允許通過安全隧道連接到您的網路",
|
"siteDescription": "允許通過安全隧道連接到您的網路",
|
||||||
|
"sitesBannerTitle": "連接任何網路",
|
||||||
|
"sitesBannerDescription": "站點是與遠端網路的連接,使 Pangolin 能夠為任何地方的使用者提供對公共或私有資源的存取。在任何可以執行二進位檔案或容器的地方安裝站點網路連接器 (Newt) 以建立連接。",
|
||||||
|
"sitesBannerButtonText": "安裝站點",
|
||||||
"siteCreate": "創建站點",
|
"siteCreate": "創建站點",
|
||||||
"siteCreateDescription2": "按照下面的步驟創建和連接一個新站點",
|
"siteCreateDescription2": "按照下面的步驟創建和連接一個新站點",
|
||||||
"siteCreateDescription": "創建一個新站點開始連接您的資源",
|
"siteCreateDescription": "創建一個新站點開始連接您的資源",
|
||||||
@@ -65,8 +70,8 @@
|
|||||||
"siteLoadWGConfig": "正在載入 WireGuard 配置...",
|
"siteLoadWGConfig": "正在載入 WireGuard 配置...",
|
||||||
"siteDocker": "擴展 Docker 部署詳細資訊",
|
"siteDocker": "擴展 Docker 部署詳細資訊",
|
||||||
"toggle": "切換",
|
"toggle": "切換",
|
||||||
"dockerCompose": "Docker 配置",
|
"dockerCompose": "Docker Compose",
|
||||||
"dockerRun": "停靠欄",
|
"dockerRun": "Docker Run",
|
||||||
"siteLearnLocal": "本地站點不需要隧道連接,點擊了解更多",
|
"siteLearnLocal": "本地站點不需要隧道連接,點擊了解更多",
|
||||||
"siteConfirmCopy": "我已經複製了配置資訊",
|
"siteConfirmCopy": "我已經複製了配置資訊",
|
||||||
"searchSitesProgress": "搜索站點...",
|
"searchSitesProgress": "搜索站點...",
|
||||||
@@ -98,9 +103,10 @@
|
|||||||
"siteLocalDescriptionSaas": "僅本地資源。沒有隧道。僅在遠程節點上可用。",
|
"siteLocalDescriptionSaas": "僅本地資源。沒有隧道。僅在遠程節點上可用。",
|
||||||
"siteSeeAll": "查看所有站點",
|
"siteSeeAll": "查看所有站點",
|
||||||
"siteTunnelDescription": "確定如何連接到您的網站",
|
"siteTunnelDescription": "確定如何連接到您的網站",
|
||||||
"siteNewtCredentials": "Newt 憑據",
|
"siteNewtCredentials": "Newt 憑證",
|
||||||
"siteNewtCredentialsDescription": "這是 Newt 伺服器的身份驗證憑據",
|
"siteNewtCredentialsDescription": "這是 Newt 伺服器的身份驗證憑證",
|
||||||
"siteCredentialsSave": "保存您的憑據",
|
"remoteNodeCredentialsDescription": "這是遠端節點與伺服器進行驗證的方式",
|
||||||
|
"siteCredentialsSave": "保存您的憑證",
|
||||||
"siteCredentialsSaveDescription": "您只能看到一次。請確保將其複製並保存到一個安全的地方。",
|
"siteCredentialsSaveDescription": "您只能看到一次。請確保將其複製並保存到一個安全的地方。",
|
||||||
"siteInfo": "站點資訊",
|
"siteInfo": "站點資訊",
|
||||||
"status": "狀態",
|
"status": "狀態",
|
||||||
@@ -144,8 +150,14 @@
|
|||||||
"expires": "過期時間",
|
"expires": "過期時間",
|
||||||
"never": "永不過期",
|
"never": "永不過期",
|
||||||
"shareErrorSelectResource": "請選擇一個資源",
|
"shareErrorSelectResource": "請選擇一個資源",
|
||||||
"resourceTitle": "管理資源",
|
"proxyResourceTitle": "管理公開資源",
|
||||||
"resourceDescription": "為您的私人應用程式創建安全代理",
|
"proxyResourceDescription": "建立和管理可透過網頁瀏覽器公開存取的資源",
|
||||||
|
"proxyResourcesBannerTitle": "基於網頁的公開存取",
|
||||||
|
"proxyResourcesBannerDescription": "公開資源是任何人都可以透過網頁瀏覽器存取的 HTTPS 或 TCP/UDP 代理。與私有資源不同,它們不需要客戶端軟體,並且可以包含基於身份和情境感知的存取策略。",
|
||||||
|
"clientResourceTitle": "管理私有資源",
|
||||||
|
"clientResourceDescription": "建立和管理只能透過已連接的客戶端存取的資源",
|
||||||
|
"privateResourcesBannerTitle": "零信任私有存取",
|
||||||
|
"privateResourcesBannerDescription": "私有資源使用零信任安全性,確保使用者和機器只能存取您明確授權的資源。連接使用者裝置或機器客戶端以透過安全的虛擬私人網路存取這些資源。",
|
||||||
"resourcesSearch": "搜索資源...",
|
"resourcesSearch": "搜索資源...",
|
||||||
"resourceAdd": "添加資源",
|
"resourceAdd": "添加資源",
|
||||||
"resourceErrorDelte": "刪除資源時出錯",
|
"resourceErrorDelte": "刪除資源時出錯",
|
||||||
@@ -179,7 +191,7 @@
|
|||||||
"baseDomain": "根域名",
|
"baseDomain": "根域名",
|
||||||
"subdomnainDescription": "您的資源可以訪問的子域名。",
|
"subdomnainDescription": "您的資源可以訪問的子域名。",
|
||||||
"resourceRawSettings": "TCP/UDP 設置",
|
"resourceRawSettings": "TCP/UDP 設置",
|
||||||
"resourceRawSettingsDescription": "配置如何通過 TCP/UDP 訪問您的資源。 您映射資源到主機Pangolin伺服器上的埠,這樣您就可以訪問伺服器-公共-ip:mapped埠的資源。",
|
"resourceRawSettingsDescription": "設定如何透過 TCP/UDP 存取資源",
|
||||||
"protocol": "協議",
|
"protocol": "協議",
|
||||||
"protocolSelect": "選擇協議",
|
"protocolSelect": "選擇協議",
|
||||||
"resourcePortNumber": "埠號",
|
"resourcePortNumber": "埠號",
|
||||||
@@ -436,6 +448,16 @@
|
|||||||
"inviteEmailSent": "發送邀請郵件給用戶",
|
"inviteEmailSent": "發送邀請郵件給用戶",
|
||||||
"inviteValid": "有效",
|
"inviteValid": "有效",
|
||||||
"selectDuration": "選擇持續時間",
|
"selectDuration": "選擇持續時間",
|
||||||
|
"selectResource": "選擇資源",
|
||||||
|
"filterByResource": "依資源篩選",
|
||||||
|
"resetFilters": "重設篩選條件",
|
||||||
|
"totalBlocked": "被 Pangolin 阻擋的請求",
|
||||||
|
"totalRequests": "總請求數",
|
||||||
|
"requestsByCountry": "依國家/地區的請求",
|
||||||
|
"requestsByDay": "依日期的請求",
|
||||||
|
"blocked": "已阻擋",
|
||||||
|
"allowed": "已允許",
|
||||||
|
"topCountries": "熱門國家/地區",
|
||||||
"accessRoleSelect": "選擇角色",
|
"accessRoleSelect": "選擇角色",
|
||||||
"inviteEmailSentDescription": "一封電子郵件已經發送給用戶,帶有下面的訪問連結。他們必須訪問該連結才能接受邀請。",
|
"inviteEmailSentDescription": "一封電子郵件已經發送給用戶,帶有下面的訪問連結。他們必須訪問該連結才能接受邀請。",
|
||||||
"inviteSentDescription": "用戶已被邀請。他們必須訪問下面的連結才能接受邀請。",
|
"inviteSentDescription": "用戶已被邀請。他們必須訪問下面的連結才能接受邀請。",
|
||||||
@@ -465,7 +487,7 @@
|
|||||||
"proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。",
|
"proxyErrorTls": "無效的 TLS 伺服器名稱。使用域名格式,或保存空以刪除 TLS 伺服器名稱。",
|
||||||
"proxyEnableSSL": "啟用 SSL",
|
"proxyEnableSSL": "啟用 SSL",
|
||||||
"proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。",
|
"proxyEnableSSLDescription": "啟用 SSL/TLS 加密以確保您目標的 HTTPS 連接。",
|
||||||
"target": "Target",
|
"target": "目標",
|
||||||
"configureTarget": "配置目標",
|
"configureTarget": "配置目標",
|
||||||
"targetErrorFetch": "獲取目標失敗",
|
"targetErrorFetch": "獲取目標失敗",
|
||||||
"targetErrorFetchDescription": "獲取目標時出錯",
|
"targetErrorFetchDescription": "獲取目標時出錯",
|
||||||
@@ -516,6 +538,8 @@
|
|||||||
"targetCreatedDescription": "目標已成功創建",
|
"targetCreatedDescription": "目標已成功創建",
|
||||||
"targetErrorCreate": "創建目標失敗",
|
"targetErrorCreate": "創建目標失敗",
|
||||||
"targetErrorCreateDescription": "創建目標時出錯",
|
"targetErrorCreateDescription": "創建目標時出錯",
|
||||||
|
"tlsServerName": "TLS 伺服器名稱",
|
||||||
|
"tlsServerNameDescription": "用於 SNI 的 TLS 伺服器名稱",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"proxyAdditional": "附加代理設置",
|
"proxyAdditional": "附加代理設置",
|
||||||
"proxyAdditionalDescription": "配置你的資源如何處理代理設置",
|
"proxyAdditionalDescription": "配置你的資源如何處理代理設置",
|
||||||
@@ -702,6 +726,7 @@
|
|||||||
"resourceTransferSubmit": "轉移資源",
|
"resourceTransferSubmit": "轉移資源",
|
||||||
"siteDestination": "目標站點",
|
"siteDestination": "目標站點",
|
||||||
"searchSites": "搜索站點",
|
"searchSites": "搜索站點",
|
||||||
|
"countries": "國家/地區",
|
||||||
"accessRoleCreate": "創建角色",
|
"accessRoleCreate": "創建角色",
|
||||||
"accessRoleCreateDescription": "創建一個新角色來分組用戶並管理他們的權限。",
|
"accessRoleCreateDescription": "創建一個新角色來分組用戶並管理他們的權限。",
|
||||||
"accessRoleCreateSubmit": "創建角色",
|
"accessRoleCreateSubmit": "創建角色",
|
||||||
@@ -825,6 +850,7 @@
|
|||||||
"orgPolicyConfig": "配置組織訪問權限",
|
"orgPolicyConfig": "配置組織訪問權限",
|
||||||
"idpUpdatedDescription": "身份提供商更新成功",
|
"idpUpdatedDescription": "身份提供商更新成功",
|
||||||
"redirectUrl": "重定向網址",
|
"redirectUrl": "重定向網址",
|
||||||
|
"orgIdpRedirectUrls": "重新導向網址",
|
||||||
"redirectUrlAbout": "關於重定向網址",
|
"redirectUrlAbout": "關於重定向網址",
|
||||||
"redirectUrlAboutDescription": "這是用戶在驗證後將被重定向到的URL。您需要在身份提供商設置中配置此URL。",
|
"redirectUrlAboutDescription": "這是用戶在驗證後將被重定向到的URL。您需要在身份提供商設置中配置此URL。",
|
||||||
"pangolinAuth": "認證 - Pangolin",
|
"pangolinAuth": "認證 - Pangolin",
|
||||||
@@ -909,6 +935,10 @@
|
|||||||
"passwordResetSent": "我們將發送一個驗證碼到這個電子郵件地址。",
|
"passwordResetSent": "我們將發送一個驗證碼到這個電子郵件地址。",
|
||||||
"passwordResetCode": "驗證碼",
|
"passwordResetCode": "驗證碼",
|
||||||
"passwordResetCodeDescription": "請檢查您的電子郵件以獲取驗證碼。",
|
"passwordResetCodeDescription": "請檢查您的電子郵件以獲取驗證碼。",
|
||||||
|
"generatePasswordResetCode": "產生密碼重設代碼",
|
||||||
|
"passwordResetCodeGenerated": "密碼重設代碼已產生",
|
||||||
|
"passwordResetCodeGeneratedDescription": "請將此代碼分享給使用者。他們可以用它來重設密碼。",
|
||||||
|
"passwordResetUrl": "重設網址",
|
||||||
"passwordNew": "新密碼",
|
"passwordNew": "新密碼",
|
||||||
"passwordNewConfirm": "確認新密碼",
|
"passwordNewConfirm": "確認新密碼",
|
||||||
"changePassword": "更改密碼",
|
"changePassword": "更改密碼",
|
||||||
@@ -926,6 +956,9 @@
|
|||||||
"pincodeAuth": "驗證器代碼",
|
"pincodeAuth": "驗證器代碼",
|
||||||
"pincodeSubmit2": "提交代碼",
|
"pincodeSubmit2": "提交代碼",
|
||||||
"passwordResetSubmit": "請求重設",
|
"passwordResetSubmit": "請求重設",
|
||||||
|
"passwordResetAlreadyHaveCode": "輸入代碼",
|
||||||
|
"passwordResetSmtpRequired": "請聯絡您的管理員",
|
||||||
|
"passwordResetSmtpRequiredDescription": "需要密碼重設代碼才能重設您的密碼。請聯絡您的管理員尋求協助。",
|
||||||
"passwordBack": "回到密碼",
|
"passwordBack": "回到密碼",
|
||||||
"loginBack": "返回登錄",
|
"loginBack": "返回登錄",
|
||||||
"signup": "註冊",
|
"signup": "註冊",
|
||||||
@@ -1013,6 +1046,7 @@
|
|||||||
"updateOrgUser": "更新組織用戶",
|
"updateOrgUser": "更新組織用戶",
|
||||||
"createOrgUser": "創建組織用戶",
|
"createOrgUser": "創建組織用戶",
|
||||||
"actionUpdateOrg": "更新組織",
|
"actionUpdateOrg": "更新組織",
|
||||||
|
"actionRemoveInvitation": "移除邀請",
|
||||||
"actionUpdateUser": "更新用戶",
|
"actionUpdateUser": "更新用戶",
|
||||||
"actionGetUser": "獲取用戶",
|
"actionGetUser": "獲取用戶",
|
||||||
"actionGetOrgUser": "獲取組織用戶",
|
"actionGetOrgUser": "獲取組織用戶",
|
||||||
@@ -1022,6 +1056,8 @@
|
|||||||
"actionGetSite": "獲取站點",
|
"actionGetSite": "獲取站點",
|
||||||
"actionListSites": "站點列表",
|
"actionListSites": "站點列表",
|
||||||
"actionApplyBlueprint": "應用藍圖",
|
"actionApplyBlueprint": "應用藍圖",
|
||||||
|
"actionListBlueprints": "藍圖列表",
|
||||||
|
"actionGetBlueprint": "獲取藍圖",
|
||||||
"setupToken": "設置令牌",
|
"setupToken": "設置令牌",
|
||||||
"setupTokenDescription": "從伺服器控制台輸入設定令牌。",
|
"setupTokenDescription": "從伺服器控制台輸入設定令牌。",
|
||||||
"setupTokenRequired": "需要設置令牌",
|
"setupTokenRequired": "需要設置令牌",
|
||||||
@@ -1091,12 +1127,15 @@
|
|||||||
"actionListSiteResources": "列出站點資源",
|
"actionListSiteResources": "列出站點資源",
|
||||||
"actionUpdateSiteResource": "更新站點資源",
|
"actionUpdateSiteResource": "更新站點資源",
|
||||||
"actionListInvitations": "邀請列表",
|
"actionListInvitations": "邀請列表",
|
||||||
|
"actionExportLogs": "匯出日誌",
|
||||||
|
"actionViewLogs": "查看日誌",
|
||||||
"noneSelected": "未選擇",
|
"noneSelected": "未選擇",
|
||||||
"orgNotFound2": "未找到組織。",
|
"orgNotFound2": "未找到組織。",
|
||||||
"searchProgress": "搜索中...",
|
"searchProgress": "搜索中...",
|
||||||
"create": "創建",
|
"create": "創建",
|
||||||
"orgs": "組織",
|
"orgs": "組織",
|
||||||
"loginError": "登錄時出錯",
|
"loginError": "登錄時出錯",
|
||||||
|
"loginRequiredForDevice": "需要登入以驗證您的裝置。",
|
||||||
"passwordForgot": "忘記密碼?",
|
"passwordForgot": "忘記密碼?",
|
||||||
"otpAuth": "兩步驗證",
|
"otpAuth": "兩步驗證",
|
||||||
"otpAuthDescription": "從您的身份驗證程序中輸入代碼或您的單次備份代碼。",
|
"otpAuthDescription": "從您的身份驗證程序中輸入代碼或您的單次備份代碼。",
|
||||||
@@ -1151,8 +1190,12 @@
|
|||||||
"sidebarHome": "首頁",
|
"sidebarHome": "首頁",
|
||||||
"sidebarSites": "站點",
|
"sidebarSites": "站點",
|
||||||
"sidebarResources": "資源",
|
"sidebarResources": "資源",
|
||||||
|
"sidebarProxyResources": "公開",
|
||||||
|
"sidebarClientResources": "私有",
|
||||||
"sidebarAccessControl": "訪問控制",
|
"sidebarAccessControl": "訪問控制",
|
||||||
|
"sidebarLogsAndAnalytics": "日誌與分析",
|
||||||
"sidebarUsers": "用戶",
|
"sidebarUsers": "用戶",
|
||||||
|
"sidebarAdmin": "管理員",
|
||||||
"sidebarInvitations": "邀請",
|
"sidebarInvitations": "邀請",
|
||||||
"sidebarRoles": "角色",
|
"sidebarRoles": "角色",
|
||||||
"sidebarShareableLinks": "分享連結",
|
"sidebarShareableLinks": "分享連結",
|
||||||
@@ -1162,8 +1205,14 @@
|
|||||||
"sidebarIdentityProviders": "身份提供商",
|
"sidebarIdentityProviders": "身份提供商",
|
||||||
"sidebarLicense": "證書",
|
"sidebarLicense": "證書",
|
||||||
"sidebarClients": "用戶端",
|
"sidebarClients": "用戶端",
|
||||||
|
"sidebarUserDevices": "使用者",
|
||||||
|
"sidebarMachineClients": "機器",
|
||||||
"sidebarDomains": "域",
|
"sidebarDomains": "域",
|
||||||
|
"sidebarGeneral": "管理",
|
||||||
|
"sidebarLogAndAnalytics": "日誌與分析",
|
||||||
"sidebarBluePrints": "藍圖",
|
"sidebarBluePrints": "藍圖",
|
||||||
|
"sidebarOrganization": "組織",
|
||||||
|
"sidebarLogsAnalytics": "分析",
|
||||||
"blueprints": "藍圖",
|
"blueprints": "藍圖",
|
||||||
"blueprintsDescription": "應用聲明配置並查看先前運行的",
|
"blueprintsDescription": "應用聲明配置並查看先前運行的",
|
||||||
"blueprintAdd": "添加藍圖",
|
"blueprintAdd": "添加藍圖",
|
||||||
@@ -1273,12 +1322,24 @@
|
|||||||
"accountSetupSuccess": "帳號設定完成!歡迎來到 Pangolin!",
|
"accountSetupSuccess": "帳號設定完成!歡迎來到 Pangolin!",
|
||||||
"documentation": "文件",
|
"documentation": "文件",
|
||||||
"saveAllSettings": "保存所有設置",
|
"saveAllSettings": "保存所有設置",
|
||||||
|
"saveResourceTargets": "儲存目標",
|
||||||
|
"saveResourceHttp": "儲存代理設定",
|
||||||
|
"saveProxyProtocol": "儲存代理協定設定",
|
||||||
"settingsUpdated": "設置已更新",
|
"settingsUpdated": "設置已更新",
|
||||||
"settingsUpdatedDescription": "所有設置已成功更新",
|
"settingsUpdatedDescription": "所有設置已成功更新",
|
||||||
"settingsErrorUpdate": "設置更新失敗",
|
"settingsErrorUpdate": "設置更新失敗",
|
||||||
"settingsErrorUpdateDescription": "更新設置時發生錯誤",
|
"settingsErrorUpdateDescription": "更新設置時發生錯誤",
|
||||||
"sidebarCollapse": "摺疊",
|
"sidebarCollapse": "摺疊",
|
||||||
"sidebarExpand": "展開",
|
"sidebarExpand": "展開",
|
||||||
|
"productUpdateMoreInfo": "還有 {noOfUpdates} 項更新",
|
||||||
|
"productUpdateInfo": "{noOfUpdates} 項更新",
|
||||||
|
"productUpdateWhatsNew": "新功能",
|
||||||
|
"productUpdateTitle": "產品更新",
|
||||||
|
"productUpdateEmpty": "沒有更新",
|
||||||
|
"dismissAll": "全部關閉",
|
||||||
|
"pangolinUpdateAvailable": "有可用更新",
|
||||||
|
"pangolinUpdateAvailableInfo": "版本 {version} 已準備好安裝",
|
||||||
|
"pangolinUpdateAvailableReleaseNotes": "查看發行說明",
|
||||||
"newtUpdateAvailable": "更新可用",
|
"newtUpdateAvailable": "更新可用",
|
||||||
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。請更新到最新版本以獲得最佳體驗。",
|
"newtUpdateAvailableInfo": "新版本的 Newt 已可用。請更新到最新版本以獲得最佳體驗。",
|
||||||
"domainPickerEnterDomain": "域名",
|
"domainPickerEnterDomain": "域名",
|
||||||
@@ -1421,6 +1482,9 @@
|
|||||||
"and": "和",
|
"and": "和",
|
||||||
"privacyPolicy": "隱私政策"
|
"privacyPolicy": "隱私政策"
|
||||||
},
|
},
|
||||||
|
"signUpMarketing": {
|
||||||
|
"keepMeInTheLoop": "透過電子郵件接收新聞、更新和新功能通知。"
|
||||||
|
},
|
||||||
"siteRequired": "需要站點。",
|
"siteRequired": "需要站點。",
|
||||||
"olmTunnel": "Olm 隧道",
|
"olmTunnel": "Olm 隧道",
|
||||||
"olmTunnelDescription": "使用 Olm 進行用戶端連接",
|
"olmTunnelDescription": "使用 Olm 進行用戶端連接",
|
||||||
@@ -1454,15 +1518,14 @@
|
|||||||
"sitesFetchError": "獲取站點時出錯。",
|
"sitesFetchError": "獲取站點時出錯。",
|
||||||
"olmErrorFetchReleases": "獲取 Olm 發布版本時出錯。",
|
"olmErrorFetchReleases": "獲取 Olm 發布版本時出錯。",
|
||||||
"olmErrorFetchLatest": "獲取最新 Olm 發布版本時出錯。",
|
"olmErrorFetchLatest": "獲取最新 Olm 發布版本時出錯。",
|
||||||
"remoteSubnets": "遠程子網",
|
|
||||||
"enterCidrRange": "輸入 CIDR 範圍",
|
"enterCidrRange": "輸入 CIDR 範圍",
|
||||||
"remoteSubnetsDescription": "添加可以通過用戶端遠端存取該站點的 CIDR 範圍。使用類似 10.0.0.0/24 的格式。這僅適用於 VPN 用戶端連接。",
|
|
||||||
"resourceEnableProxy": "啟用公共代理",
|
"resourceEnableProxy": "啟用公共代理",
|
||||||
"resourceEnableProxyDescription": "啟用到此資源的公共代理。這允許外部網路通過開放埠訪問資源。需要 Traefik 配置。",
|
"resourceEnableProxyDescription": "啟用到此資源的公共代理。這允許外部網路通過開放埠訪問資源。需要 Traefik 配置。",
|
||||||
"externalProxyEnabled": "外部代理已啟用",
|
"externalProxyEnabled": "外部代理已啟用",
|
||||||
"addNewTarget": "添加新目標",
|
"addNewTarget": "添加新目標",
|
||||||
"targetsList": "目標列表",
|
"targetsList": "目標列表",
|
||||||
"advancedMode": "高級模式",
|
"advancedMode": "高級模式",
|
||||||
|
"advancedSettings": "進階設定",
|
||||||
"targetErrorDuplicateTargetFound": "找到重複的目標",
|
"targetErrorDuplicateTargetFound": "找到重複的目標",
|
||||||
"healthCheckHealthy": "正常",
|
"healthCheckHealthy": "正常",
|
||||||
"healthCheckUnhealthy": "不正常",
|
"healthCheckUnhealthy": "不正常",
|
||||||
@@ -1474,6 +1537,7 @@
|
|||||||
"enableHealthChecksDescription": "監視此目標的健康狀況。如果需要,您可以監視一個不同的終點。",
|
"enableHealthChecksDescription": "監視此目標的健康狀況。如果需要,您可以監視一個不同的終點。",
|
||||||
"healthScheme": "方法",
|
"healthScheme": "方法",
|
||||||
"healthSelectScheme": "選擇方法",
|
"healthSelectScheme": "選擇方法",
|
||||||
|
"healthCheckPortInvalid": "健康檢查連接埠必須介於 1 到 65535 之間",
|
||||||
"healthCheckPath": "路徑",
|
"healthCheckPath": "路徑",
|
||||||
"healthHostname": "IP / 主機",
|
"healthHostname": "IP / 主機",
|
||||||
"healthPort": "埠",
|
"healthPort": "埠",
|
||||||
@@ -1522,9 +1586,15 @@
|
|||||||
"resourcesTableNoProxyResourcesFound": "未找到代理資源。",
|
"resourcesTableNoProxyResourcesFound": "未找到代理資源。",
|
||||||
"resourcesTableNoInternalResourcesFound": "未找到內部資源。",
|
"resourcesTableNoInternalResourcesFound": "未找到內部資源。",
|
||||||
"resourcesTableDestination": "目標",
|
"resourcesTableDestination": "目標",
|
||||||
"resourcesTableTheseResourcesForUseWith": "這些資源供...使用",
|
"resourcesTableAlias": "別名",
|
||||||
"resourcesTableClients": "用戶端",
|
"resourcesTableClients": "用戶端",
|
||||||
"resourcesTableAndOnlyAccessibleInternally": "且僅在與用戶端連接時可內部訪問。",
|
"resourcesTableAndOnlyAccessibleInternally": "且僅在與用戶端連接時可內部訪問。",
|
||||||
|
"resourcesTableNoTargets": "無目標",
|
||||||
|
"resourcesTableHealthy": "健康",
|
||||||
|
"resourcesTableDegraded": "降級",
|
||||||
|
"resourcesTableOffline": "離線",
|
||||||
|
"resourcesTableUnknown": "未知",
|
||||||
|
"resourcesTableNotMonitored": "未監控",
|
||||||
"editInternalResourceDialogEditClientResource": "編輯用戶端資源",
|
"editInternalResourceDialogEditClientResource": "編輯用戶端資源",
|
||||||
"editInternalResourceDialogUpdateResourceProperties": "更新 {resourceName} 的資源屬性和目標配置。",
|
"editInternalResourceDialogUpdateResourceProperties": "更新 {resourceName} 的資源屬性和目標配置。",
|
||||||
"editInternalResourceDialogResourceProperties": "資源屬性",
|
"editInternalResourceDialogResourceProperties": "資源屬性",
|
||||||
@@ -1545,6 +1615,17 @@
|
|||||||
"editInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式",
|
"editInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式",
|
||||||
"editInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1",
|
"editInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1",
|
||||||
"editInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536",
|
"editInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536",
|
||||||
|
"editInternalResourceDialogPortModeRequired": "連接埠模式需要協定、代理連接埠和目標連接埠",
|
||||||
|
"editInternalResourceDialogMode": "模式",
|
||||||
|
"editInternalResourceDialogModePort": "連接埠",
|
||||||
|
"editInternalResourceDialogModeHost": "主機",
|
||||||
|
"editInternalResourceDialogModeCidr": "CIDR",
|
||||||
|
"editInternalResourceDialogDestination": "目的地",
|
||||||
|
"editInternalResourceDialogDestinationHostDescription": "站點網路上資源的 IP 位址或主機名稱。",
|
||||||
|
"editInternalResourceDialogDestinationIPDescription": "站點網路上資源的 IP 或主機名稱位址。",
|
||||||
|
"editInternalResourceDialogDestinationCidrDescription": "站點網路上資源的 CIDR 範圍。",
|
||||||
|
"editInternalResourceDialogAlias": "別名",
|
||||||
|
"editInternalResourceDialogAliasDescription": "此資源的可選內部 DNS 別名。",
|
||||||
"createInternalResourceDialogNoSitesAvailable": "暫無可用站點",
|
"createInternalResourceDialogNoSitesAvailable": "暫無可用站點",
|
||||||
"createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一個子網的 Newt 站點來創建內部資源。",
|
"createInternalResourceDialogNoSitesAvailableDescription": "您需要至少配置一個子網的 Newt 站點來創建內部資源。",
|
||||||
"createInternalResourceDialogClose": "關閉",
|
"createInternalResourceDialogClose": "關閉",
|
||||||
@@ -1553,9 +1634,8 @@
|
|||||||
"createInternalResourceDialogResourceProperties": "資源屬性",
|
"createInternalResourceDialogResourceProperties": "資源屬性",
|
||||||
"createInternalResourceDialogName": "名稱",
|
"createInternalResourceDialogName": "名稱",
|
||||||
"createInternalResourceDialogSite": "站點",
|
"createInternalResourceDialogSite": "站點",
|
||||||
"createInternalResourceDialogSelectSite": "選擇站點...",
|
"selectSite": "選擇站點...",
|
||||||
"createInternalResourceDialogSearchSites": "搜索站點...",
|
"noSitesFound": "找不到站點。",
|
||||||
"createInternalResourceDialogNoSitesFound": "未找到站點。",
|
|
||||||
"createInternalResourceDialogProtocol": "協議",
|
"createInternalResourceDialogProtocol": "協議",
|
||||||
"createInternalResourceDialogTcp": "TCP",
|
"createInternalResourceDialogTcp": "TCP",
|
||||||
"createInternalResourceDialogUdp": "UDP",
|
"createInternalResourceDialogUdp": "UDP",
|
||||||
@@ -1578,11 +1658,22 @@
|
|||||||
"createInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式",
|
"createInternalResourceDialogInvalidIPAddressFormat": "無效的 IP 位址格式",
|
||||||
"createInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1",
|
"createInternalResourceDialogDestinationPortMin": "目標埠必須至少為 1",
|
||||||
"createInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536",
|
"createInternalResourceDialogDestinationPortMax": "目標埠必須小於 65536",
|
||||||
|
"createInternalResourceDialogPortModeRequired": "連接埠模式需要協定、代理連接埠和目標連接埠",
|
||||||
|
"createInternalResourceDialogMode": "模式",
|
||||||
|
"createInternalResourceDialogModePort": "連接埠",
|
||||||
|
"createInternalResourceDialogModeHost": "主機",
|
||||||
|
"createInternalResourceDialogModeCidr": "CIDR",
|
||||||
|
"createInternalResourceDialogDestination": "目的地",
|
||||||
|
"createInternalResourceDialogDestinationHostDescription": "站點網路上資源的 IP 位址或主機名稱。",
|
||||||
|
"createInternalResourceDialogDestinationCidrDescription": "站點網路上資源的 CIDR 範圍。",
|
||||||
|
"createInternalResourceDialogAlias": "別名",
|
||||||
|
"createInternalResourceDialogAliasDescription": "此資源的可選內部 DNS 別名。",
|
||||||
"siteConfiguration": "配置",
|
"siteConfiguration": "配置",
|
||||||
"siteAcceptClientConnections": "接受用戶端連接",
|
"siteAcceptClientConnections": "接受用戶端連接",
|
||||||
"siteAcceptClientConnectionsDescription": "允許其他設備透過此 Newt 實例使用用戶端作為閘道器連接。",
|
"siteAcceptClientConnectionsDescription": "允許其他設備透過此 Newt 實例使用用戶端作為閘道器連接。",
|
||||||
"siteAddress": "站點地址",
|
"siteAddress": "站點地址",
|
||||||
"siteAddressDescription": "指定主機的 IP 位址以供用戶端連接。這是 Pangolin 網路中站點的內部地址,供用戶端訪問。必須在 Org 子網內。",
|
"siteAddressDescription": "指定主機的 IP 位址以供用戶端連接。這是 Pangolin 網路中站點的內部地址,供用戶端訪問。必須在 Org 子網內。",
|
||||||
|
"siteNameDescription": "站點的顯示名稱,可以稍後更改。",
|
||||||
"autoLoginExternalIdp": "自動使用外部 IDP 登錄",
|
"autoLoginExternalIdp": "自動使用外部 IDP 登錄",
|
||||||
"autoLoginExternalIdpDescription": "立即將用戶重定向到外部 IDP 進行身份驗證。",
|
"autoLoginExternalIdpDescription": "立即將用戶重定向到外部 IDP 進行身份驗證。",
|
||||||
"selectIdp": "選擇 IDP",
|
"selectIdp": "選擇 IDP",
|
||||||
@@ -1606,6 +1697,8 @@
|
|||||||
"remoteExitNodeConfirmDelete": "確認刪除節點",
|
"remoteExitNodeConfirmDelete": "確認刪除節點",
|
||||||
"remoteExitNodeDelete": "刪除節點",
|
"remoteExitNodeDelete": "刪除節點",
|
||||||
"sidebarRemoteExitNodes": "遠程節點",
|
"sidebarRemoteExitNodes": "遠程節點",
|
||||||
|
"remoteExitNodeId": "ID",
|
||||||
|
"remoteExitNodeSecretKey": "密鑰",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "創建節點",
|
"title": "創建節點",
|
||||||
"description": "創建一個新節點來擴展您的網路連接",
|
"description": "創建一個新節點來擴展您的網路連接",
|
||||||
@@ -1729,12 +1822,33 @@
|
|||||||
"idpAzureClientIdDescription2": "您的 Azure 應用程式註冊用戶端 ID",
|
"idpAzureClientIdDescription2": "您的 Azure 應用程式註冊用戶端 ID",
|
||||||
"idpAzureClientSecretDescription2": "您的 Azure 應用程式註冊用戶端金鑰",
|
"idpAzureClientSecretDescription2": "您的 Azure 應用程式註冊用戶端金鑰",
|
||||||
"idpGoogleDescription": "Google OAuth2/OIDC 提供商",
|
"idpGoogleDescription": "Google OAuth2/OIDC 提供商",
|
||||||
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC provider",
|
"idpAzureDescription": "Microsoft Azure OAuth2/OIDC 提供者",
|
||||||
"subnet": "子網",
|
"subnet": "子網",
|
||||||
"subnetDescription": "此組織網路配置的子網。",
|
"subnetDescription": "此組織網路配置的子網。",
|
||||||
|
"customDomain": "自訂網域",
|
||||||
"authPage": "認證頁面",
|
"authPage": "認證頁面",
|
||||||
"authPageDescription": "配置您的組織認證頁面",
|
"authPageDescription": "配置您的組織認證頁面",
|
||||||
"authPageDomain": "認證頁面域",
|
"authPageDomain": "認證頁面域",
|
||||||
|
"authPageBranding": "自訂品牌",
|
||||||
|
"authPageBrandingDescription": "設定此組織驗證頁面上顯示的品牌",
|
||||||
|
"authPageBrandingUpdated": "驗證頁面品牌更新成功",
|
||||||
|
"authPageBrandingRemoved": "驗證頁面品牌移除成功",
|
||||||
|
"authPageBrandingRemoveTitle": "移除驗證頁面品牌",
|
||||||
|
"authPageBrandingQuestionRemove": "您確定要移除驗證頁面的品牌嗎?",
|
||||||
|
"authPageBrandingDeleteConfirm": "確認刪除品牌",
|
||||||
|
"brandingLogoURL": "Logo 網址",
|
||||||
|
"brandingPrimaryColor": "主要顏色",
|
||||||
|
"brandingLogoWidth": "寬度 (px)",
|
||||||
|
"brandingLogoHeight": "高度 (px)",
|
||||||
|
"brandingOrgTitle": "組織驗證頁面標題",
|
||||||
|
"brandingOrgDescription": "{orgName} 將被替換為組織名稱",
|
||||||
|
"brandingOrgSubtitle": "組織驗證頁面副標題",
|
||||||
|
"brandingResourceTitle": "資源驗證頁面標題",
|
||||||
|
"brandingResourceSubtitle": "資源驗證頁面副標題",
|
||||||
|
"brandingResourceDescription": "{resourceName} 將被替換為組織名稱",
|
||||||
|
"saveAuthPageDomain": "儲存網域",
|
||||||
|
"saveAuthPageBranding": "儲存品牌",
|
||||||
|
"removeAuthPageBranding": "移除品牌",
|
||||||
"noDomainSet": "沒有域設置",
|
"noDomainSet": "沒有域設置",
|
||||||
"changeDomain": "更改域",
|
"changeDomain": "更改域",
|
||||||
"selectDomain": "選擇域",
|
"selectDomain": "選擇域",
|
||||||
@@ -1762,6 +1876,15 @@
|
|||||||
"orgAuthChooseIdpDescription": "選擇您的身份提供商以繼續",
|
"orgAuthChooseIdpDescription": "選擇您的身份提供商以繼續",
|
||||||
"orgAuthNoIdpConfigured": "此機構沒有配置任何身份提供者。您可以使用您的 Pangolin 身份登錄。",
|
"orgAuthNoIdpConfigured": "此機構沒有配置任何身份提供者。您可以使用您的 Pangolin 身份登錄。",
|
||||||
"orgAuthSignInWithPangolin": "使用 Pangolin 登錄",
|
"orgAuthSignInWithPangolin": "使用 Pangolin 登錄",
|
||||||
|
"orgAuthSignInToOrg": "登入組織",
|
||||||
|
"orgAuthSelectOrgTitle": "組織登入",
|
||||||
|
"orgAuthSelectOrgDescription": "輸入您的組織 ID 以繼續",
|
||||||
|
"orgAuthOrgIdPlaceholder": "your-organization",
|
||||||
|
"orgAuthOrgIdHelp": "輸入您組織的唯一識別碼",
|
||||||
|
"orgAuthSelectOrgHelp": "輸入組織 ID 後,您將被導向到組織的登入頁面,在那裡您可以使用 SSO 或組織憑證。",
|
||||||
|
"orgAuthRememberOrgId": "記住此組織 ID",
|
||||||
|
"orgAuthBackToSignIn": "返回標準登入",
|
||||||
|
"orgAuthNoAccount": "沒有帳戶?",
|
||||||
"subscriptionRequiredToUse": "需要訂閱才能使用此功能。",
|
"subscriptionRequiredToUse": "需要訂閱才能使用此功能。",
|
||||||
"idpDisabled": "身份提供者已禁用。",
|
"idpDisabled": "身份提供者已禁用。",
|
||||||
"orgAuthPageDisabled": "組織認證頁面已禁用。",
|
"orgAuthPageDisabled": "組織認證頁面已禁用。",
|
||||||
@@ -1776,6 +1899,8 @@
|
|||||||
"enableTwoFactorAuthentication": "啟用兩步驗證",
|
"enableTwoFactorAuthentication": "啟用兩步驗證",
|
||||||
"completeSecuritySteps": "完成安全步驟",
|
"completeSecuritySteps": "完成安全步驟",
|
||||||
"securitySettings": "安全設定",
|
"securitySettings": "安全設定",
|
||||||
|
"dangerSection": "危險區域",
|
||||||
|
"dangerSectionDescription": "永久刪除與此組織相關的所有資料",
|
||||||
"securitySettingsDescription": "配置您組織的安全策略",
|
"securitySettingsDescription": "配置您組織的安全策略",
|
||||||
"requireTwoFactorForAllUsers": "所有用戶需要兩步驗證",
|
"requireTwoFactorForAllUsers": "所有用戶需要兩步驗證",
|
||||||
"requireTwoFactorDescription": "如果啟用,此組織的所有內部用戶必須啟用雙重身份驗證才能訪問組織。",
|
"requireTwoFactorDescription": "如果啟用,此組織的所有內部用戶必須啟用雙重身份驗證才能訪問組織。",
|
||||||
@@ -1813,7 +1938,7 @@
|
|||||||
"securityPolicyChangeWarningText": "這將影響組織中的所有用戶",
|
"securityPolicyChangeWarningText": "這將影響組織中的所有用戶",
|
||||||
"authPageErrorUpdateMessage": "更新身份驗證頁面設置時出錯",
|
"authPageErrorUpdateMessage": "更新身份驗證頁面設置時出錯",
|
||||||
"authPageErrorUpdate": "無法更新認證頁面",
|
"authPageErrorUpdate": "無法更新認證頁面",
|
||||||
"authPageUpdated": "身份驗證頁面更新成功",
|
"authPageDomainUpdated": "驗證頁面網域更新成功",
|
||||||
"healthCheckNotAvailable": "本地的",
|
"healthCheckNotAvailable": "本地的",
|
||||||
"rewritePath": "重寫路徑",
|
"rewritePath": "重寫路徑",
|
||||||
"rewritePathDescription": "在轉發到目標之前,可以選擇重寫路徑。",
|
"rewritePathDescription": "在轉發到目標之前,可以選擇重寫路徑。",
|
||||||
@@ -1839,8 +1964,19 @@
|
|||||||
"enterpriseEdition": "企業版",
|
"enterpriseEdition": "企業版",
|
||||||
"unlicensed": "未授權",
|
"unlicensed": "未授權",
|
||||||
"beta": "測試版",
|
"beta": "測試版",
|
||||||
"manageClients": "管理用戶端",
|
"manageUserDevices": "使用者裝置",
|
||||||
"manageClientsDescription": "用戶端是可以連接到您的站點的設備",
|
"manageUserDevicesDescription": "查看和管理使用者用於私密連接資源的裝置",
|
||||||
|
"downloadClientBannerTitle": "下載 Pangolin 客戶端",
|
||||||
|
"downloadClientBannerDescription": "下載適用於您系統的 Pangolin 客戶端,以連接到 Pangolin 網路並私密存取資源。",
|
||||||
|
"manageMachineClients": "管理機器客戶端",
|
||||||
|
"manageMachineClientsDescription": "建立和管理伺服器和系統用於私密連接資源的客戶端",
|
||||||
|
"machineClientsBannerTitle": "伺服器與自動化系統",
|
||||||
|
"machineClientsBannerDescription": "機器客戶端適用於與特定使用者無關的伺服器和自動化系統。它們使用 ID 和密鑰進行驗證,可以透過 Pangolin CLI、Olm CLI 或 Olm 容器執行。",
|
||||||
|
"machineClientsBannerPangolinCLI": "Pangolin CLI",
|
||||||
|
"machineClientsBannerOlmCLI": "Olm CLI",
|
||||||
|
"machineClientsBannerOlmContainer": "Olm 容器",
|
||||||
|
"clientsTableUserClients": "使用者",
|
||||||
|
"clientsTableMachineClients": "機器",
|
||||||
"licenseTableValidUntil": "有效期至",
|
"licenseTableValidUntil": "有效期至",
|
||||||
"saasLicenseKeysSettingsTitle": "企業許可證",
|
"saasLicenseKeysSettingsTitle": "企業許可證",
|
||||||
"saasLicenseKeysSettingsDescription": "為自我託管的 Pangolin 實例生成和管理企業許可證金鑰",
|
"saasLicenseKeysSettingsDescription": "為自我託管的 Pangolin 實例生成和管理企業許可證金鑰",
|
||||||
@@ -1980,6 +2116,7 @@
|
|||||||
"clientMessageRemove": "一旦刪除,用戶端將無法連接到站點。",
|
"clientMessageRemove": "一旦刪除,用戶端將無法連接到站點。",
|
||||||
"sidebarLogs": "日誌",
|
"sidebarLogs": "日誌",
|
||||||
"request": "請求",
|
"request": "請求",
|
||||||
|
"requests": "請求",
|
||||||
"logs": "日誌",
|
"logs": "日誌",
|
||||||
"logsSettingsDescription": "監視從此 orginization 中收集的日誌",
|
"logsSettingsDescription": "監視從此 orginization 中收集的日誌",
|
||||||
"searchLogs": "搜索日誌...",
|
"searchLogs": "搜索日誌...",
|
||||||
@@ -1988,6 +2125,8 @@
|
|||||||
"timestamp": "時間戳",
|
"timestamp": "時間戳",
|
||||||
"accessLogs": "訪問日誌",
|
"accessLogs": "訪問日誌",
|
||||||
"exportCsv": "導出 CSV",
|
"exportCsv": "導出 CSV",
|
||||||
|
"exportError": "匯出 CSV 時發生未知錯誤",
|
||||||
|
"exportCsvTooltip": "在時間範圍內",
|
||||||
"actorId": "執行者 ID",
|
"actorId": "執行者 ID",
|
||||||
"allowedByRule": "根據規則允許",
|
"allowedByRule": "根據規則允許",
|
||||||
"allowedNoAuth": "無認證",
|
"allowedNoAuth": "無認證",
|
||||||
@@ -2005,6 +2144,7 @@
|
|||||||
"ip": "IP",
|
"ip": "IP",
|
||||||
"reason": "原因",
|
"reason": "原因",
|
||||||
"requestLogs": "請求日誌",
|
"requestLogs": "請求日誌",
|
||||||
|
"requestAnalytics": "請求分析",
|
||||||
"host": "主機",
|
"host": "主機",
|
||||||
"location": "地點",
|
"location": "地點",
|
||||||
"actionLogs": "操作日誌",
|
"actionLogs": "操作日誌",
|
||||||
@@ -2014,6 +2154,7 @@
|
|||||||
"logRetention": "日誌保留",
|
"logRetention": "日誌保留",
|
||||||
"logRetentionDescription": "管理不同類型的日誌為這個機構保留多長時間或禁用這些日誌",
|
"logRetentionDescription": "管理不同類型的日誌為這個機構保留多長時間或禁用這些日誌",
|
||||||
"requestLogsDescription": "查看此機構資源的詳細請求日誌",
|
"requestLogsDescription": "查看此機構資源的詳細請求日誌",
|
||||||
|
"requestAnalyticsDescription": "查看此組織資源的詳細請求分析",
|
||||||
"logRetentionRequestLabel": "請求日誌保留",
|
"logRetentionRequestLabel": "請求日誌保留",
|
||||||
"logRetentionRequestDescription": "保留請求日誌的時間",
|
"logRetentionRequestDescription": "保留請求日誌的時間",
|
||||||
"logRetentionAccessLabel": "訪問日誌保留",
|
"logRetentionAccessLabel": "訪問日誌保留",
|
||||||
@@ -2027,6 +2168,7 @@
|
|||||||
"logRetention30Days": "30 天",
|
"logRetention30Days": "30 天",
|
||||||
"logRetention90Days": "90 天",
|
"logRetention90Days": "90 天",
|
||||||
"logRetentionForever": "永遠的",
|
"logRetentionForever": "永遠的",
|
||||||
|
"logRetentionEndOfFollowingYear": "次年年底",
|
||||||
"actionLogsDescription": "查看此機構執行的操作歷史",
|
"actionLogsDescription": "查看此機構執行的操作歷史",
|
||||||
"accessLogsDescription": "查看此機構資源的訪問認證請求",
|
"accessLogsDescription": "查看此機構資源的訪問認證請求",
|
||||||
"licenseRequiredToUse": "需要企業許可證才能使用此功能。",
|
"licenseRequiredToUse": "需要企業許可證才能使用此功能。",
|
||||||
@@ -2082,6 +2224,43 @@
|
|||||||
"supportMessageSent": "消息已發送!",
|
"supportMessageSent": "消息已發送!",
|
||||||
"supportWillContact": "我們很快就會聯繫起來!",
|
"supportWillContact": "我們很快就會聯繫起來!",
|
||||||
"selectLogRetention": "選擇保留日誌",
|
"selectLogRetention": "選擇保留日誌",
|
||||||
|
"terms": "條款",
|
||||||
|
"privacy": "隱私權",
|
||||||
|
"security": "安全性",
|
||||||
|
"docs": "文件",
|
||||||
|
"deviceActivation": "裝置啟用",
|
||||||
|
"deviceCodeInvalidFormat": "代碼必須為 9 個字元(例如:A1AJ-N5JD)",
|
||||||
|
"deviceCodeInvalidOrExpired": "代碼無效或已過期",
|
||||||
|
"deviceCodeVerifyFailed": "驗證裝置代碼失敗",
|
||||||
|
"signedInAs": "已登入為",
|
||||||
|
"deviceCodeEnterPrompt": "輸入裝置上顯示的代碼",
|
||||||
|
"continue": "繼續",
|
||||||
|
"deviceUnknownLocation": "未知位置",
|
||||||
|
"deviceAuthorizationRequested": "此授權請求來自 {location},時間為 {date}。請確保您信任此裝置,因為它將獲得帳戶存取權限。",
|
||||||
|
"deviceLabel": "裝置:{deviceName}",
|
||||||
|
"deviceWantsAccess": "想要存取您的帳戶",
|
||||||
|
"deviceExistingAccess": "現有存取權限:",
|
||||||
|
"deviceFullAccess": "完整帳戶存取權限",
|
||||||
|
"deviceOrganizationsAccess": "存取您帳戶有權限的所有組織",
|
||||||
|
"deviceAuthorize": "授權 {applicationName}",
|
||||||
|
"deviceConnected": "裝置已連接!",
|
||||||
|
"deviceAuthorizedMessage": "裝置已獲授權存取您的帳戶。請返回客戶端應用程式。",
|
||||||
|
"pangolinCloud": "Pangolin 雲端",
|
||||||
|
"viewDevices": "查看裝置",
|
||||||
|
"viewDevicesDescription": "管理您已連接的裝置",
|
||||||
|
"noDevices": "找不到裝置",
|
||||||
|
"dateCreated": "建立日期",
|
||||||
|
"unnamedDevice": "未命名裝置",
|
||||||
|
"deviceQuestionRemove": "您確定要刪除此裝置嗎?",
|
||||||
|
"deviceMessageRemove": "此操作無法復原。",
|
||||||
|
"deviceDeleteConfirm": "刪除裝置",
|
||||||
|
"deleteDevice": "刪除裝置",
|
||||||
|
"errorLoadingDevices": "載入裝置時發生錯誤",
|
||||||
|
"failedToLoadDevices": "載入裝置失敗",
|
||||||
|
"deviceDeleted": "裝置已刪除",
|
||||||
|
"deviceDeletedDescription": "裝置已成功刪除。",
|
||||||
|
"errorDeletingDevice": "刪除裝置時發生錯誤",
|
||||||
|
"failedToDeleteDevice": "刪除裝置失敗",
|
||||||
"showColumns": "顯示列",
|
"showColumns": "顯示列",
|
||||||
"hideColumns": "隱藏列",
|
"hideColumns": "隱藏列",
|
||||||
"columnVisibility": "列可見性",
|
"columnVisibility": "列可見性",
|
||||||
@@ -2095,5 +2274,125 @@
|
|||||||
"selectedResources": "選定的資源",
|
"selectedResources": "選定的資源",
|
||||||
"enableSelected": "啟用選中的",
|
"enableSelected": "啟用選中的",
|
||||||
"disableSelected": "禁用選中的",
|
"disableSelected": "禁用選中的",
|
||||||
"checkSelectedStatus": "檢查選中的狀態"
|
"checkSelectedStatus": "檢查選中的狀態",
|
||||||
|
"clients": "客戶端",
|
||||||
|
"accessClientSelect": "選擇機器客戶端",
|
||||||
|
"resourceClientDescription": "可以存取此資源的機器客戶端",
|
||||||
|
"regenerate": "重新產生",
|
||||||
|
"credentials": "憑證",
|
||||||
|
"savecredentials": "儲存憑證",
|
||||||
|
"regenerateCredentialsButton": "重新產生憑證",
|
||||||
|
"regenerateCredentials": "重新產生憑證",
|
||||||
|
"generatedcredentials": "已產生的憑證",
|
||||||
|
"copyandsavethesecredentials": "複製並儲存這些憑證",
|
||||||
|
"copyandsavethesecredentialsdescription": "離開此頁面後將不會再顯示這些憑證。請立即安全儲存。",
|
||||||
|
"credentialsSaved": "憑證已儲存",
|
||||||
|
"credentialsSavedDescription": "憑證已成功重新產生並儲存。",
|
||||||
|
"credentialsSaveError": "憑證儲存錯誤",
|
||||||
|
"credentialsSaveErrorDescription": "重新產生和儲存憑證時發生錯誤。",
|
||||||
|
"regenerateCredentialsWarning": "重新產生憑證將使先前的憑證失效並導致斷線。請確保更新任何使用這些憑證的設定。",
|
||||||
|
"confirm": "確認",
|
||||||
|
"regenerateCredentialsConfirmation": "您確定要重新產生憑證嗎?",
|
||||||
|
"endpoint": "端點",
|
||||||
|
"Id": "ID",
|
||||||
|
"SecretKey": "密鑰",
|
||||||
|
"niceId": "友善 ID",
|
||||||
|
"niceIdUpdated": "友善 ID 已更新",
|
||||||
|
"niceIdUpdatedSuccessfully": "友善 ID 更新成功",
|
||||||
|
"niceIdUpdateError": "更新友善 ID 時發生錯誤",
|
||||||
|
"niceIdUpdateErrorDescription": "更新友善 ID 時發生錯誤。",
|
||||||
|
"niceIdCannotBeEmpty": "友善 ID 不能為空",
|
||||||
|
"enterIdentifier": "輸入識別碼",
|
||||||
|
"identifier": "識別碼",
|
||||||
|
"deviceLoginUseDifferentAccount": "不是您嗎?使用其他帳戶。",
|
||||||
|
"deviceLoginDeviceRequestingAccessToAccount": "有裝置正在請求存取此帳戶。",
|
||||||
|
"noData": "無資料",
|
||||||
|
"machineClients": "機器客戶端",
|
||||||
|
"install": "安裝",
|
||||||
|
"run": "執行",
|
||||||
|
"clientNameDescription": "客戶端的顯示名稱,可以稍後更改。",
|
||||||
|
"clientAddress": "客戶端位址(進階)",
|
||||||
|
"setupFailedToFetchSubnet": "取得預設子網路失敗",
|
||||||
|
"setupSubnetAdvanced": "子網路(進階)",
|
||||||
|
"setupSubnetDescription": "此組織內部網路的子網路。",
|
||||||
|
"setupUtilitySubnet": "工具子網路(進階)",
|
||||||
|
"setupUtilitySubnetDescription": "此組織別名位址和 DNS 伺服器的子網路。",
|
||||||
|
"siteRegenerateAndDisconnect": "重新產生並斷開連接",
|
||||||
|
"siteRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此站點的連接嗎?",
|
||||||
|
"siteRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開站點連接。站點需要使用新憑證重新啟動。",
|
||||||
|
"siteRegenerateCredentialsConfirmation": "您確定要重新產生此站點的憑證嗎?",
|
||||||
|
"siteRegenerateCredentialsWarning": "這將重新產生憑證。站點將保持連接,直到您手動重新啟動並使用新憑證。",
|
||||||
|
"clientRegenerateAndDisconnect": "重新產生並斷開連接",
|
||||||
|
"clientRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此客戶端的連接嗎?",
|
||||||
|
"clientRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開客戶端連接。客戶端需要使用新憑證重新啟動。",
|
||||||
|
"clientRegenerateCredentialsConfirmation": "您確定要重新產生此客戶端的憑證嗎?",
|
||||||
|
"clientRegenerateCredentialsWarning": "這將重新產生憑證。客戶端將保持連接,直到您手動重新啟動並使用新憑證。",
|
||||||
|
"remoteExitNodeRegenerateAndDisconnect": "重新產生並斷開連接",
|
||||||
|
"remoteExitNodeRegenerateAndDisconnectConfirmation": "您確定要重新產生憑證並斷開此遠端出口節點的連接嗎?",
|
||||||
|
"remoteExitNodeRegenerateAndDisconnectWarning": "這將重新產生憑證並立即斷開遠端出口節點連接。遠端出口節點需要使用新憑證重新啟動。",
|
||||||
|
"remoteExitNodeRegenerateCredentialsConfirmation": "您確定要重新產生此遠端出口節點的憑證嗎?",
|
||||||
|
"remoteExitNodeRegenerateCredentialsWarning": "這將重新產生憑證。遠端出口節點將保持連接,直到您手動重新啟動並使用新憑證。",
|
||||||
|
"agent": "代理",
|
||||||
|
"personalUseOnly": "僅限個人使用",
|
||||||
|
"loginPageLicenseWatermark": "此實例僅授權個人使用。",
|
||||||
|
"instanceIsUnlicensed": "此實例未授權。",
|
||||||
|
"portRestrictions": "連接埠限制",
|
||||||
|
"allPorts": "全部",
|
||||||
|
"custom": "自訂",
|
||||||
|
"allPortsAllowed": "允許所有連接埠",
|
||||||
|
"allPortsBlocked": "阻擋所有連接埠",
|
||||||
|
"tcpPortsDescription": "指定此資源允許的 TCP 連接埠。使用「*」表示所有連接埠,留空表示阻擋全部,或輸入以逗號分隔的連接埠和範圍(例如:80,443,8000-9000)。",
|
||||||
|
"udpPortsDescription": "指定此資源允許的 UDP 連接埠。使用「*」表示所有連接埠,留空表示阻擋全部,或輸入以逗號分隔的連接埠和範圍(例如:53,123,500-600)。",
|
||||||
|
"organizationLoginPageTitle": "組織登入頁面",
|
||||||
|
"organizationLoginPageDescription": "自訂此組織的登入頁面",
|
||||||
|
"resourceLoginPageTitle": "資源登入頁面",
|
||||||
|
"resourceLoginPageDescription": "自訂個別資源的登入頁面",
|
||||||
|
"enterConfirmation": "輸入確認",
|
||||||
|
"blueprintViewDetails": "詳細資訊",
|
||||||
|
"defaultIdentityProvider": "預設身份提供者",
|
||||||
|
"defaultIdentityProviderDescription": "當選擇預設身份提供者時,使用者將自動被重新導向到該提供者進行驗證。",
|
||||||
|
"editInternalResourceDialogNetworkSettings": "網路設定",
|
||||||
|
"editInternalResourceDialogAccessPolicy": "存取策略",
|
||||||
|
"editInternalResourceDialogAddRoles": "新增角色",
|
||||||
|
"editInternalResourceDialogAddUsers": "新增使用者",
|
||||||
|
"editInternalResourceDialogAddClients": "新增客戶端",
|
||||||
|
"editInternalResourceDialogDestinationLabel": "目的地",
|
||||||
|
"editInternalResourceDialogDestinationDescription": "指定內部資源的目的地位址。根據所選模式,這可以是主機名稱、IP 位址或 CIDR 範圍。可選擇設定內部 DNS 別名以便識別。",
|
||||||
|
"editInternalResourceDialogPortRestrictionsDescription": "限制對特定 TCP/UDP 連接埠的存取,或允許/阻擋所有連接埠。",
|
||||||
|
"editInternalResourceDialogTcp": "TCP",
|
||||||
|
"editInternalResourceDialogUdp": "UDP",
|
||||||
|
"editInternalResourceDialogIcmp": "ICMP",
|
||||||
|
"editInternalResourceDialogAccessControl": "存取控制",
|
||||||
|
"editInternalResourceDialogAccessControlDescription": "控制哪些角色、使用者和機器客戶端在連接時可以存取此資源。管理員始終擁有存取權限。",
|
||||||
|
"editInternalResourceDialogPortRangeValidationError": "連接埠範圍必須是「*」表示所有連接埠,或以逗號分隔的連接埠和範圍列表(例如:「80,443,8000-9000」)。連接埠必須介於 1 到 65535 之間。",
|
||||||
|
"orgAuthWhatsThis": "我在哪裡可以找到我的組織 ID?",
|
||||||
|
"learnMore": "了解更多",
|
||||||
|
"backToHome": "返回首頁",
|
||||||
|
"needToSignInToOrg": "需要使用您組織的身份提供者嗎?",
|
||||||
|
"maintenanceMode": "維護模式",
|
||||||
|
"maintenanceModeDescription": "向訪客顯示維護頁面",
|
||||||
|
"maintenanceModeType": "維護模式類型",
|
||||||
|
"showMaintenancePage": "向訪客顯示維護頁面",
|
||||||
|
"enableMaintenanceMode": "啟用維護模式",
|
||||||
|
"automatic": "自動",
|
||||||
|
"automaticModeDescription": "僅在所有後端目標都關閉或不健康時顯示維護頁面。只要至少有一個目標健康,您的資源就會正常運作。",
|
||||||
|
"forced": "強制",
|
||||||
|
"forcedModeDescription": "無論後端健康狀況如何,始終顯示維護頁面。當您想要阻止所有存取時,用於計劃維護。",
|
||||||
|
"warning:": "警告:",
|
||||||
|
"forcedeModeWarning": "所有流量將被導向維護頁面。您的後端資源將不會收到任何請求。",
|
||||||
|
"pageTitle": "頁面標題",
|
||||||
|
"pageTitleDescription": "維護頁面上顯示的主標題",
|
||||||
|
"maintenancePageMessage": "維護訊息",
|
||||||
|
"maintenancePageMessagePlaceholder": "我們很快就會回來!我們的網站目前正在進行預定維護。",
|
||||||
|
"maintenancePageMessageDescription": "說明維護的詳細訊息",
|
||||||
|
"maintenancePageTimeTitle": "預計完成時間(可選)",
|
||||||
|
"maintenanceTime": "例如:2 小時、11 月 1 日下午 5:00",
|
||||||
|
"maintenanceEstimatedTimeDescription": "您預計何時完成維護",
|
||||||
|
"editDomain": "編輯網域",
|
||||||
|
"editDomainDescription": "為您的資源選擇網域",
|
||||||
|
"maintenanceModeDisabledTooltip": "此功能需要有效的授權才能啟用。",
|
||||||
|
"maintenanceScreenTitle": "服務暫時無法使用",
|
||||||
|
"maintenanceScreenMessage": "我們目前遇到技術問題。請稍後再試。",
|
||||||
|
"maintenanceScreenEstimatedCompletion": "預計完成時間:",
|
||||||
|
"createInternalResourceDialogDestinationRequired": "目的地為必填欄位"
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,7 @@ import createNextIntlPlugin from "next-intl/plugin";
|
|||||||
const withNextIntl = createNextIntlPlugin();
|
const withNextIntl = createNextIntlPlugin();
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
|
reactStrictMode: false,
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true
|
ignoreDuringBuilds: true
|
||||||
},
|
},
|
||||||
|
|||||||
11557
package-lock.json
generated
234
package.json
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
|
"description": "Identity-aware VPN and proxy for remote access to anything, anywhere and Dashboard UI",
|
||||||
"homepage": "https://github.com/fosrl/pangolin",
|
"homepage": "https://github.com/fosrl/pangolin",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -12,33 +12,33 @@
|
|||||||
"license": "SEE LICENSE IN LICENSE AND README.md",
|
"license": "SEE LICENSE IN LICENSE AND README.md",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts",
|
"dev": "NODE_ENV=development ENVIRONMENT=dev tsx watch server/index.ts",
|
||||||
"db:pg:generate": "drizzle-kit generate --config=./drizzle.pg.config.ts",
|
"dev:check": "npx tsc --noEmit && npm run format:check",
|
||||||
"db:sqlite:generate": "drizzle-kit generate --config=./drizzle.sqlite.config.ts",
|
"dev:setup": "cp config/config.example.yml config/config.yml && npm run set:oss && npm run set:sqlite && npm run db:sqlite:generate && npm run db:sqlite:push",
|
||||||
"db:pg:push": "npx tsx server/db/pg/migrate.ts",
|
"db:generate": "drizzle-kit generate --config=./drizzle.config.ts",
|
||||||
"db:sqlite:push": "npx tsx server/db/sqlite/migrate.ts",
|
"db:push": "npx tsx server/db/migrate.ts",
|
||||||
"db:sqlite:studio": "drizzle-kit studio --config=./drizzle.sqlite.config.ts",
|
"db:studio": "drizzle-kit studio --config=./drizzle.config.ts",
|
||||||
"db:pg:studio": "drizzle-kit studio --config=./drizzle.pg.config.ts",
|
|
||||||
"db:clear-migrations": "rm -rf server/migrations",
|
"db:clear-migrations": "rm -rf server/migrations",
|
||||||
"set:oss": "echo 'export const build = \"oss\" as any;' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
"set:oss": "echo 'export const build = \"oss\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.oss.json tsconfig.json",
|
||||||
"set:saas": "echo 'export const build = \"saas\" as any;' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
"set:saas": "echo 'export const build = \"saas\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.saas.json tsconfig.json",
|
||||||
"set:enterprise": "echo 'export const build = \"enterprise\" as any;' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
"set:enterprise": "echo 'export const build = \"enterprise\" as \"saas\" | \"enterprise\" | \"oss\";' > server/build.ts && cp tsconfig.enterprise.json tsconfig.json",
|
||||||
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts",
|
"set:sqlite": "echo 'export * from \"./sqlite\";\nexport const driver: \"pg\" | \"sqlite\" = \"sqlite\";' > server/db/index.ts && cp drizzle.sqlite.config.ts drizzle.config.ts && cp server/setup/migrationsSqlite.ts server/setup/migrations.ts",
|
||||||
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts",
|
"set:pg": "echo 'export * from \"./pg\";\nexport const driver: \"pg\" | \"sqlite\" = \"pg\";' > server/db/index.ts && cp drizzle.pg.config.ts drizzle.config.ts && cp server/setup/migrationsPg.ts server/setup/migrations.ts",
|
||||||
"next:build": "next build",
|
"build:next": "next build",
|
||||||
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
"build": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrations.ts -o dist/migrations.mjs",
|
||||||
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
|
||||||
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
|
"start": "ENVIRONMENT=prod node dist/migrations.mjs && ENVIRONMENT=prod NODE_ENV=development node --enable-source-maps dist/server.mjs",
|
||||||
"email": "email dev --dir server/emails/templates --port 3005",
|
"email": "email dev --dir server/emails/templates --port 3005",
|
||||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs",
|
||||||
|
"format:check": "prettier --check .",
|
||||||
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "8.1.0",
|
"@asteasolutions/zod-to-openapi": "8.4.0",
|
||||||
"@aws-sdk/client-s3": "3.922.0",
|
"@aws-sdk/client-s3": "3.989.0",
|
||||||
"@faker-js/faker": "^10.1.0",
|
"@faker-js/faker": "10.3.0",
|
||||||
"@headlessui/react": "^2.2.9",
|
"@headlessui/react": "2.2.9",
|
||||||
"@hookform/resolvers": "5.2.2",
|
"@hookform/resolvers": "5.2.2",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "4.7.0",
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "2.0.2",
|
||||||
"@oslojs/crypto": "1.0.1",
|
"@oslojs/crypto": "1.0.1",
|
||||||
"@oslojs/encoding": "1.1.0",
|
"@oslojs/encoding": "1.1.0",
|
||||||
"@radix-ui/react-avatar": "1.1.11",
|
"@radix-ui/react-avatar": "1.1.11",
|
||||||
@@ -49,138 +49,128 @@
|
|||||||
"@radix-ui/react-icons": "1.3.2",
|
"@radix-ui/react-icons": "1.3.2",
|
||||||
"@radix-ui/react-label": "2.1.8",
|
"@radix-ui/react-label": "2.1.8",
|
||||||
"@radix-ui/react-popover": "1.1.15",
|
"@radix-ui/react-popover": "1.1.15",
|
||||||
"@radix-ui/react-progress": "^1.1.8",
|
"@radix-ui/react-progress": "1.1.8",
|
||||||
"@radix-ui/react-radio-group": "1.3.8",
|
"@radix-ui/react-radio-group": "1.3.8",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "1.2.10",
|
||||||
"@radix-ui/react-select": "2.2.6",
|
"@radix-ui/react-select": "2.2.6",
|
||||||
"@radix-ui/react-separator": "1.1.8",
|
"@radix-ui/react-separator": "1.1.8",
|
||||||
"@radix-ui/react-slot": "1.2.4",
|
"@radix-ui/react-slot": "1.2.4",
|
||||||
"@radix-ui/react-switch": "1.2.6",
|
"@radix-ui/react-switch": "1.2.6",
|
||||||
"@radix-ui/react-tabs": "1.1.13",
|
"@radix-ui/react-tabs": "1.1.13",
|
||||||
"@radix-ui/react-toast": "1.2.15",
|
"@radix-ui/react-toast": "1.2.15",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@radix-ui/react-tooltip": "1.2.8",
|
||||||
"@react-email/components": "0.5.7",
|
"@react-email/components": "1.0.7",
|
||||||
"@react-email/render": "^1.3.2",
|
"@react-email/render": "2.0.4",
|
||||||
"@react-email/tailwind": "1.2.2",
|
"@react-email/tailwind": "2.0.4",
|
||||||
"@simplewebauthn/browser": "^13.2.2",
|
"@simplewebauthn/browser": "13.2.2",
|
||||||
"@simplewebauthn/server": "^13.2.2",
|
"@simplewebauthn/server": "13.2.2",
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "0.5.11",
|
||||||
"@tanstack/react-query": "^5.90.6",
|
"@tanstack/react-query": "5.90.21",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"arctic": "^3.7.0",
|
"arctic": "3.7.0",
|
||||||
"axios": "^1.13.2",
|
"axios": "1.13.5",
|
||||||
"better-sqlite3": "11.7.0",
|
"better-sqlite3": "11.9.1",
|
||||||
"canvas-confetti": "1.9.4",
|
"canvas-confetti": "1.9.4",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "1.1.1",
|
"cmdk": "1.1.1",
|
||||||
"cookie": "^1.0.2",
|
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"cookies": "^0.9.1",
|
"cors": "2.8.6",
|
||||||
"cors": "2.8.5",
|
"crypto-js": "4.2.0",
|
||||||
"crypto-js": "^4.2.0",
|
"d3": "7.9.0",
|
||||||
"d3": "^7.9.0",
|
"drizzle-orm": "0.45.1",
|
||||||
"date-fns": "4.1.0",
|
"express": "5.2.1",
|
||||||
"drizzle-orm": "0.44.7",
|
|
||||||
"eslint": "9.39.1",
|
|
||||||
"eslint-config-next": "16.0.3",
|
|
||||||
"express": "5.1.0",
|
|
||||||
"express-rate-limit": "8.2.1",
|
"express-rate-limit": "8.2.1",
|
||||||
"glob": "11.1.0",
|
"glob": "13.0.3",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.1",
|
||||||
"i": "^0.3.7",
|
|
||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"ioredis": "5.8.2",
|
"ioredis": "5.9.3",
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "0.16.0",
|
||||||
"js-yaml": "4.1.1",
|
"js-yaml": "4.1.1",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "9.0.3",
|
||||||
"lucide-react": "^0.552.0",
|
"lucide-react": "0.563.0",
|
||||||
"maxmind": "5.0.1",
|
"maxmind": "5.0.5",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.5.7",
|
"next": "15.5.12",
|
||||||
"next-intl": "^4.4.0",
|
"next-intl": "4.8.2",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nextjs-toploader": "^3.9.17",
|
"nextjs-toploader": "3.9.17",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-fetch": "3.3.2",
|
"nodemailer": "8.0.1",
|
||||||
"nodemailer": "7.0.10",
|
|
||||||
"npm": "^11.6.4",
|
|
||||||
"nprogress": "^0.2.0",
|
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "^8.16.2",
|
"pg": "8.18.0",
|
||||||
"posthog-node": "^5.11.2",
|
"posthog-node": "5.24.15",
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.2.1",
|
"react": "19.2.4",
|
||||||
"react-day-picker": "9.11.1",
|
"react-day-picker": "9.13.2",
|
||||||
"react-dom": "19.2.1",
|
"react-dom": "19.2.4",
|
||||||
"react-easy-sort": "^1.8.0",
|
"react-easy-sort": "1.8.0",
|
||||||
"react-hook-form": "7.66.0",
|
"react-hook-form": "7.71.1",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "5.5.0",
|
||||||
"rebuild": "0.1.2",
|
"recharts": "2.15.4",
|
||||||
"recharts": "^2.15.4",
|
"reodotdev": "1.0.0",
|
||||||
"reodotdev": "^1.0.0",
|
"resend": "6.9.2",
|
||||||
"resend": "^6.4.2",
|
"semver": "7.7.4",
|
||||||
"semver": "^7.7.3",
|
"sshpk": "^1.18.0",
|
||||||
"stripe": "18.2.1",
|
"stripe": "20.3.1",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "3.4.0",
|
||||||
"topojson-client": "^3.1.0",
|
"topojson-client": "3.1.0",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "1.4.0",
|
||||||
"uuid": "^13.0.0",
|
"use-debounce": "^10.1.0",
|
||||||
|
"uuid": "13.0.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"visionscarto-world-atlas": "^1.0.0",
|
"visionscarto-world-atlas": "1.0.0",
|
||||||
"winston": "3.18.3",
|
"winston": "3.19.0",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.3",
|
"ws": "8.19.0",
|
||||||
"yaml": "^2.8.1",
|
"yaml": "2.8.2",
|
||||||
"yargs": "18.0.0",
|
"yargs": "18.0.0",
|
||||||
"zod": "4.1.12",
|
"zod": "4.3.6",
|
||||||
"zod-validation-error": "5.0.0"
|
"zod-validation-error": "5.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.51.1",
|
"@dotenvx/dotenvx": "1.52.0",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@react-email/preview-server": "4.3.2",
|
"@react-email/preview-server": "5.2.8",
|
||||||
"@tailwindcss/postcss": "^4.1.17",
|
"@tailwindcss/postcss": "4.1.18",
|
||||||
"@tanstack/react-query-devtools": "^5.90.2",
|
"@tanstack/react-query-devtools": "5.91.3",
|
||||||
"@types/better-sqlite3": "7.6.12",
|
"@types/better-sqlite3": "7.6.13",
|
||||||
"@types/cookie-parser": "1.4.10",
|
"@types/cookie-parser": "1.4.10",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "4.2.2",
|
||||||
"@types/d3": "^7.4.3",
|
"@types/d3": "7.4.3",
|
||||||
"@types/express": "5.0.5",
|
"@types/express": "5.0.6",
|
||||||
"@types/express-session": "^1.18.2",
|
"@types/express-session": "1.18.2",
|
||||||
"@types/jmespath": "^0.15.2",
|
"@types/jmespath": "0.15.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "9.0.10",
|
||||||
"@types/node": "24.10.1",
|
"@types/node": "25.2.3",
|
||||||
"@types/nodemailer": "7.0.3",
|
"@types/nodemailer": "7.0.9",
|
||||||
"@types/nprogress": "^0.2.3",
|
"@types/nprogress": "0.2.3",
|
||||||
"@types/pg": "8.15.6",
|
"@types/pg": "8.16.0",
|
||||||
"@types/react": "19.2.2",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.2",
|
"@types/react-dom": "19.2.3",
|
||||||
"@types/semver": "^7.7.1",
|
"@types/semver": "7.7.1",
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/sshpk": "^1.17.4",
|
||||||
"@types/topojson-client": "^3.1.5",
|
"@types/swagger-ui-express": "4.1.8",
|
||||||
|
"@types/topojson-client": "3.1.5",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@types/yargs": "17.0.34",
|
"@types/yargs": "17.0.35",
|
||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
"drizzle-kit": "0.31.6",
|
"drizzle-kit": "0.31.9",
|
||||||
"esbuild": "0.27.0",
|
"esbuild": "0.27.3",
|
||||||
"esbuild-node-externals": "1.19.1",
|
"esbuild-node-externals": "1.20.1",
|
||||||
"postcss": "^8",
|
"eslint": "9.39.2",
|
||||||
"react-email": "4.3.2",
|
"eslint-config-next": "16.1.6",
|
||||||
"tailwindcss": "^4.1.4",
|
"postcss": "8.5.6",
|
||||||
|
"prettier": "3.8.1",
|
||||||
|
"react-email": "5.2.8",
|
||||||
|
"tailwindcss": "4.1.18",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.20.6",
|
"tsx": "4.21.0",
|
||||||
"typescript": "^5",
|
"typescript": "5.9.3",
|
||||||
"typescript-eslint": "^8.46.3"
|
"typescript-eslint": "8.55.0"
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"emblor": {
|
|
||||||
"react": "19.0.0",
|
|
||||||
"react-dom": "19.0.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
"@tailwindcss/postcss": {}
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 687 KiB |
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 493 KiB |
|
Before Width: | Height: | Size: 636 KiB |
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 484 KiB |
BIN
public/screenshots/private-resources.png
Normal file
|
After Width: | Height: | Size: 421 KiB |
BIN
public/screenshots/public-resources.png
Normal file
|
After Width: | Height: | Size: 484 KiB |
|
Before Width: | Height: | Size: 713 KiB |
|
Before Width: | Height: | Size: 456 KiB |
|
Before Width: | Height: | Size: 674 KiB After Width: | Height: | Size: 396 KiB |
BIN
public/screenshots/user-devices.png
Normal file
|
After Width: | Height: | Size: 434 KiB |
@@ -78,6 +78,10 @@ export enum ActionsEnum {
|
|||||||
updateSiteResource = "updateSiteResource",
|
updateSiteResource = "updateSiteResource",
|
||||||
createClient = "createClient",
|
createClient = "createClient",
|
||||||
deleteClient = "deleteClient",
|
deleteClient = "deleteClient",
|
||||||
|
archiveClient = "archiveClient",
|
||||||
|
unarchiveClient = "unarchiveClient",
|
||||||
|
blockClient = "blockClient",
|
||||||
|
unblockClient = "unblockClient",
|
||||||
updateClient = "updateClient",
|
updateClient = "updateClient",
|
||||||
listClients = "listClients",
|
listClients = "listClients",
|
||||||
getClient = "getClient",
|
getClient = "getClient",
|
||||||
@@ -125,7 +129,10 @@ export enum ActionsEnum {
|
|||||||
getBlueprint = "getBlueprint",
|
getBlueprint = "getBlueprint",
|
||||||
applyBlueprint = "applyBlueprint",
|
applyBlueprint = "applyBlueprint",
|
||||||
viewLogs = "viewLogs",
|
viewLogs = "viewLogs",
|
||||||
exportLogs = "exportLogs"
|
exportLogs = "exportLogs",
|
||||||
|
listApprovals = "listApprovals",
|
||||||
|
updateApprovals = "updateApprovals",
|
||||||
|
signSshKey = "signSshKey"
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
|
|||||||
45
server/auth/canUserAccessSiteResource.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { db } from "@server/db";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { roleSiteResources, userSiteResources } from "@server/db";
|
||||||
|
|
||||||
|
export async function canUserAccessSiteResource({
|
||||||
|
userId,
|
||||||
|
resourceId,
|
||||||
|
roleId
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
resourceId: number;
|
||||||
|
roleId: number;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
const roleResourceAccess = await db
|
||||||
|
.select()
|
||||||
|
.from(roleSiteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(roleSiteResources.siteResourceId, resourceId),
|
||||||
|
eq(roleSiteResources.roleId, roleId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (roleResourceAccess.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userResourceAccess = await db
|
||||||
|
.select()
|
||||||
|
.from(userSiteResources)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(userSiteResources.userId, userId),
|
||||||
|
eq(userSiteResources.siteResourceId, resourceId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (userResourceAccess.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
@@ -2,13 +2,13 @@ import { hash, verify } from "@node-rs/argon2";
|
|||||||
|
|
||||||
export async function verifyPassword(
|
export async function verifyPassword(
|
||||||
password: string,
|
password: string,
|
||||||
hash: string,
|
hash: string
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const validPassword = await verify(hash, password, {
|
const validPassword = await verify(hash, password, {
|
||||||
memoryCost: 19456,
|
memoryCost: 19456,
|
||||||
timeCost: 2,
|
timeCost: 2,
|
||||||
outputLen: 32,
|
outputLen: 32,
|
||||||
parallelism: 1,
|
parallelism: 1
|
||||||
});
|
});
|
||||||
return validPassword;
|
return validPassword;
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ export async function hashPassword(password: string): Promise<string> {
|
|||||||
memoryCost: 19456,
|
memoryCost: 19456,
|
||||||
timeCost: 2,
|
timeCost: 2,
|
||||||
outputLen: 32,
|
outputLen: 32,
|
||||||
parallelism: 1,
|
parallelism: 1
|
||||||
});
|
});
|
||||||
|
|
||||||
return passwordHash;
|
return passwordHash;
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ export const passwordSchema = z
|
|||||||
.string()
|
.string()
|
||||||
.min(8, { message: "Password must be at least 8 characters long" })
|
.min(8, { message: "Password must be at least 8 characters long" })
|
||||||
.max(128, { message: "Password must be at most 128 characters long" })
|
.max(128, { message: "Password must be at most 128 characters long" })
|
||||||
.regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]).*$/, {
|
.regex(
|
||||||
|
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]).*$/,
|
||||||
|
{
|
||||||
message: `Your password must meet the following conditions:
|
message: `Your password must meet the following conditions:
|
||||||
at least one uppercase English letter,
|
at least one uppercase English letter,
|
||||||
at least one lowercase English letter,
|
at least one lowercase English letter,
|
||||||
at least one digit,
|
at least one digit,
|
||||||
at least one special character.`
|
at least one special character.`
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
encodeHexLowerCase,
|
|
||||||
} from "@oslojs/encoding";
|
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
import { Newt, newts, newtSessions, NewtSession } from "@server/db";
|
import { Newt, newts, newtSessions, NewtSession } from "@server/db";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
@@ -10,25 +8,25 @@ export const EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
|||||||
|
|
||||||
export async function createNewtSession(
|
export async function createNewtSession(
|
||||||
token: string,
|
token: string,
|
||||||
newtId: string,
|
newtId: string
|
||||||
): Promise<NewtSession> {
|
): Promise<NewtSession> {
|
||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token)),
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const session: NewtSession = {
|
const session: NewtSession = {
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
newtId,
|
newtId,
|
||||||
expiresAt: new Date(Date.now() + EXPIRES).getTime(),
|
expiresAt: new Date(Date.now() + EXPIRES).getTime()
|
||||||
};
|
};
|
||||||
await db.insert(newtSessions).values(session);
|
await db.insert(newtSessions).values(session);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateNewtSessionToken(
|
export async function validateNewtSessionToken(
|
||||||
token: string,
|
token: string
|
||||||
): Promise<SessionValidationResult> {
|
): Promise<SessionValidationResult> {
|
||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token)),
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const result = await db
|
const result = await db
|
||||||
.select({ newt: newts, session: newtSessions })
|
.select({ newt: newts, session: newtSessions })
|
||||||
@@ -45,14 +43,12 @@ export async function validateNewtSessionToken(
|
|||||||
.where(eq(newtSessions.sessionId, session.sessionId));
|
.where(eq(newtSessions.sessionId, session.sessionId));
|
||||||
return { session: null, newt: null };
|
return { session: null, newt: null };
|
||||||
}
|
}
|
||||||
if (Date.now() >= session.expiresAt - (EXPIRES / 2)) {
|
if (Date.now() >= session.expiresAt - EXPIRES / 2) {
|
||||||
session.expiresAt = new Date(
|
session.expiresAt = new Date(Date.now() + EXPIRES).getTime();
|
||||||
Date.now() + EXPIRES,
|
|
||||||
).getTime();
|
|
||||||
await db
|
await db
|
||||||
.update(newtSessions)
|
.update(newtSessions)
|
||||||
.set({
|
.set({
|
||||||
expiresAt: session.expiresAt,
|
expiresAt: session.expiresAt
|
||||||
})
|
})
|
||||||
.where(eq(newtSessions.sessionId, session.sessionId));
|
.where(eq(newtSessions.sessionId, session.sessionId));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import {
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
encodeHexLowerCase,
|
|
||||||
} from "@oslojs/encoding";
|
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
import { Olm, olms, olmSessions, OlmSession } from "@server/db";
|
import { Olm, olms, olmSessions, OlmSession } from "@server/db";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
@@ -10,25 +8,25 @@ export const EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
|||||||
|
|
||||||
export async function createOlmSession(
|
export async function createOlmSession(
|
||||||
token: string,
|
token: string,
|
||||||
olmId: string,
|
olmId: string
|
||||||
): Promise<OlmSession> {
|
): Promise<OlmSession> {
|
||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token)),
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const session: OlmSession = {
|
const session: OlmSession = {
|
||||||
sessionId: sessionId,
|
sessionId: sessionId,
|
||||||
olmId,
|
olmId,
|
||||||
expiresAt: new Date(Date.now() + EXPIRES).getTime(),
|
expiresAt: new Date(Date.now() + EXPIRES).getTime()
|
||||||
};
|
};
|
||||||
await db.insert(olmSessions).values(session);
|
await db.insert(olmSessions).values(session);
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function validateOlmSessionToken(
|
export async function validateOlmSessionToken(
|
||||||
token: string,
|
token: string
|
||||||
): Promise<SessionValidationResult> {
|
): Promise<SessionValidationResult> {
|
||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token)),
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const result = await db
|
const result = await db
|
||||||
.select({ olm: olms, session: olmSessions })
|
.select({ olm: olms, session: olmSessions })
|
||||||
@@ -45,14 +43,12 @@ export async function validateOlmSessionToken(
|
|||||||
.where(eq(olmSessions.sessionId, session.sessionId));
|
.where(eq(olmSessions.sessionId, session.sessionId));
|
||||||
return { session: null, olm: null };
|
return { session: null, olm: null };
|
||||||
}
|
}
|
||||||
if (Date.now() >= session.expiresAt - (EXPIRES / 2)) {
|
if (Date.now() >= session.expiresAt - EXPIRES / 2) {
|
||||||
session.expiresAt = new Date(
|
session.expiresAt = new Date(Date.now() + EXPIRES).getTime();
|
||||||
Date.now() + EXPIRES,
|
|
||||||
).getTime();
|
|
||||||
await db
|
await db
|
||||||
.update(olmSessions)
|
.update(olmSessions)
|
||||||
.set({
|
.set({
|
||||||
expiresAt: session.expiresAt,
|
expiresAt: session.expiresAt
|
||||||
})
|
})
|
||||||
.where(eq(olmSessions.sessionId, session.sessionId));
|
.where(eq(olmSessions.sessionId, session.sessionId));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cleanup as wsCleanup } from "@server/routers/ws";
|
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
|
||||||
|
|
||||||
async function cleanup() {
|
async function cleanup() {
|
||||||
await wsCleanup();
|
await wsCleanup();
|
||||||
|
|||||||
@@ -56,15 +56,15 @@ Ensure drizzle-kit is installed.
|
|||||||
You must have a connection string in your config file, as shown above.
|
You must have a connection string in your config file, as shown above.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run db:pg:generate
|
npm run db:generate
|
||||||
npm run db:pg:push
|
npm run db:push
|
||||||
```
|
```
|
||||||
|
|
||||||
### SQLite
|
### SQLite
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm run db:sqlite:generate
|
npm run db:generate
|
||||||
npm run db:sqlite:push
|
npm run db:push
|
||||||
```
|
```
|
||||||
|
|
||||||
## Build Time
|
## Build Time
|
||||||
|
|||||||
321
server/db/asns.ts
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
// Curated list of major ASNs (Cloud Providers, CDNs, ISPs, etc.)
|
||||||
|
// This is not exhaustive - there are 100,000+ ASNs globally
|
||||||
|
// Users can still enter any ASN manually in the input field
|
||||||
|
export const MAJOR_ASNS = [
|
||||||
|
{
|
||||||
|
name: "ALL ASNs",
|
||||||
|
code: "ALL",
|
||||||
|
asn: 0 // Special value that will match all
|
||||||
|
},
|
||||||
|
// Major Cloud Providers
|
||||||
|
{
|
||||||
|
name: "Google LLC",
|
||||||
|
code: "AS15169",
|
||||||
|
asn: 15169
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Amazon AWS",
|
||||||
|
code: "AS16509",
|
||||||
|
asn: 16509
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Amazon AWS (EC2)",
|
||||||
|
code: "AS14618",
|
||||||
|
asn: 14618
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Microsoft Azure",
|
||||||
|
code: "AS8075",
|
||||||
|
asn: 8075
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Microsoft Corporation",
|
||||||
|
code: "AS8068",
|
||||||
|
asn: 8068
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "DigitalOcean",
|
||||||
|
code: "AS14061",
|
||||||
|
asn: 14061
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Linode",
|
||||||
|
code: "AS63949",
|
||||||
|
asn: 63949
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Hetzner Online",
|
||||||
|
code: "AS24940",
|
||||||
|
asn: 24940
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OVH SAS",
|
||||||
|
code: "AS16276",
|
||||||
|
asn: 16276
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Oracle Cloud",
|
||||||
|
code: "AS31898",
|
||||||
|
asn: 31898
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Alibaba Cloud",
|
||||||
|
code: "AS45102",
|
||||||
|
asn: 45102
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "IBM Cloud",
|
||||||
|
code: "AS36351",
|
||||||
|
asn: 36351
|
||||||
|
},
|
||||||
|
|
||||||
|
// CDNs
|
||||||
|
{
|
||||||
|
name: "Cloudflare",
|
||||||
|
code: "AS13335",
|
||||||
|
asn: 13335
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Fastly",
|
||||||
|
code: "AS54113",
|
||||||
|
asn: 54113
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Akamai Technologies",
|
||||||
|
code: "AS20940",
|
||||||
|
asn: 20940
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Akamai (Primary)",
|
||||||
|
code: "AS16625",
|
||||||
|
asn: 16625
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mobile Carriers - US
|
||||||
|
{
|
||||||
|
name: "T-Mobile USA",
|
||||||
|
code: "AS21928",
|
||||||
|
asn: 21928
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Verizon Wireless",
|
||||||
|
code: "AS6167",
|
||||||
|
asn: 6167
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "AT&T Mobility",
|
||||||
|
code: "AS20057",
|
||||||
|
asn: 20057
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Sprint (T-Mobile)",
|
||||||
|
code: "AS1239",
|
||||||
|
asn: 1239
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "US Cellular",
|
||||||
|
code: "AS6430",
|
||||||
|
asn: 6430
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mobile Carriers - Europe
|
||||||
|
{
|
||||||
|
name: "Vodafone UK",
|
||||||
|
code: "AS25135",
|
||||||
|
asn: 25135
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "EE (UK)",
|
||||||
|
code: "AS12576",
|
||||||
|
asn: 12576
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Three UK",
|
||||||
|
code: "AS29194",
|
||||||
|
asn: 29194
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "O2 UK",
|
||||||
|
code: "AS13285",
|
||||||
|
asn: 13285
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Telefonica Spain Mobile",
|
||||||
|
code: "AS12430",
|
||||||
|
asn: 12430
|
||||||
|
},
|
||||||
|
|
||||||
|
// Mobile Carriers - Asia
|
||||||
|
{
|
||||||
|
name: "NTT DoCoMo (Japan)",
|
||||||
|
code: "AS9605",
|
||||||
|
asn: 9605
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SoftBank Mobile (Japan)",
|
||||||
|
code: "AS17676",
|
||||||
|
asn: 17676
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "SK Telecom (Korea)",
|
||||||
|
code: "AS9318",
|
||||||
|
asn: 9318
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "KT Corporation Mobile (Korea)",
|
||||||
|
code: "AS4766",
|
||||||
|
asn: 4766
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Airtel India",
|
||||||
|
code: "AS24560",
|
||||||
|
asn: 24560
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "China Mobile",
|
||||||
|
code: "AS9808",
|
||||||
|
asn: 9808
|
||||||
|
},
|
||||||
|
|
||||||
|
// Major US ISPs
|
||||||
|
{
|
||||||
|
name: "AT&T Services",
|
||||||
|
code: "AS7018",
|
||||||
|
asn: 7018
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Comcast Cable",
|
||||||
|
code: "AS7922",
|
||||||
|
asn: 7922
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Verizon",
|
||||||
|
code: "AS701",
|
||||||
|
asn: 701
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Cox Communications",
|
||||||
|
code: "AS22773",
|
||||||
|
asn: 22773
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Charter Communications",
|
||||||
|
code: "AS20115",
|
||||||
|
asn: 20115
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CenturyLink",
|
||||||
|
code: "AS209",
|
||||||
|
asn: 209
|
||||||
|
},
|
||||||
|
|
||||||
|
// Major European ISPs
|
||||||
|
{
|
||||||
|
name: "Deutsche Telekom",
|
||||||
|
code: "AS3320",
|
||||||
|
asn: 3320
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Vodafone",
|
||||||
|
code: "AS1273",
|
||||||
|
asn: 1273
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "British Telecom",
|
||||||
|
code: "AS2856",
|
||||||
|
asn: 2856
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Orange",
|
||||||
|
code: "AS3215",
|
||||||
|
asn: 3215
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Telefonica",
|
||||||
|
code: "AS12956",
|
||||||
|
asn: 12956
|
||||||
|
},
|
||||||
|
|
||||||
|
// Major Asian ISPs
|
||||||
|
{
|
||||||
|
name: "China Telecom",
|
||||||
|
code: "AS4134",
|
||||||
|
asn: 4134
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "China Unicom",
|
||||||
|
code: "AS4837",
|
||||||
|
asn: 4837
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NTT Communications",
|
||||||
|
code: "AS2914",
|
||||||
|
asn: 2914
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "KDDI Corporation",
|
||||||
|
code: "AS2516",
|
||||||
|
asn: 2516
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Reliance Jio (India)",
|
||||||
|
code: "AS55836",
|
||||||
|
asn: 55836
|
||||||
|
},
|
||||||
|
|
||||||
|
// VPN/Proxy Providers
|
||||||
|
{
|
||||||
|
name: "Private Internet Access",
|
||||||
|
code: "AS46562",
|
||||||
|
asn: 46562
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "NordVPN",
|
||||||
|
code: "AS202425",
|
||||||
|
asn: 202425
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Mullvad VPN",
|
||||||
|
code: "AS213281",
|
||||||
|
asn: 213281
|
||||||
|
},
|
||||||
|
|
||||||
|
// Social Media / Major Tech
|
||||||
|
{
|
||||||
|
name: "Facebook/Meta",
|
||||||
|
code: "AS32934",
|
||||||
|
asn: 32934
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Twitter/X",
|
||||||
|
code: "AS13414",
|
||||||
|
asn: 13414
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Apple",
|
||||||
|
code: "AS714",
|
||||||
|
asn: 714
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Netflix",
|
||||||
|
code: "AS2906",
|
||||||
|
asn: 2906
|
||||||
|
},
|
||||||
|
|
||||||
|
// Academic/Research
|
||||||
|
{
|
||||||
|
name: "MIT",
|
||||||
|
code: "AS3",
|
||||||
|
asn: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Stanford University",
|
||||||
|
code: "AS32",
|
||||||
|
asn: 32
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CERN",
|
||||||
|
code: "AS513",
|
||||||
|
asn: 513
|
||||||
|
}
|
||||||
|
];
|
||||||
150
server/db/ios_models.json
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"iPad1,1": "iPad",
|
||||||
|
"iPad2,1": "iPad 2",
|
||||||
|
"iPad2,2": "iPad 2",
|
||||||
|
"iPad2,3": "iPad 2",
|
||||||
|
"iPad2,4": "iPad 2",
|
||||||
|
"iPad3,1": "iPad 3rd Gen",
|
||||||
|
"iPad3,3": "iPad 3rd Gen",
|
||||||
|
"iPad3,2": "iPad 3rd Gen",
|
||||||
|
"iPad3,4": "iPad 4th Gen",
|
||||||
|
"iPad3,5": "iPad 4th Gen",
|
||||||
|
"iPad3,6": "iPad 4th Gen",
|
||||||
|
"iPad6,11": "iPad 9.7 5th Gen",
|
||||||
|
"iPad6,12": "iPad 9.7 5th Gen",
|
||||||
|
"iPad7,5": "iPad 9.7 6th Gen",
|
||||||
|
"iPad7,6": "iPad 9.7 6th Gen",
|
||||||
|
"iPad7,11": "iPad 10.2 7th Gen",
|
||||||
|
"iPad7,12": "iPad 10.2 7th Gen",
|
||||||
|
"iPad11,6": "iPad 10.2 8th Gen",
|
||||||
|
"iPad11,7": "iPad 10.2 8th Gen",
|
||||||
|
"iPad12,1": "iPad 10.2 9th Gen",
|
||||||
|
"iPad12,2": "iPad 10.2 9th Gen",
|
||||||
|
"iPad13,18": "iPad 10.9 10th Gen",
|
||||||
|
"iPad13,19": "iPad 10.9 10th Gen",
|
||||||
|
"iPad4,1": "iPad Air",
|
||||||
|
"iPad4,2": "iPad Air",
|
||||||
|
"iPad4,3": "iPad Air",
|
||||||
|
"iPad5,3": "iPad Air 2",
|
||||||
|
"iPad5,4": "iPad Air 2",
|
||||||
|
"iPad11,3": "iPad Air 3rd Gen",
|
||||||
|
"iPad11,4": "iPad Air 3rd Gen",
|
||||||
|
"iPad13,1": "iPad Air 4th Gen",
|
||||||
|
"iPad13,2": "iPad Air 4th Gen",
|
||||||
|
"iPad13,16": "iPad Air 5th Gen",
|
||||||
|
"iPad13,17": "iPad Air 5th Gen",
|
||||||
|
"iPad14,8": "iPad Air M2 11",
|
||||||
|
"iPad14,9": "iPad Air M2 11",
|
||||||
|
"iPad14,10": "iPad Air M2 13",
|
||||||
|
"iPad14,11": "iPad Air M2 13",
|
||||||
|
"iPad2,5": "iPad mini",
|
||||||
|
"iPad2,6": "iPad mini",
|
||||||
|
"iPad2,7": "iPad mini",
|
||||||
|
"iPad4,4": "iPad mini 2",
|
||||||
|
"iPad4,5": "iPad mini 2",
|
||||||
|
"iPad4,6": "iPad mini 2",
|
||||||
|
"iPad4,7": "iPad mini 3",
|
||||||
|
"iPad4,8": "iPad mini 3",
|
||||||
|
"iPad4,9": "iPad mini 3",
|
||||||
|
"iPad5,1": "iPad mini 4",
|
||||||
|
"iPad5,2": "iPad mini 4",
|
||||||
|
"iPad11,1": "iPad mini 5th Gen",
|
||||||
|
"iPad11,2": "iPad mini 5th Gen",
|
||||||
|
"iPad14,1": "iPad mini 6th Gen",
|
||||||
|
"iPad14,2": "iPad mini 6th Gen",
|
||||||
|
"iPad6,7": "iPad Pro 12.9",
|
||||||
|
"iPad6,8": "iPad Pro 12.9",
|
||||||
|
"iPad6,3": "iPad Pro 9.7",
|
||||||
|
"iPad6,4": "iPad Pro 9.7",
|
||||||
|
"iPad7,3": "iPad Pro 10.5",
|
||||||
|
"iPad7,4": "iPad Pro 10.5",
|
||||||
|
"iPad7,1": "iPad Pro 12.9",
|
||||||
|
"iPad7,2": "iPad Pro 12.9",
|
||||||
|
"iPad8,1": "iPad Pro 11",
|
||||||
|
"iPad8,2": "iPad Pro 11",
|
||||||
|
"iPad8,3": "iPad Pro 11",
|
||||||
|
"iPad8,4": "iPad Pro 11",
|
||||||
|
"iPad8,5": "iPad Pro 12.9",
|
||||||
|
"iPad8,6": "iPad Pro 12.9",
|
||||||
|
"iPad8,7": "iPad Pro 12.9",
|
||||||
|
"iPad8,8": "iPad Pro 12.9",
|
||||||
|
"iPad8,9": "iPad Pro 11",
|
||||||
|
"iPad8,10": "iPad Pro 11",
|
||||||
|
"iPad8,11": "iPad Pro 12.9",
|
||||||
|
"iPad8,12": "iPad Pro 12.9",
|
||||||
|
"iPad13,4": "iPad Pro 11",
|
||||||
|
"iPad13,5": "iPad Pro 11",
|
||||||
|
"iPad13,6": "iPad Pro 11",
|
||||||
|
"iPad13,7": "iPad Pro 11",
|
||||||
|
"iPad13,8": "iPad Pro 12.9",
|
||||||
|
"iPad13,9": "iPad Pro 12.9",
|
||||||
|
"iPad13,10": "iPad Pro 12.9",
|
||||||
|
"iPad13,11": "iPad Pro 12.9",
|
||||||
|
"iPad14,3": "iPad Pro 11",
|
||||||
|
"iPad14,4": "iPad Pro 11",
|
||||||
|
"iPad14,5": "iPad Pro 12.9",
|
||||||
|
"iPad14,6": "iPad Pro 12.9",
|
||||||
|
"iPad16,3": "iPad Pro M4 11",
|
||||||
|
"iPad16,4": "iPad Pro M4 11",
|
||||||
|
"iPad16,5": "iPad Pro M4 13",
|
||||||
|
"iPad16,6": "iPad Pro M4 13",
|
||||||
|
"iPhone1,1": "iPhone",
|
||||||
|
"iPhone1,2": "iPhone 3G",
|
||||||
|
"iPhone2,1": "iPhone 3GS",
|
||||||
|
"iPhone3,1": "iPhone 4",
|
||||||
|
"iPhone3,2": "iPhone 4",
|
||||||
|
"iPhone3,3": "iPhone 4",
|
||||||
|
"iPhone4,1": "iPhone 4S",
|
||||||
|
"iPhone5,1": "iPhone 5",
|
||||||
|
"iPhone5,2": "iPhone 5",
|
||||||
|
"iPhone5,3": "iPhone 5c",
|
||||||
|
"iPhone5,4": "iPhone 5c",
|
||||||
|
"iPhone6,1": "iPhone 5s",
|
||||||
|
"iPhone6,2": "iPhone 5s",
|
||||||
|
"iPhone7,2": "iPhone 6",
|
||||||
|
"iPhone7,1": "iPhone 6 Plus",
|
||||||
|
"iPhone8,1": "iPhone 6s",
|
||||||
|
"iPhone8,2": "iPhone 6s Plus",
|
||||||
|
"iPhone8,4": "iPhone SE",
|
||||||
|
"iPhone9,1": "iPhone 7",
|
||||||
|
"iPhone9,3": "iPhone 7",
|
||||||
|
"iPhone9,2": "iPhone 7 Plus",
|
||||||
|
"iPhone9,4": "iPhone 7 Plus",
|
||||||
|
"iPhone10,1": "iPhone 8",
|
||||||
|
"iPhone10,4": "iPhone 8",
|
||||||
|
"iPhone10,2": "iPhone 8 Plus",
|
||||||
|
"iPhone10,5": "iPhone 8 Plus",
|
||||||
|
"iPhone10,3": "iPhone X",
|
||||||
|
"iPhone10,6": "iPhone X",
|
||||||
|
"iPhone11,2": "iPhone Xs",
|
||||||
|
"iPhone11,6": "iPhone Xs Max",
|
||||||
|
"iPhone11,8": "iPhone XR",
|
||||||
|
"iPhone12,1": "iPhone 11",
|
||||||
|
"iPhone12,3": "iPhone 11 Pro",
|
||||||
|
"iPhone12,5": "iPhone 11 Pro Max",
|
||||||
|
"iPhone12,8": "iPhone SE",
|
||||||
|
"iPhone13,1": "iPhone 12 mini",
|
||||||
|
"iPhone13,2": "iPhone 12",
|
||||||
|
"iPhone13,3": "iPhone 12 Pro",
|
||||||
|
"iPhone13,4": "iPhone 12 Pro Max",
|
||||||
|
"iPhone14,4": "iPhone 13 mini",
|
||||||
|
"iPhone14,5": "iPhone 13",
|
||||||
|
"iPhone14,2": "iPhone 13 Pro",
|
||||||
|
"iPhone14,3": "iPhone 13 Pro Max",
|
||||||
|
"iPhone14,6": "iPhone SE",
|
||||||
|
"iPhone14,7": "iPhone 14",
|
||||||
|
"iPhone14,8": "iPhone 14 Plus",
|
||||||
|
"iPhone15,2": "iPhone 14 Pro",
|
||||||
|
"iPhone15,3": "iPhone 14 Pro Max",
|
||||||
|
"iPhone15,4": "iPhone 15",
|
||||||
|
"iPhone15,5": "iPhone 15 Plus",
|
||||||
|
"iPhone16,1": "iPhone 15 Pro",
|
||||||
|
"iPhone16,2": "iPhone 15 Pro Max",
|
||||||
|
"iPod1,1": "iPod touch Original",
|
||||||
|
"iPod2,1": "iPod touch 2nd",
|
||||||
|
"iPod3,1": "iPod touch 3rd Gen",
|
||||||
|
"iPod4,1": "iPod touch 4th",
|
||||||
|
"iPod5,1": "iPod touch 5th",
|
||||||
|
"iPod7,1": "iPod touch 6th Gen",
|
||||||
|
"iPod9,1": "iPod touch 7th Gen"
|
||||||
|
}
|
||||||
201
server/db/mac_models.json
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
{
|
||||||
|
"PowerMac4,4": "eMac",
|
||||||
|
"PowerMac6,4": "eMac",
|
||||||
|
"PowerBook2,1": "iBook",
|
||||||
|
"PowerBook2,2": "iBook",
|
||||||
|
"PowerBook4,1": "iBook",
|
||||||
|
"PowerBook4,2": "iBook",
|
||||||
|
"PowerBook4,3": "iBook",
|
||||||
|
"PowerBook6,3": "iBook",
|
||||||
|
"PowerBook6,5": "iBook",
|
||||||
|
"PowerBook6,7": "iBook",
|
||||||
|
"iMac,1": "iMac",
|
||||||
|
"PowerMac2,1": "iMac",
|
||||||
|
"PowerMac2,2": "iMac",
|
||||||
|
"PowerMac4,1": "iMac",
|
||||||
|
"PowerMac4,2": "iMac",
|
||||||
|
"PowerMac4,5": "iMac",
|
||||||
|
"PowerMac6,1": "iMac",
|
||||||
|
"PowerMac6,3*": "iMac",
|
||||||
|
"PowerMac6,3": "iMac",
|
||||||
|
"PowerMac8,1": "iMac",
|
||||||
|
"PowerMac8,2": "iMac",
|
||||||
|
"PowerMac12,1": "iMac",
|
||||||
|
"iMac4,1": "iMac",
|
||||||
|
"iMac4,2": "iMac",
|
||||||
|
"iMac5,2": "iMac",
|
||||||
|
"iMac5,1": "iMac",
|
||||||
|
"iMac6,1": "iMac",
|
||||||
|
"iMac7,1": "iMac",
|
||||||
|
"iMac8,1": "iMac",
|
||||||
|
"iMac9,1": "iMac",
|
||||||
|
"iMac10,1": "iMac",
|
||||||
|
"iMac11,1": "iMac",
|
||||||
|
"iMac11,2": "iMac",
|
||||||
|
"iMac11,3": "iMac",
|
||||||
|
"iMac12,1": "iMac",
|
||||||
|
"iMac12,2": "iMac",
|
||||||
|
"iMac13,1": "iMac",
|
||||||
|
"iMac13,2": "iMac",
|
||||||
|
"iMac14,1": "iMac",
|
||||||
|
"iMac14,3": "iMac",
|
||||||
|
"iMac14,2": "iMac",
|
||||||
|
"iMac14,4": "iMac",
|
||||||
|
"iMac15,1": "iMac",
|
||||||
|
"iMac16,1": "iMac",
|
||||||
|
"iMac16,2": "iMac",
|
||||||
|
"iMac17,1": "iMac",
|
||||||
|
"iMac18,1": "iMac",
|
||||||
|
"iMac18,2": "iMac",
|
||||||
|
"iMac18,3": "iMac",
|
||||||
|
"iMac19,2": "iMac",
|
||||||
|
"iMac19,1": "iMac",
|
||||||
|
"iMac20,1": "iMac",
|
||||||
|
"iMac20,2": "iMac",
|
||||||
|
"iMac21,2": "iMac",
|
||||||
|
"iMac21,1": "iMac",
|
||||||
|
"iMacPro1,1": "iMac Pro",
|
||||||
|
"PowerMac10,1": "Mac mini",
|
||||||
|
"PowerMac10,2": "Mac mini",
|
||||||
|
"Macmini1,1": "Mac mini",
|
||||||
|
"Macmini2,1": "Mac mini",
|
||||||
|
"Macmini3,1": "Mac mini",
|
||||||
|
"Macmini4,1": "Mac mini",
|
||||||
|
"Macmini5,1": "Mac mini",
|
||||||
|
"Macmini5,2": "Mac mini",
|
||||||
|
"Macmini5,3": "Mac mini",
|
||||||
|
"Macmini6,1": "Mac mini",
|
||||||
|
"Macmini6,2": "Mac mini",
|
||||||
|
"Macmini7,1": "Mac mini",
|
||||||
|
"Macmini8,1": "Mac mini",
|
||||||
|
"ADP3,2": "Mac mini",
|
||||||
|
"Macmini9,1": "Mac mini",
|
||||||
|
"Mac14,3": "Mac mini",
|
||||||
|
"Mac14,12": "Mac mini",
|
||||||
|
"MacPro1,1*": "Mac Pro",
|
||||||
|
"MacPro2,1": "Mac Pro",
|
||||||
|
"MacPro3,1": "Mac Pro",
|
||||||
|
"MacPro4,1": "Mac Pro",
|
||||||
|
"MacPro5,1": "Mac Pro",
|
||||||
|
"MacPro6,1": "Mac Pro",
|
||||||
|
"MacPro7,1": "Mac Pro",
|
||||||
|
"N/A*": "Power Macintosh",
|
||||||
|
"PowerMac1,1": "Power Macintosh",
|
||||||
|
"PowerMac3,1": "Power Macintosh",
|
||||||
|
"PowerMac3,3": "Power Macintosh",
|
||||||
|
"PowerMac3,4": "Power Macintosh",
|
||||||
|
"PowerMac3,5": "Power Macintosh",
|
||||||
|
"PowerMac3,6": "Power Macintosh",
|
||||||
|
"Mac13,1": "Mac Studio",
|
||||||
|
"Mac13,2": "Mac Studio",
|
||||||
|
"MacBook1,1": "MacBook",
|
||||||
|
"MacBook2,1": "MacBook",
|
||||||
|
"MacBook3,1": "MacBook",
|
||||||
|
"MacBook4,1": "MacBook",
|
||||||
|
"MacBook5,1": "MacBook",
|
||||||
|
"MacBook5,2": "MacBook",
|
||||||
|
"MacBook6,1": "MacBook",
|
||||||
|
"MacBook7,1": "MacBook",
|
||||||
|
"MacBook8,1": "MacBook",
|
||||||
|
"MacBook9,1": "MacBook",
|
||||||
|
"MacBook10,1": "MacBook",
|
||||||
|
"MacBookAir1,1": "MacBook Air",
|
||||||
|
"MacBookAir2,1": "MacBook Air",
|
||||||
|
"MacBookAir3,1": "MacBook Air",
|
||||||
|
"MacBookAir3,2": "MacBook Air",
|
||||||
|
"MacBookAir4,1": "MacBook Air",
|
||||||
|
"MacBookAir4,2": "MacBook Air",
|
||||||
|
"MacBookAir5,1": "MacBook Air",
|
||||||
|
"MacBookAir5,2": "MacBook Air",
|
||||||
|
"MacBookAir6,1": "MacBook Air",
|
||||||
|
"MacBookAir6,2": "MacBook Air",
|
||||||
|
"MacBookAir7,1": "MacBook Air",
|
||||||
|
"MacBookAir7,2": "MacBook Air",
|
||||||
|
"MacBookAir8,1": "MacBook Air",
|
||||||
|
"MacBookAir8,2": "MacBook Air",
|
||||||
|
"MacBookAir9,1": "MacBook Air",
|
||||||
|
"MacBookAir10,1": "MacBook Air",
|
||||||
|
"Mac14,2": "MacBook Air",
|
||||||
|
"MacBookPro1,1": "MacBook Pro",
|
||||||
|
"MacBookPro1,2": "MacBook Pro",
|
||||||
|
"MacBookPro2,2": "MacBook Pro",
|
||||||
|
"MacBookPro2,1": "MacBook Pro",
|
||||||
|
"MacBookPro3,1": "MacBook Pro",
|
||||||
|
"MacBookPro4,1": "MacBook Pro",
|
||||||
|
"MacBookPro5,1": "MacBook Pro",
|
||||||
|
"MacBookPro5,2": "MacBook Pro",
|
||||||
|
"MacBookPro5,5": "MacBook Pro",
|
||||||
|
"MacBookPro5,4": "MacBook Pro",
|
||||||
|
"MacBookPro5,3": "MacBook Pro",
|
||||||
|
"MacBookPro7,1": "MacBook Pro",
|
||||||
|
"MacBookPro6,2": "MacBook Pro",
|
||||||
|
"MacBookPro6,1": "MacBook Pro",
|
||||||
|
"MacBookPro8,1": "MacBook Pro",
|
||||||
|
"MacBookPro8,2": "MacBook Pro",
|
||||||
|
"MacBookPro8,3": "MacBook Pro",
|
||||||
|
"MacBookPro9,2": "MacBook Pro",
|
||||||
|
"MacBookPro9,1": "MacBook Pro",
|
||||||
|
"MacBookPro10,1": "MacBook Pro",
|
||||||
|
"MacBookPro10,2": "MacBook Pro",
|
||||||
|
"MacBookPro11,1": "MacBook Pro",
|
||||||
|
"MacBookPro11,2": "MacBook Pro",
|
||||||
|
"MacBookPro11,3": "MacBook Pro",
|
||||||
|
"MacBookPro12,1": "MacBook Pro",
|
||||||
|
"MacBookPro11,4": "MacBook Pro",
|
||||||
|
"MacBookPro11,5": "MacBook Pro",
|
||||||
|
"MacBookPro13,1": "MacBook Pro",
|
||||||
|
"MacBookPro13,2": "MacBook Pro",
|
||||||
|
"MacBookPro13,3": "MacBook Pro",
|
||||||
|
"MacBookPro14,1": "MacBook Pro",
|
||||||
|
"MacBookPro14,2": "MacBook Pro",
|
||||||
|
"MacBookPro14,3": "MacBook Pro",
|
||||||
|
"MacBookPro15,2": "MacBook Pro",
|
||||||
|
"MacBookPro15,1": "MacBook Pro",
|
||||||
|
"MacBookPro15,3": "MacBook Pro",
|
||||||
|
"MacBookPro15,4": "MacBook Pro",
|
||||||
|
"MacBookPro16,1": "MacBook Pro",
|
||||||
|
"MacBookPro16,3": "MacBook Pro",
|
||||||
|
"MacBookPro16,2": "MacBook Pro",
|
||||||
|
"MacBookPro16,4": "MacBook Pro",
|
||||||
|
"MacBookPro17,1": "MacBook Pro",
|
||||||
|
"MacBookPro18,3": "MacBook Pro",
|
||||||
|
"MacBookPro18,4": "MacBook Pro",
|
||||||
|
"MacBookPro18,1": "MacBook Pro",
|
||||||
|
"MacBookPro18,2": "MacBook Pro",
|
||||||
|
"Mac14,7": "MacBook Pro",
|
||||||
|
"Mac14,9": "MacBook Pro",
|
||||||
|
"Mac14,5": "MacBook Pro",
|
||||||
|
"Mac14,10": "MacBook Pro",
|
||||||
|
"Mac14,6": "MacBook Pro",
|
||||||
|
"PowerMac1,2": "Power Macintosh",
|
||||||
|
"PowerMac5,1": "Power Macintosh",
|
||||||
|
"PowerMac7,2": "Power Macintosh",
|
||||||
|
"PowerMac7,3": "Power Macintosh",
|
||||||
|
"PowerMac9,1": "Power Macintosh",
|
||||||
|
"PowerMac11,2": "Power Macintosh",
|
||||||
|
"PowerBook1,1": "PowerBook",
|
||||||
|
"PowerBook3,1": "PowerBook",
|
||||||
|
"PowerBook3,2": "PowerBook",
|
||||||
|
"PowerBook3,3": "PowerBook",
|
||||||
|
"PowerBook3,4": "PowerBook",
|
||||||
|
"PowerBook3,5": "PowerBook",
|
||||||
|
"PowerBook6,1": "PowerBook",
|
||||||
|
"PowerBook5,1": "PowerBook",
|
||||||
|
"PowerBook6,2": "PowerBook",
|
||||||
|
"PowerBook5,2": "PowerBook",
|
||||||
|
"PowerBook5,3": "PowerBook",
|
||||||
|
"PowerBook6,4": "PowerBook",
|
||||||
|
"PowerBook5,4": "PowerBook",
|
||||||
|
"PowerBook5,5": "PowerBook",
|
||||||
|
"PowerBook6,8": "PowerBook",
|
||||||
|
"PowerBook5,6": "PowerBook",
|
||||||
|
"PowerBook5,7": "PowerBook",
|
||||||
|
"PowerBook5,8": "PowerBook",
|
||||||
|
"PowerBook5,9": "PowerBook",
|
||||||
|
"RackMac1,1": "Xserve",
|
||||||
|
"RackMac1,2": "Xserve",
|
||||||
|
"RackMac3,1": "Xserve",
|
||||||
|
"Xserve1,1": "Xserve",
|
||||||
|
"Xserve2,1": "Xserve",
|
||||||
|
"Xserve3,1": "Xserve"
|
||||||
|
}
|
||||||
13
server/db/maxmindAsn.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import maxmind, { AsnResponse, Reader } from "maxmind";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
|
let maxmindAsnLookup: Reader<AsnResponse> | null;
|
||||||
|
if (config.getRawConfig().server.maxmind_asn_path) {
|
||||||
|
maxmindAsnLookup = await maxmind.open<AsnResponse>(
|
||||||
|
config.getRawConfig().server.maxmind_asn_path!
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
maxmindAsnLookup = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { maxmindAsnLookup };
|
||||||
3
server/db/migrate.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { runMigrations } from "./";
|
||||||
|
|
||||||
|
await runMigrations();
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { db, resources, siteResources } from "@server/db";
|
import { clients, db, resources, siteResources } from "@server/db";
|
||||||
|
import { randomInt } from "crypto";
|
||||||
import { exitNodes, sites } from "@server/db";
|
import { exitNodes, sites } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { __DIRNAME } from "@server/lib/consts";
|
import { __DIRNAME } from "@server/lib/consts";
|
||||||
@@ -15,6 +16,43 @@ if (!dev) {
|
|||||||
}
|
}
|
||||||
export const names = JSON.parse(readFileSync(file, "utf-8"));
|
export const names = JSON.parse(readFileSync(file, "utf-8"));
|
||||||
|
|
||||||
|
// Load iOS and Mac model mappings
|
||||||
|
let iosModelsFile: string;
|
||||||
|
let macModelsFile: string;
|
||||||
|
if (!dev) {
|
||||||
|
iosModelsFile = join(__DIRNAME, "ios_models.json");
|
||||||
|
macModelsFile = join(__DIRNAME, "mac_models.json");
|
||||||
|
} else {
|
||||||
|
iosModelsFile = join("server/db/ios_models.json");
|
||||||
|
macModelsFile = join("server/db/mac_models.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
const iosModels: Record<string, string> = JSON.parse(
|
||||||
|
readFileSync(iosModelsFile, "utf-8")
|
||||||
|
);
|
||||||
|
const macModels: Record<string, string> = JSON.parse(
|
||||||
|
readFileSync(macModelsFile, "utf-8")
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function getUniqueClientName(orgId: string): Promise<string> {
|
||||||
|
let loops = 0;
|
||||||
|
while (true) {
|
||||||
|
if (loops > 100) {
|
||||||
|
throw new Error("Could not generate a unique name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = generateName();
|
||||||
|
const count = await db
|
||||||
|
.select({ niceId: clients.niceId, orgId: clients.orgId })
|
||||||
|
.from(clients)
|
||||||
|
.where(and(eq(clients.niceId, name), eq(clients.orgId, orgId)));
|
||||||
|
if (count.length === 0) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
loops++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function getUniqueSiteName(orgId: string): Promise<string> {
|
export async function getUniqueSiteName(orgId: string): Promise<string> {
|
||||||
let loops = 0;
|
let loops = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -46,11 +84,21 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
|
|||||||
db
|
db
|
||||||
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))),
|
.where(
|
||||||
|
and(eq(resources.niceId, name), eq(resources.orgId, orgId))
|
||||||
|
),
|
||||||
db
|
db
|
||||||
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
|
.select({
|
||||||
|
niceId: siteResources.niceId,
|
||||||
|
orgId: siteResources.orgId
|
||||||
|
})
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)))
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.niceId, name),
|
||||||
|
eq(siteResources.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
]);
|
]);
|
||||||
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
|
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
|
||||||
return name;
|
return name;
|
||||||
@@ -59,7 +107,9 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUniqueSiteResourceName(orgId: string): Promise<string> {
|
export async function getUniqueSiteResourceName(
|
||||||
|
orgId: string
|
||||||
|
): Promise<string> {
|
||||||
let loops = 0;
|
let loops = 0;
|
||||||
while (true) {
|
while (true) {
|
||||||
if (loops > 100) {
|
if (loops > 100) {
|
||||||
@@ -71,11 +121,21 @@ export async function getUniqueSiteResourceName(orgId: string): Promise<string>
|
|||||||
db
|
db
|
||||||
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
.select({ niceId: resources.niceId, orgId: resources.orgId })
|
||||||
.from(resources)
|
.from(resources)
|
||||||
.where(and(eq(resources.niceId, name), eq(resources.orgId, orgId))),
|
.where(
|
||||||
|
and(eq(resources.niceId, name), eq(resources.orgId, orgId))
|
||||||
|
),
|
||||||
db
|
db
|
||||||
.select({ niceId: siteResources.niceId, orgId: siteResources.orgId })
|
.select({
|
||||||
|
niceId: siteResources.niceId,
|
||||||
|
orgId: siteResources.orgId
|
||||||
|
})
|
||||||
.from(siteResources)
|
.from(siteResources)
|
||||||
.where(and(eq(siteResources.niceId, name), eq(siteResources.orgId, orgId)))
|
.where(
|
||||||
|
and(
|
||||||
|
eq(siteResources.niceId, name),
|
||||||
|
eq(siteResources.orgId, orgId)
|
||||||
|
)
|
||||||
|
)
|
||||||
]);
|
]);
|
||||||
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
|
if (resourceCount.length === 0 && siteResourceCount.length === 0) {
|
||||||
return name;
|
return name;
|
||||||
@@ -86,9 +146,7 @@ export async function getUniqueSiteResourceName(orgId: string): Promise<string>
|
|||||||
|
|
||||||
export async function getUniqueExitNodeEndpointName(): Promise<string> {
|
export async function getUniqueExitNodeEndpointName(): Promise<string> {
|
||||||
let loops = 0;
|
let loops = 0;
|
||||||
const count = await db
|
const count = await db.select().from(exitNodes);
|
||||||
.select()
|
|
||||||
.from(exitNodes);
|
|
||||||
while (true) {
|
while (true) {
|
||||||
if (loops > 100) {
|
if (loops > 100) {
|
||||||
throw new Error("Could not generate a unique name");
|
throw new Error("Could not generate a unique name");
|
||||||
@@ -107,14 +165,11 @@ export async function getUniqueExitNodeEndpointName(): Promise<string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function generateName(): string {
|
export function generateName(): string {
|
||||||
const name = (
|
const name = (
|
||||||
names.descriptors[
|
names.descriptors[randomInt(names.descriptors.length)] +
|
||||||
Math.floor(Math.random() * names.descriptors.length)
|
|
||||||
] +
|
|
||||||
"-" +
|
"-" +
|
||||||
names.animals[Math.floor(Math.random() * names.animals.length)]
|
names.animals[randomInt(names.animals.length)]
|
||||||
)
|
)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/\s/g, "-");
|
.replace(/\s/g, "-");
|
||||||
@@ -122,3 +177,29 @@ export function generateName(): string {
|
|||||||
// clean out any non-alphanumeric characters except for dashes
|
// clean out any non-alphanumeric characters except for dashes
|
||||||
return name.replace(/[^a-z0-9-]/g, "");
|
return name.replace(/[^a-z0-9-]/g, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMacDeviceName(macIdentifier?: string | null): string | null {
|
||||||
|
if (macIdentifier && macModels[macIdentifier]) {
|
||||||
|
return macModels[macIdentifier];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIosDeviceName(iosIdentifier?: string | null): string | null {
|
||||||
|
if (iosIdentifier && iosModels[iosIdentifier]) {
|
||||||
|
return iosModels[iosIdentifier];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserDeviceName(
|
||||||
|
model: string | null,
|
||||||
|
fallBack: string | null
|
||||||
|
): string {
|
||||||
|
return (
|
||||||
|
getMacDeviceName(model) ||
|
||||||
|
getIosDeviceName(model) ||
|
||||||
|
fallBack ||
|
||||||
|
"Unknown Device"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,27 +6,27 @@ import { withReplicas } from "drizzle-orm/pg-core";
|
|||||||
function createDb() {
|
function createDb() {
|
||||||
const config = readConfigFile();
|
const config = readConfigFile();
|
||||||
|
|
||||||
if (!config.postgres) {
|
// check the environment variables for postgres config first before the config file
|
||||||
// check the environment variables for postgres config
|
|
||||||
if (process.env.POSTGRES_CONNECTION_STRING) {
|
if (process.env.POSTGRES_CONNECTION_STRING) {
|
||||||
config.postgres = {
|
config.postgres = {
|
||||||
connection_string: process.env.POSTGRES_CONNECTION_STRING
|
connection_string: process.env.POSTGRES_CONNECTION_STRING
|
||||||
};
|
};
|
||||||
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
|
if (process.env.POSTGRES_REPLICA_CONNECTION_STRINGS) {
|
||||||
const replicas =
|
const replicas =
|
||||||
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(
|
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(",").map(
|
||||||
","
|
(conn) => ({
|
||||||
).map((conn) => ({
|
|
||||||
connection_string: conn.trim()
|
connection_string: conn.trim()
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
config.postgres.replicas = replicas;
|
config.postgres.replicas = replicas;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
if (!config.postgres) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Postgres configuration is missing in the configuration file."
|
"Postgres configuration is missing in the configuration file."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const connectionString = config.postgres?.connection_string;
|
const connectionString = config.postgres?.connection_string;
|
||||||
const replicaConnections = config.postgres?.replicas || [];
|
const replicaConnections = config.postgres?.replicas || [];
|
||||||
@@ -51,7 +51,7 @@ function createDb() {
|
|||||||
if (!replicaConnections.length) {
|
if (!replicaConnections.length) {
|
||||||
replicas.push(
|
replicas.push(
|
||||||
DrizzlePostgres(primaryPool, {
|
DrizzlePostgres(primaryPool, {
|
||||||
logger: process.env.NODE_ENV === "development"
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -65,7 +65,7 @@ function createDb() {
|
|||||||
});
|
});
|
||||||
replicas.push(
|
replicas.push(
|
||||||
DrizzlePostgres(replicaPool, {
|
DrizzlePostgres(replicaPool, {
|
||||||
logger: process.env.NODE_ENV === "development"
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -73,7 +73,7 @@ function createDb() {
|
|||||||
|
|
||||||
return withReplicas(
|
return withReplicas(
|
||||||
DrizzlePostgres(primaryPool, {
|
DrizzlePostgres(primaryPool, {
|
||||||
logger: process.env.QUERY_LOGGING === "true"
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
}),
|
}),
|
||||||
replicas as any
|
replicas as any
|
||||||
);
|
);
|
||||||
@@ -81,6 +81,7 @@ function createDb() {
|
|||||||
|
|
||||||
export const db = createDb();
|
export const db = createDb();
|
||||||
export default db;
|
export default db;
|
||||||
|
export const primaryDb = db.$primary;
|
||||||
export type Transaction = Parameters<
|
export type Transaction = Parameters<
|
||||||
Parameters<(typeof db)["transaction"]>[0]
|
Parameters<(typeof db)["transaction"]>[0]
|
||||||
>[0];
|
>[0];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./driver";
|
export * from "./driver";
|
||||||
export * from "./schema/schema";
|
export * from "./schema/schema";
|
||||||
export * from "./schema/privateSchema";
|
export * from "./schema/privateSchema";
|
||||||
|
export * from "./migrate";
|
||||||
|
|||||||
@@ -4,18 +4,16 @@ import path from "path";
|
|||||||
|
|
||||||
const migrationsFolder = path.join("server/migrations");
|
const migrationsFolder = path.join("server/migrations");
|
||||||
|
|
||||||
const runMigrations = async () => {
|
export const runMigrations = async () => {
|
||||||
console.log("Running migrations...");
|
console.log("Running migrations...");
|
||||||
try {
|
try {
|
||||||
await migrate(db as any, {
|
await migrate(db as any, {
|
||||||
migrationsFolder: migrationsFolder
|
migrationsFolder: migrationsFolder
|
||||||
});
|
});
|
||||||
console.log("Migrations completed successfully.");
|
console.log("Migrations completed successfully. ✅");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error running migrations:", error);
|
console.error("Error running migrations:", error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
runMigrations();
|
|
||||||
|
|||||||
@@ -10,7 +10,15 @@ import {
|
|||||||
index
|
index
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
import { domains, orgs, targets, users, exitNodes, sessions } from "./schema";
|
import {
|
||||||
|
domains,
|
||||||
|
orgs,
|
||||||
|
targets,
|
||||||
|
users,
|
||||||
|
exitNodes,
|
||||||
|
sessions,
|
||||||
|
clients
|
||||||
|
} from "./schema";
|
||||||
|
|
||||||
export const certificates = pgTable("certificates", {
|
export const certificates = pgTable("certificates", {
|
||||||
certId: serial("certId").primaryKey(),
|
certId: serial("certId").primaryKey(),
|
||||||
@@ -74,11 +82,14 @@ export const subscriptions = pgTable("subscriptions", {
|
|||||||
canceledAt: bigint("canceledAt", { mode: "number" }),
|
canceledAt: bigint("canceledAt", { mode: "number" }),
|
||||||
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
|
||||||
updatedAt: bigint("updatedAt", { mode: "number" }),
|
updatedAt: bigint("updatedAt", { mode: "number" }),
|
||||||
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" })
|
version: integer("version"),
|
||||||
|
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" }),
|
||||||
|
type: varchar("type", { length: 50 }) // tier1, tier2, tier3, or license
|
||||||
});
|
});
|
||||||
|
|
||||||
export const subscriptionItems = pgTable("subscriptionItems", {
|
export const subscriptionItems = pgTable("subscriptionItems", {
|
||||||
subscriptionItemId: serial("subscriptionItemId").primaryKey(),
|
subscriptionItemId: serial("subscriptionItemId").primaryKey(),
|
||||||
|
stripeSubscriptionItemId: varchar("stripeSubscriptionItemId", { length: 255 }),
|
||||||
subscriptionId: varchar("subscriptionId", { length: 255 })
|
subscriptionId: varchar("subscriptionId", { length: 255 })
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => subscriptions.subscriptionId, {
|
.references(() => subscriptions.subscriptionId, {
|
||||||
@@ -86,6 +97,7 @@ export const subscriptionItems = pgTable("subscriptionItems", {
|
|||||||
}),
|
}),
|
||||||
planId: varchar("planId", { length: 255 }).notNull(),
|
planId: varchar("planId", { length: 255 }).notNull(),
|
||||||
priceId: varchar("priceId", { length: 255 }),
|
priceId: varchar("priceId", { length: 255 }),
|
||||||
|
featureId: varchar("featureId", { length: 255 }),
|
||||||
meterId: varchar("meterId", { length: 255 }),
|
meterId: varchar("meterId", { length: 255 }),
|
||||||
unitAmount: real("unitAmount"),
|
unitAmount: real("unitAmount"),
|
||||||
tiers: text("tiers"),
|
tiers: text("tiers"),
|
||||||
@@ -128,6 +140,7 @@ export const limits = pgTable("limits", {
|
|||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
value: real("value"),
|
value: real("value"),
|
||||||
|
override: boolean("override").default(false),
|
||||||
description: text("description")
|
description: text("description")
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -204,6 +217,29 @@ export const loginPageOrg = pgTable("loginPageOrg", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" })
|
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const loginPageBranding = pgTable("loginPageBranding", {
|
||||||
|
loginPageBrandingId: serial("loginPageBrandingId").primaryKey(),
|
||||||
|
logoUrl: text("logoUrl"),
|
||||||
|
logoWidth: integer("logoWidth").notNull(),
|
||||||
|
logoHeight: integer("logoHeight").notNull(),
|
||||||
|
primaryColor: text("primaryColor"),
|
||||||
|
resourceTitle: text("resourceTitle").notNull(),
|
||||||
|
resourceSubtitle: text("resourceSubtitle"),
|
||||||
|
orgTitle: text("orgTitle"),
|
||||||
|
orgSubtitle: text("orgSubtitle")
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loginPageBrandingOrg = pgTable("loginPageBrandingOrg", {
|
||||||
|
loginPageBrandingId: integer("loginPageBrandingId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => loginPageBranding.loginPageBrandingId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => orgs.orgId, { onDelete: "cascade" })
|
||||||
|
});
|
||||||
|
|
||||||
export const sessionTransferToken = pgTable("sessionTransferToken", {
|
export const sessionTransferToken = pgTable("sessionTransferToken", {
|
||||||
token: varchar("token").primaryKey(),
|
token: varchar("token").primaryKey(),
|
||||||
sessionId: varchar("sessionId")
|
sessionId: varchar("sessionId")
|
||||||
@@ -215,7 +251,9 @@ export const sessionTransferToken = pgTable("sessionTransferToken", {
|
|||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const actionAuditLog = pgTable("actionAuditLog", {
|
export const actionAuditLog = pgTable(
|
||||||
|
"actionAuditLog",
|
||||||
|
{
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
@@ -226,12 +264,19 @@ export const actionAuditLog = pgTable("actionAuditLog", {
|
|||||||
actorId: varchar("actorId", { length: 255 }).notNull(),
|
actorId: varchar("actorId", { length: 255 }).notNull(),
|
||||||
action: varchar("action", { length: 100 }).notNull(),
|
action: varchar("action", { length: 100 }).notNull(),
|
||||||
metadata: text("metadata")
|
metadata: text("metadata")
|
||||||
}, (table) => ([
|
},
|
||||||
|
(table) => [
|
||||||
index("idx_actionAuditLog_timestamp").on(table.timestamp),
|
index("idx_actionAuditLog_timestamp").on(table.timestamp),
|
||||||
index("idx_actionAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
index("idx_actionAuditLog_org_timestamp").on(
|
||||||
]));
|
table.orgId,
|
||||||
|
table.timestamp
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
export const accessAuditLog = pgTable("accessAuditLog", {
|
export const accessAuditLog = pgTable(
|
||||||
|
"accessAuditLog",
|
||||||
|
{
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
timestamp: bigint("timestamp", { mode: "number" }).notNull(), // this is EPOCH time in seconds
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
@@ -247,11 +292,43 @@ export const accessAuditLog = pgTable("accessAuditLog", {
|
|||||||
location: text("location"),
|
location: text("location"),
|
||||||
userAgent: text("userAgent"),
|
userAgent: text("userAgent"),
|
||||||
metadata: text("metadata")
|
metadata: text("metadata")
|
||||||
}, (table) => ([
|
},
|
||||||
|
(table) => [
|
||||||
index("idx_identityAuditLog_timestamp").on(table.timestamp),
|
index("idx_identityAuditLog_timestamp").on(table.timestamp),
|
||||||
index("idx_identityAuditLog_org_timestamp").on(table.orgId, table.timestamp)
|
index("idx_identityAuditLog_org_timestamp").on(
|
||||||
]));
|
table.orgId,
|
||||||
|
table.timestamp
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const approvals = pgTable("approvals", {
|
||||||
|
approvalId: serial("approvalId").primaryKey(),
|
||||||
|
timestamp: integer("timestamp").notNull(), // this is EPOCH time in seconds
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}), // clients reference user devices (in this case)
|
||||||
|
userId: varchar("userId")
|
||||||
|
.references(() => users.userId, {
|
||||||
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
|
decision: varchar("decision")
|
||||||
|
.$type<"approved" | "denied" | "pending">()
|
||||||
|
.default("pending")
|
||||||
|
.notNull(),
|
||||||
|
type: varchar("type")
|
||||||
|
.$type<"user_device" /*| 'proxy' // for later */>()
|
||||||
|
.notNull()
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Approval = InferSelectModel<typeof approvals>;
|
||||||
export type Limit = InferSelectModel<typeof limits>;
|
export type Limit = InferSelectModel<typeof limits>;
|
||||||
export type Account = InferSelectModel<typeof account>;
|
export type Account = InferSelectModel<typeof account>;
|
||||||
export type Certificate = InferSelectModel<typeof certificates>;
|
export type Certificate = InferSelectModel<typeof certificates>;
|
||||||
@@ -269,5 +346,6 @@ export type RemoteExitNodeSession = InferSelectModel<
|
|||||||
>;
|
>;
|
||||||
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
export type ExitNodeOrg = InferSelectModel<typeof exitNodeOrgs>;
|
||||||
export type LoginPage = InferSelectModel<typeof loginPage>;
|
export type LoginPage = InferSelectModel<typeof loginPage>;
|
||||||
|
export type LoginPageBranding = InferSelectModel<typeof loginPageBranding>;
|
||||||
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
export type ActionAuditLog = InferSelectModel<typeof actionAuditLog>;
|
||||||
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
export type AccessAuditLog = InferSelectModel<typeof accessAuditLog>;
|
||||||
@@ -1,17 +1,16 @@
|
|||||||
import {
|
|
||||||
pgTable,
|
|
||||||
serial,
|
|
||||||
varchar,
|
|
||||||
boolean,
|
|
||||||
integer,
|
|
||||||
bigint,
|
|
||||||
real,
|
|
||||||
text,
|
|
||||||
index
|
|
||||||
} from "drizzle-orm/pg-core";
|
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
|
||||||
import { randomUUID } from "crypto";
|
import { randomUUID } from "crypto";
|
||||||
import { alias } from "yargs";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
bigint,
|
||||||
|
boolean,
|
||||||
|
index,
|
||||||
|
integer,
|
||||||
|
pgTable,
|
||||||
|
real,
|
||||||
|
serial,
|
||||||
|
text,
|
||||||
|
varchar
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const domains = pgTable("domains", {
|
export const domains = pgTable("domains", {
|
||||||
domainId: varchar("domainId").primaryKey(),
|
domainId: varchar("domainId").primaryKey(),
|
||||||
@@ -46,15 +45,19 @@ export const orgs = pgTable("orgs", {
|
|||||||
requireTwoFactor: boolean("requireTwoFactor"),
|
requireTwoFactor: boolean("requireTwoFactor"),
|
||||||
maxSessionLengthHours: integer("maxSessionLengthHours"),
|
maxSessionLengthHours: integer("maxSessionLengthHours"),
|
||||||
passwordExpiryDays: integer("passwordExpiryDays"),
|
passwordExpiryDays: integer("passwordExpiryDays"),
|
||||||
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
|
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever, and 9001 = end of the following year
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(7),
|
.default(7),
|
||||||
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
|
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0),
|
.default(0),
|
||||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
|
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(0)
|
.default(0),
|
||||||
|
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||||
|
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||||
|
isBillingOrg: boolean("isBillingOrg"),
|
||||||
|
billingOrgId: varchar("billingOrgId")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgDomains = pgTable("orgDomains", {
|
export const orgDomains = pgTable("orgDomains", {
|
||||||
@@ -131,7 +134,18 @@ export const resources = pgTable("resources", {
|
|||||||
}),
|
}),
|
||||||
headers: text("headers"), // comma-separated list of headers to add to the request
|
headers: text("headers"), // comma-separated list of headers to add to the request
|
||||||
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
|
proxyProtocol: boolean("proxyProtocol").notNull().default(false),
|
||||||
proxyProtocolVersion: integer("proxyProtocolVersion").default(1)
|
proxyProtocolVersion: integer("proxyProtocolVersion").default(1),
|
||||||
|
|
||||||
|
maintenanceModeEnabled: boolean("maintenanceModeEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
maintenanceModeType: text("maintenanceModeType", {
|
||||||
|
enum: ["forced", "automatic"]
|
||||||
|
}).default("forced"), // "forced" = always show, "automatic" = only when down
|
||||||
|
maintenanceTitle: text("maintenanceTitle"),
|
||||||
|
maintenanceMessage: text("maintenanceMessage"),
|
||||||
|
maintenanceEstimatedTime: text("maintenanceEstimatedTime"),
|
||||||
|
postAuthPath: text("postAuthPath")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
@@ -176,8 +190,10 @@ export const targetHealthCheck = pgTable("targetHealthCheck", {
|
|||||||
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
|
hcFollowRedirects: boolean("hcFollowRedirects").default(true),
|
||||||
hcMethod: varchar("hcMethod").default("GET"),
|
hcMethod: varchar("hcMethod").default("GET"),
|
||||||
hcStatus: integer("hcStatus"), // http code
|
hcStatus: integer("hcStatus"), // http code
|
||||||
hcHealth: text("hcHealth").default("unknown"), // "unknown", "healthy", "unhealthy"
|
hcHealth: text("hcHealth")
|
||||||
hcTlsServerName: text("hcTlsServerName"),
|
.$type<"unknown" | "healthy" | "unhealthy">()
|
||||||
|
.default("unknown"), // "unknown", "healthy", "unhealthy"
|
||||||
|
hcTlsServerName: text("hcTlsServerName")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const exitNodes = pgTable("exitNodes", {
|
export const exitNodes = pgTable("exitNodes", {
|
||||||
@@ -206,14 +222,17 @@ export const siteResources = pgTable("siteResources", {
|
|||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
||||||
niceId: varchar("niceId").notNull(),
|
niceId: varchar("niceId").notNull(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
mode: varchar("mode").notNull(), // "host" | "cidr" | "port"
|
mode: varchar("mode").$type<"host" | "cidr">().notNull(), // "host" | "cidr" | "port"
|
||||||
protocol: varchar("protocol"), // only for port mode
|
protocol: varchar("protocol"), // only for port mode
|
||||||
proxyPort: integer("proxyPort"), // only for port mode
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationPort: integer("destinationPort"), // only for port mode
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
|
destination: varchar("destination").notNull(), // ip, cidr, hostname; validate against the mode
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
alias: varchar("alias"),
|
alias: varchar("alias"),
|
||||||
aliasAddress: varchar("aliasAddress")
|
aliasAddress: varchar("aliasAddress"),
|
||||||
|
tcpPortRangeString: varchar("tcpPortRangeString").notNull().default("*"),
|
||||||
|
udpPortRangeString: varchar("udpPortRangeString").notNull().default("*"),
|
||||||
|
disableIcmp: boolean("disableIcmp").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSiteResources = pgTable("clientSiteResources", {
|
export const clientSiteResources = pgTable("clientSiteResources", {
|
||||||
@@ -313,7 +332,8 @@ export const userOrgs = pgTable("userOrgs", {
|
|||||||
.notNull()
|
.notNull()
|
||||||
.references(() => roles.roleId),
|
.references(() => roles.roleId),
|
||||||
isOwner: boolean("isOwner").notNull().default(false),
|
isOwner: boolean("isOwner").notNull().default(false),
|
||||||
autoProvisioned: boolean("autoProvisioned").default(false)
|
autoProvisioned: boolean("autoProvisioned").default(false),
|
||||||
|
pamUsername: varchar("pamUsername") // cleaned username for ssh and such
|
||||||
});
|
});
|
||||||
|
|
||||||
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
|
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
|
||||||
@@ -351,7 +371,8 @@ export const roles = pgTable("roles", {
|
|||||||
.notNull(),
|
.notNull(),
|
||||||
isAdmin: boolean("isAdmin"),
|
isAdmin: boolean("isAdmin"),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
description: varchar("description")
|
description: varchar("description"),
|
||||||
|
requireDeviceApproval: boolean("requireDeviceApproval").default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const roleActions = pgTable("roleActions", {
|
export const roleActions = pgTable("roleActions", {
|
||||||
@@ -452,6 +473,23 @@ export const resourceHeaderAuth = pgTable("resourceHeaderAuth", {
|
|||||||
headerAuthHash: varchar("headerAuthHash").notNull()
|
headerAuthHash: varchar("headerAuthHash").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const resourceHeaderAuthExtendedCompatibility = pgTable(
|
||||||
|
"resourceHeaderAuthExtendedCompatibility",
|
||||||
|
{
|
||||||
|
headerAuthExtendedCompatibilityId: serial(
|
||||||
|
"headerAuthExtendedCompatibilityId"
|
||||||
|
).primaryKey(),
|
||||||
|
resourceId: integer("resourceId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
|
extendedCompatibilityIsActivated: boolean(
|
||||||
|
"extendedCompatibilityIsActivated"
|
||||||
|
)
|
||||||
|
.notNull()
|
||||||
|
.default(true)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
export const resourceAccessToken = pgTable("resourceAccessToken", {
|
||||||
accessTokenId: varchar("accessTokenId").primaryKey(),
|
accessTokenId: varchar("accessTokenId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
@@ -560,7 +598,8 @@ export const idp = pgTable("idp", {
|
|||||||
type: varchar("type").notNull(),
|
type: varchar("type").notNull(),
|
||||||
defaultRoleMapping: varchar("defaultRoleMapping"),
|
defaultRoleMapping: varchar("defaultRoleMapping"),
|
||||||
defaultOrgMapping: varchar("defaultOrgMapping"),
|
defaultOrgMapping: varchar("defaultOrgMapping"),
|
||||||
autoProvision: boolean("autoProvision").notNull().default(false)
|
autoProvision: boolean("autoProvision").notNull().default(false),
|
||||||
|
tags: text("tags")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const idpOidcConfig = pgTable("idpOidcConfig", {
|
export const idpOidcConfig = pgTable("idpOidcConfig", {
|
||||||
@@ -644,6 +683,7 @@ export const clients = pgTable("clients", {
|
|||||||
// optionally tied to a user and in this case delete when the user deletes
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
}),
|
}),
|
||||||
|
niceId: varchar("niceId").notNull(),
|
||||||
olmId: text("olmId"), // to lock it to a specific olm optionally
|
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
pubKey: varchar("pubKey"),
|
pubKey: varchar("pubKey"),
|
||||||
@@ -656,7 +696,12 @@ export const clients = pgTable("clients", {
|
|||||||
online: boolean("online").notNull().default(false),
|
online: boolean("online").notNull().default(false),
|
||||||
// endpoint: varchar("endpoint"),
|
// endpoint: varchar("endpoint"),
|
||||||
lastHolePunch: integer("lastHolePunch"),
|
lastHolePunch: integer("lastHolePunch"),
|
||||||
maxConnections: integer("maxConnections")
|
maxConnections: integer("maxConnections"),
|
||||||
|
archived: boolean("archived").notNull().default(false),
|
||||||
|
blocked: boolean("blocked").notNull().default(false),
|
||||||
|
approvalState: varchar("approvalState").$type<
|
||||||
|
"pending" | "approved" | "denied"
|
||||||
|
>()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clientSitesAssociationsCache = pgTable(
|
export const clientSitesAssociationsCache = pgTable(
|
||||||
@@ -680,6 +725,16 @@ export const clientSiteResourcesAssociationsCache = pgTable(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const clientPostureSnapshots = pgTable("clientPostureSnapshots", {
|
||||||
|
snapshotId: serial("snapshotId").primaryKey(),
|
||||||
|
|
||||||
|
clientId: integer("clientId").references(() => clients.clientId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
}),
|
||||||
|
|
||||||
|
collectedAt: integer("collectedAt").notNull()
|
||||||
|
});
|
||||||
|
|
||||||
export const olms = pgTable("olms", {
|
export const olms = pgTable("olms", {
|
||||||
olmId: varchar("id").primaryKey(),
|
olmId: varchar("id").primaryKey(),
|
||||||
secretHash: varchar("secretHash").notNull(),
|
secretHash: varchar("secretHash").notNull(),
|
||||||
@@ -694,7 +749,118 @@ export const olms = pgTable("olms", {
|
|||||||
userId: text("userId").references(() => users.userId, {
|
userId: text("userId").references(() => users.userId, {
|
||||||
// optionally tied to a user and in this case delete when the user deletes
|
// optionally tied to a user and in this case delete when the user deletes
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
}),
|
||||||
|
archived: boolean("archived").notNull().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const currentFingerprint = pgTable("currentFingerprint", {
|
||||||
|
fingerprintId: serial("id").primaryKey(),
|
||||||
|
|
||||||
|
olmId: text("olmId")
|
||||||
|
.references(() => olms.olmId, { onDelete: "cascade" })
|
||||||
|
.notNull(),
|
||||||
|
|
||||||
|
firstSeen: integer("firstSeen").notNull(),
|
||||||
|
lastSeen: integer("lastSeen").notNull(),
|
||||||
|
lastCollectedAt: integer("lastCollectedAt").notNull(),
|
||||||
|
|
||||||
|
username: text("username"),
|
||||||
|
hostname: text("hostname"),
|
||||||
|
platform: text("platform"),
|
||||||
|
osVersion: text("osVersion"),
|
||||||
|
kernelVersion: text("kernelVersion"),
|
||||||
|
arch: text("arch"),
|
||||||
|
deviceModel: text("deviceModel"),
|
||||||
|
serialNumber: text("serialNumber"),
|
||||||
|
platformFingerprint: varchar("platformFingerprint"),
|
||||||
|
|
||||||
|
// Platform-agnostic checks
|
||||||
|
|
||||||
|
biometricsEnabled: boolean("biometricsEnabled").notNull().default(false),
|
||||||
|
diskEncrypted: boolean("diskEncrypted").notNull().default(false),
|
||||||
|
firewallEnabled: boolean("firewallEnabled").notNull().default(false),
|
||||||
|
autoUpdatesEnabled: boolean("autoUpdatesEnabled").notNull().default(false),
|
||||||
|
tpmAvailable: boolean("tpmAvailable").notNull().default(false),
|
||||||
|
|
||||||
|
// Windows-specific posture check information
|
||||||
|
|
||||||
|
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// macOS-specific posture check information
|
||||||
|
|
||||||
|
macosSipEnabled: boolean("macosSipEnabled").notNull().default(false),
|
||||||
|
macosGatekeeperEnabled: boolean("macosGatekeeperEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
macosFirewallStealthMode: boolean("macosFirewallStealthMode")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// Linux-specific posture check information
|
||||||
|
|
||||||
|
linuxAppArmorEnabled: boolean("linuxAppArmorEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
linuxSELinuxEnabled: boolean("linuxSELinuxEnabled").notNull().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const fingerprintSnapshots = pgTable("fingerprintSnapshots", {
|
||||||
|
snapshotId: serial("id").primaryKey(),
|
||||||
|
|
||||||
|
fingerprintId: integer("fingerprintId").references(
|
||||||
|
() => currentFingerprint.fingerprintId,
|
||||||
|
{
|
||||||
|
onDelete: "set null"
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
username: text("username"),
|
||||||
|
hostname: text("hostname"),
|
||||||
|
platform: text("platform"),
|
||||||
|
osVersion: text("osVersion"),
|
||||||
|
kernelVersion: text("kernelVersion"),
|
||||||
|
arch: text("arch"),
|
||||||
|
deviceModel: text("deviceModel"),
|
||||||
|
serialNumber: text("serialNumber"),
|
||||||
|
platformFingerprint: varchar("platformFingerprint"),
|
||||||
|
|
||||||
|
// Platform-agnostic checks
|
||||||
|
|
||||||
|
biometricsEnabled: boolean("biometricsEnabled").notNull().default(false),
|
||||||
|
diskEncrypted: boolean("diskEncrypted").notNull().default(false),
|
||||||
|
firewallEnabled: boolean("firewallEnabled").notNull().default(false),
|
||||||
|
autoUpdatesEnabled: boolean("autoUpdatesEnabled").notNull().default(false),
|
||||||
|
tpmAvailable: boolean("tpmAvailable").notNull().default(false),
|
||||||
|
|
||||||
|
// Windows-specific posture check information
|
||||||
|
|
||||||
|
windowsAntivirusEnabled: boolean("windowsAntivirusEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// macOS-specific posture check information
|
||||||
|
|
||||||
|
macosSipEnabled: boolean("macosSipEnabled").notNull().default(false),
|
||||||
|
macosGatekeeperEnabled: boolean("macosGatekeeperEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
macosFirewallStealthMode: boolean("macosFirewallStealthMode")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
// Linux-specific posture check information
|
||||||
|
|
||||||
|
linuxAppArmorEnabled: boolean("linuxAppArmorEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
linuxSELinuxEnabled: boolean("linuxSELinuxEnabled")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
|
||||||
|
hash: text("hash").notNull(),
|
||||||
|
collectedAt: integer("collectedAt").notNull()
|
||||||
});
|
});
|
||||||
|
|
||||||
export const olmSessions = pgTable("clientSession", {
|
export const olmSessions = pgTable("clientSession", {
|
||||||
@@ -823,6 +989,16 @@ export const deviceWebAuthCodes = pgTable("deviceWebAuthCodes", {
|
|||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
|
||||||
|
messageId: serial("messageId").primaryKey(),
|
||||||
|
wsClientId: varchar("clientId"),
|
||||||
|
messageType: varchar("messageType"),
|
||||||
|
sentAt: bigint("sentAt", { mode: "number" }).notNull(),
|
||||||
|
receivedAt: bigint("receivedAt", { mode: "number" }),
|
||||||
|
error: text("error"),
|
||||||
|
complete: boolean("complete").notNull().default(false)
|
||||||
|
});
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
@@ -851,6 +1027,9 @@ export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
|||||||
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
export type ResourcePincode = InferSelectModel<typeof resourcePincode>;
|
||||||
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
export type ResourcePassword = InferSelectModel<typeof resourcePassword>;
|
||||||
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
export type ResourceHeaderAuth = InferSelectModel<typeof resourceHeaderAuth>;
|
||||||
|
export type ResourceHeaderAuthExtendedCompatibility = InferSelectModel<
|
||||||
|
typeof resourceHeaderAuthExtendedCompatibility
|
||||||
|
>;
|
||||||
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
export type ResourceOtp = InferSelectModel<typeof resourceOtp>;
|
||||||
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
export type ResourceAccessToken = InferSelectModel<typeof resourceAccessToken>;
|
||||||
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
export type ResourceWhitelist = InferSelectModel<typeof resourceWhitelist>;
|
||||||
@@ -880,3 +1059,4 @@ export type SecurityKey = InferSelectModel<typeof securityKeys>;
|
|||||||
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
export type WebauthnChallenge = InferSelectModel<typeof webauthnChallenge>;
|
||||||
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
export type DeviceWebAuthCode = InferSelectModel<typeof deviceWebAuthCodes>;
|
||||||
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
export type RequestAuditLog = InferSelectModel<typeof requestAuditLog>;
|
||||||
|
export type RoundTripMessageTracker = InferSelectModel<typeof roundTripMessageTracker>;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs } from "@server/db";
|
import { db, loginPage, LoginPage, loginPageOrg, Org, orgs, roles } from "@server/db";
|
||||||
import {
|
import {
|
||||||
Resource,
|
Resource,
|
||||||
ResourcePassword,
|
ResourcePassword,
|
||||||
@@ -14,7 +14,9 @@ import {
|
|||||||
sessions,
|
sessions,
|
||||||
userOrgs,
|
userOrgs,
|
||||||
userResources,
|
userResources,
|
||||||
users
|
users,
|
||||||
|
ResourceHeaderAuthExtendedCompatibility,
|
||||||
|
resourceHeaderAuthExtendedCompatibility
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
|
|
||||||
@@ -23,6 +25,7 @@ export type ResourceWithAuth = {
|
|||||||
pincode: ResourcePincode | null;
|
pincode: ResourcePincode | null;
|
||||||
password: ResourcePassword | null;
|
password: ResourcePassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | null;
|
||||||
|
headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null;
|
||||||
org: Org;
|
org: Org;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -52,10 +55,14 @@ export async function getResourceByDomain(
|
|||||||
resourceHeaderAuth,
|
resourceHeaderAuth,
|
||||||
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
eq(resourceHeaderAuth.resourceId, resources.resourceId)
|
||||||
)
|
)
|
||||||
.innerJoin(
|
.leftJoin(
|
||||||
orgs,
|
resourceHeaderAuthExtendedCompatibility,
|
||||||
eq(orgs.orgId, resources.orgId)
|
eq(
|
||||||
|
resourceHeaderAuthExtendedCompatibility.resourceId,
|
||||||
|
resources.resourceId
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
|
||||||
.where(eq(resources.fullDomain, domain))
|
.where(eq(resources.fullDomain, domain))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
@@ -68,6 +75,8 @@ export async function getResourceByDomain(
|
|||||||
pincode: result.resourcePincode,
|
pincode: result.resourcePincode,
|
||||||
password: result.resourcePassword,
|
password: result.resourcePassword,
|
||||||
headerAuth: result.resourceHeaderAuth,
|
headerAuth: result.resourceHeaderAuth,
|
||||||
|
headerAuthExtendedCompatibility:
|
||||||
|
result.resourceHeaderAuthExtendedCompatibility,
|
||||||
org: result.orgs
|
org: result.orgs
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -99,9 +108,17 @@ export async function getUserSessionWithUser(
|
|||||||
*/
|
*/
|
||||||
export async function getUserOrgRole(userId: string, orgId: string) {
|
export async function getUserOrgRole(userId: string, orgId: string) {
|
||||||
const userOrgRole = await db
|
const userOrgRole = await db
|
||||||
.select()
|
.select({
|
||||||
|
userId: userOrgs.userId,
|
||||||
|
orgId: userOrgs.orgId,
|
||||||
|
roleId: userOrgs.roleId,
|
||||||
|
isOwner: userOrgs.isOwner,
|
||||||
|
autoProvisioned: userOrgs.autoProvisioned,
|
||||||
|
roleName: roles.name
|
||||||
|
})
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
.where(and(eq(userOrgs.userId, userId), eq(userOrgs.orgId, orgId)))
|
||||||
|
.leftJoin(roles, eq(userOrgs.roleId, roles.roleId))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
return userOrgRole.length > 0 ? userOrgRole[0] : null;
|
return userOrgRole.length > 0 ? userOrgRole[0] : null;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ function createDb() {
|
|||||||
|
|
||||||
export const db = createDb();
|
export const db = createDb();
|
||||||
export default db;
|
export default db;
|
||||||
|
export const primaryDb = db;
|
||||||
export type Transaction = Parameters<
|
export type Transaction = Parameters<
|
||||||
Parameters<(typeof db)["transaction"]>[0]
|
Parameters<(typeof db)["transaction"]>[0]
|
||||||
>[0];
|
>[0];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./driver";
|
export * from "./driver";
|
||||||
export * from "./schema/schema";
|
export * from "./schema/schema";
|
||||||
export * from "./schema/privateSchema";
|
export * from "./schema/privateSchema";
|
||||||
|
export * from "./migrate";
|
||||||
|
|||||||
@@ -4,11 +4,11 @@ import path from "path";
|
|||||||
|
|
||||||
const migrationsFolder = path.join("server/migrations");
|
const migrationsFolder = path.join("server/migrations");
|
||||||
|
|
||||||
const runMigrations = async () => {
|
export const runMigrations = async () => {
|
||||||
console.log("Running migrations...");
|
console.log("Running migrations...");
|
||||||
try {
|
try {
|
||||||
migrate(db as any, {
|
migrate(db as any, {
|
||||||
migrationsFolder: migrationsFolder,
|
migrationsFolder: migrationsFolder
|
||||||
});
|
});
|
||||||
console.log("Migrations completed successfully.");
|
console.log("Migrations completed successfully.");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -16,5 +16,3 @@ const runMigrations = async () => {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
runMigrations();
|
|
||||||
|
|||||||