Compare commits
1514 Commits
clients-us
...
1.16.0-s.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d63a15715 | ||
|
|
fa2e229ada | ||
|
|
5d9700d84c | ||
|
|
f8a8cdaa5f | ||
|
|
e23e446476 | ||
|
|
fa097df50b | ||
|
|
75f34ff127 | ||
|
|
c9586b4d93 | ||
|
|
52937a6d90 | ||
|
|
186c131cce | ||
|
|
8de3f9a440 | ||
|
|
ea49e179f9 | ||
|
|
485f4f1c8e | ||
|
|
5fb35d12d7 | ||
|
|
ec8a9fe3d2 | ||
|
|
411a34e15e | ||
|
|
3df71fd2bc | ||
|
|
5e1f6085e3 | ||
|
|
53fc7ab6e3 | ||
|
|
7779ed24fe | ||
|
|
6e4193dae3 | ||
|
|
f138609f48 | ||
|
|
98154b5de3 | ||
|
|
6322fd9eef | ||
|
|
1c0949e957 | ||
|
|
c3847e6001 | ||
|
|
5cf13a963d | ||
|
|
b017877826 | ||
|
|
959f68b520 | ||
|
|
14cab3fdb8 | ||
|
|
b8d468f6de | ||
|
|
fc66394243 | ||
|
|
8fca243c9a | ||
|
|
388f710379 | ||
|
|
ba3ab4362b | ||
|
|
e18c9afc2d | ||
|
|
a9b4a86c4a | ||
|
|
200ea502dd | ||
|
|
de36db97eb | ||
|
|
30283b044f | ||
|
|
055bed8a07 | ||
|
|
12b5c2ab34 | ||
|
|
dd78674888 | ||
|
|
0d0df63847 | ||
|
|
3ab00d9da8 | ||
|
|
3e6e72c5c7 | ||
|
|
5d8a55f08c | ||
|
|
81c569aae4 | ||
|
|
88fd3fc4da | ||
|
|
2282d3ae39 | ||
|
|
c4dcec463a | ||
|
|
5b7f893ad7 | ||
|
|
2ede0d498a | ||
|
|
f518e8a0ff | ||
|
|
767284408a | ||
|
|
eef51f3b84 | ||
|
|
69b7114a49 | ||
|
|
0ea38ea568 | ||
|
|
c600da71e3 | ||
|
|
c64dd14b1a | ||
|
|
8ea6d9fa67 | ||
|
|
978ac8f53c | ||
|
|
49a326cde7 | ||
|
|
63e208f4ec | ||
|
|
f50d1549b0 | ||
|
|
55e24df671 | ||
|
|
b37e1d0cc0 | ||
|
|
afa26c0dd4 | ||
|
|
c71f46ede5 | ||
|
|
2edebaddc2 | ||
|
|
119e1d4867 | ||
|
|
63e30d3378 | ||
|
|
d6fe04ec4e | ||
|
|
b8a364af6a | ||
|
|
5ef808d4a2 | ||
|
|
848d4d91e6 | ||
|
|
a502780c9b | ||
|
|
418e099804 | ||
|
|
06258aa386 | ||
|
|
d7608b1cc8 | ||
|
|
cb86ad4104 | ||
|
|
8cd51df1e1 | ||
|
|
8ef7220766 | ||
|
|
b5333a3686 | ||
|
|
e6e92dbc0f | ||
|
|
01fdd41a10 | ||
|
|
6af06a38ae | ||
|
|
5d9c66d22d | ||
|
|
81f5a4b127 | ||
|
|
da3e68a20b | ||
|
|
8712c1719e | ||
|
|
593c5db0e8 | ||
|
|
b28391feae | ||
|
|
5f8df6d4cd | ||
|
|
c36efe7f14 | ||
|
|
cf97b6df9c | ||
|
|
720d3a8135 | ||
|
|
9c42458fa5 | ||
|
|
6d9b129ac9 | ||
|
|
e17ec798d4 | ||
|
|
58ac499f30 | ||
|
|
f07f0092ad | ||
|
|
bcd3475d17 | ||
|
|
7c04526088 | ||
|
|
2d7ab68576 | ||
|
|
218a4893b6 | ||
|
|
266bf261aa | ||
|
|
63694032e8 | ||
|
|
b77aaedb58 | ||
|
|
a316d0301f | ||
|
|
dcd499720e | ||
|
|
e18fe21eca | ||
|
|
2970b51fb8 | ||
|
|
b9236ff52e | ||
|
|
38eb0ec7ed | ||
|
|
ecba4a0b80 | ||
|
|
e6da18c952 | ||
|
|
12941ac5ae | ||
|
|
11085bda63 | ||
|
|
c03211cc53 | ||
|
|
2867459600 | ||
|
|
32b24db9bf | ||
|
|
660bf9ff87 | ||
|
|
78c4ddebba | ||
|
|
f2dfadb37b | ||
|
|
3f2bdf081f | ||
|
|
d6ba34aeea | ||
|
|
b622aca221 | ||
|
|
6442eb12fb | ||
|
|
01c15afa74 | ||
|
|
4e88f1f38a | ||
|
|
13ab505f4d | ||
|
|
7d112aab27 | ||
|
|
b786497299 | ||
|
|
eedf57af89 | ||
|
|
7a01a4e090 | ||
|
|
874794c996 | ||
|
|
5e37c4e85f | ||
|
|
4e7eac368f | ||
|
|
e8398cb221 | ||
|
|
9460e28c7b | ||
|
|
756f3f32ca | ||
|
|
362981ad19 | ||
|
|
fa4f7e4ac2 | ||
|
|
c6bca4e2ab | ||
|
|
e28b361e05 | ||
|
|
a18691011b | ||
|
|
c4a6403cba | ||
|
|
1851bf941a | ||
|
|
b7ab3c2e92 | ||
|
|
ce1ad032ba | ||
|
|
8446c68e1b | ||
|
|
40ed388b0f | ||
|
|
ce1693aa2f | ||
|
|
11d16a1552 | ||
|
|
0ac54a2c88 | ||
|
|
b7d8b32123 | ||
|
|
5987f6b2cd | ||
|
|
7ad76f5683 | ||
|
|
09a9457021 | ||
|
|
d8b45396e3 | ||
|
|
ca4643ec36 | ||
|
|
e2f78ba476 | ||
|
|
5d92190d50 | ||
|
|
2b0d6de986 | ||
|
|
057f82a561 | ||
|
|
719d2a5ffe | ||
|
|
d4bff9d5cb | ||
|
|
19fcc1f93b | ||
|
|
d45ea127c2 | ||
|
|
f591cf8601 | ||
|
|
6661a76aa8 | ||
|
|
a2ed22bfcc | ||
|
|
e370f8891a | ||
|
|
8a83e32c42 | ||
|
|
831eb6325c | ||
|
|
4d6240c987 | ||
|
|
79cf7c84dc | ||
|
|
b71f582329 | ||
|
|
8315d4b6ae | ||
|
|
b8c3cc751a | ||
|
|
d00262dc31 | ||
|
|
952d0c74d0 | ||
|
|
ffbea7af59 | ||
|
|
3debc6c8d3 | ||
|
|
5092eb58fb | ||
|
|
f0b9240575 | ||
|
|
9cf59c409e | ||
|
|
971c375398 | ||
|
|
ac4439c5ae | ||
|
|
bfd5aa30a7 | ||
|
|
9737170665 | ||
|
|
922a040466 | ||
|
|
9eacefb155 | ||
|
|
33f0782f3a | ||
|
|
e6a5cef945 | ||
|
|
4c8edb80b3 | ||
|
|
d4668fae99 | ||
|
|
ddfe55e3ae | ||
|
|
761a5f1d4c | ||
|
|
1fbcad8787 | ||
|
|
aba586e605 | ||
|
|
27b21b5ad4 | ||
|
|
b6e54dab17 | ||
|
|
1f8e89772d | ||
|
|
843b13ed57 | ||
|
|
be89e5ca55 | ||
|
|
5f3657fd56 | ||
|
|
494162400e | ||
|
|
ab65bb6a8a | ||
|
|
333625f199 | ||
|
|
dbfd715381 | ||
|
|
f1d989964e | ||
|
|
b701629498 | ||
|
|
8250946325 | ||
|
|
71f63d8e6f | ||
|
|
dd5e834db0 | ||
|
|
970ecb52f0 | ||
|
|
62ea1b40e1 | ||
|
|
3b0fd5c592 | ||
|
|
b7616026dd | ||
|
|
16ad60b89a | ||
|
|
db7971d2f7 | ||
|
|
f3f8bd3125 | ||
|
|
516fd0ee8f | ||
|
|
8d6700d493 | ||
|
|
9d4ace9b3e | ||
|
|
2800655e33 | ||
|
|
91eecee11d | ||
|
|
899e5aa395 | ||
|
|
d5820c4902 | ||
|
|
a91c002274 | ||
|
|
4d142b93dd | ||
|
|
04dcf57ff3 | ||
|
|
975550c755 | ||
|
|
a964a80d85 | ||
|
|
22c3b8f116 | ||
|
|
c4b1831cfe | ||
|
|
cdb6813384 | ||
|
|
b14b68d83c | ||
|
|
3c2f930e6b | ||
|
|
ca9c7ce555 | ||
|
|
c2e95a0607 | ||
|
|
2767ee9e80 | ||
|
|
d998a8087f | ||
|
|
fdce016921 | ||
|
|
c73d70933b | ||
|
|
e9d0ad6e37 | ||
|
|
a35586f762 | ||
|
|
f527c30923 | ||
|
|
94e70219cf | ||
|
|
6496763aae | ||
|
|
a409ec269b | ||
|
|
bc7bc8da66 | ||
|
|
52484c774e | ||
|
|
4e1e0cade1 | ||
|
|
fda5904dac | ||
|
|
69ecc22318 | ||
|
|
bff9d33ee6 | ||
|
|
edf506953b | ||
|
|
5e11746549 | ||
|
|
1ae315e303 | ||
|
|
758b03ab25 | ||
|
|
e756fad573 | ||
|
|
3547450b03 | ||
|
|
733f6692c6 | ||
|
|
2d83160b16 | ||
|
|
256fa880dd | ||
|
|
b08c5f5c67 | ||
|
|
d0862a2d26 | ||
|
|
e97340ed52 | ||
|
|
e27c81eea6 | ||
|
|
7f7f3d43b2 | ||
|
|
4b1b772098 | ||
|
|
f66b88490f | ||
|
|
18f9157169 | ||
|
|
6eb82a807b | ||
|
|
bf57a97833 | ||
|
|
e9e2093220 | ||
|
|
c3540da2e3 | ||
|
|
d228cf56dd | ||
|
|
8f4cecd963 | ||
|
|
66adff44bb | ||
|
|
be41c094dc | ||
|
|
273848ca18 | ||
|
|
1e9dbead3b | ||
|
|
aeaa8ba133 | ||
|
|
24654af635 | ||
|
|
e88a21d6db | ||
|
|
bcd01badaf | ||
|
|
8e063506e0 | ||
|
|
84f5d6137a | ||
|
|
0a8565f5e8 | ||
|
|
bd8da25a46 | ||
|
|
a841f588dd | ||
|
|
75a4362ce3 | ||
|
|
e763e001e5 | ||
|
|
69475a0ae7 | ||
|
|
53e14c2ad7 | ||
|
|
1edc33148a | ||
|
|
a4cbfc74e4 | ||
|
|
c0d25aeb02 | ||
|
|
40f49bf6da | ||
|
|
0bfce87dc6 | ||
|
|
2a0655e9de | ||
|
|
a86cfa5934 | ||
|
|
54b77523c5 | ||
|
|
ba06c8928d | ||
|
|
c8a4ac1ed4 | ||
|
|
143acbae48 | ||
|
|
937f6fdae8 | ||
|
|
ba7239ac08 | ||
|
|
2e748274c0 | ||
|
|
eab2750953 | ||
|
|
17b6cb0c73 | ||
|
|
98a4c453c1 | ||
|
|
6475dceab9 | ||
|
|
040a945774 | ||
|
|
47743a5fa8 | ||
|
|
d47d6de985 | ||
|
|
37818b8594 | ||
|
|
3b184acddd | ||
|
|
9c80404d17 | ||
|
|
aaa7082f9d | ||
|
|
a45b45b2ce | ||
|
|
e4bfbd267e | ||
|
|
65b4dcc672 | ||
|
|
36fc30b524 | ||
|
|
e724ed9137 | ||
|
|
7ca992af05 | ||
|
|
37f1c714ac | ||
|
|
397a43fb60 | ||
|
|
45e0a648c6 | ||
|
|
7336aa81d9 | ||
|
|
d727c10d98 | ||
|
|
321d77a317 | ||
|
|
19b8a6b737 | ||
|
|
f2e69dfb96 | ||
|
|
8207e49317 | ||
|
|
b75600b9ea | ||
|
|
7b01f1bef6 | ||
|
|
e7bd2c0001 | ||
|
|
a26076e9db | ||
|
|
9711a0fb8e | ||
|
|
accc670411 | ||
|
|
071c41a54f | ||
|
|
35ba6c19c3 | ||
|
|
14c8348166 | ||
|
|
7d6ee72025 | ||
|
|
ea0e770b57 | ||
|
|
193b7ff21e | ||
|
|
d814ad9f3e | ||
|
|
da8b620c75 | ||
|
|
911b5e6814 | ||
|
|
f991fd9c71 | ||
|
|
652e4c922d | ||
|
|
4364e3fbc1 | ||
|
|
a783fdecbc | ||
|
|
16f67455a2 | ||
|
|
0850a28d20 | ||
|
|
5ca598139e | ||
|
|
df1bf09163 | ||
|
|
50bc8d3e9c | ||
|
|
86d089024e | ||
|
|
d5c1cf594d | ||
|
|
a0b5731e69 | ||
|
|
ceb359d614 | ||
|
|
a49a9f8e3b | ||
|
|
766606b08d | ||
|
|
fed56c1959 | ||
|
|
ae6ed8ad97 | ||
|
|
c1ca0b8e2c | ||
|
|
569dc735ce | ||
|
|
dd11c2c871 | ||
|
|
8def4a2b68 | ||
|
|
13a5f24b07 | ||
|
|
0989d6353e | ||
|
|
4139a7b73f | ||
|
|
be60d66ce3 | ||
|
|
0a33043874 | ||
|
|
96d1d983e5 | ||
|
|
7ffb260d7c | ||
|
|
ce74489df5 | ||
|
|
342b188fae | ||
|
|
fa6fee7b55 | ||
|
|
c53d5a4d7d | ||
|
|
521e905724 | ||
|
|
4623090050 | ||
|
|
dd9e5cc541 | ||
|
|
626be6a347 | ||
|
|
56327ed503 | ||
|
|
6d1665004b | ||
|
|
59b8119fbd | ||
|
|
9ff863db5e | ||
|
|
e2ac6e6d4d | ||
|
|
df4101875a | ||
|
|
3f5c788d48 | ||
|
|
45cd4df6e5 | ||
|
|
94ac3ec76e | ||
|
|
af7263a0b1 | ||
|
|
035396f95c | ||
|
|
f318f6304b | ||
|
|
9d0ff472e5 | ||
|
|
d27482e812 | ||
|
|
d5b6de70da | ||
|
|
69c2212ea0 | ||
|
|
10be9bcd56 | ||
|
|
f531def0d2 | ||
|
|
ed40eae655 | ||
|
|
ba5ae6ed04 | ||
|
|
d6ade102dc | ||
|
|
0a6301697e | ||
|
|
13b4fc6725 | ||
|
|
c94d246c24 | ||
|
|
5b779ba9fe | ||
|
|
3ba2cb19a9 | ||
|
|
a095dddd01 | ||
|
|
1b5cfaa49b | ||
|
|
66f3fabbae | ||
|
|
0be8fb7931 | ||
|
|
431e6ffaae | ||
|
|
7d8185e0ee | ||
|
|
dff45748bd | ||
|
|
da514ef314 | ||
|
|
7f73cde794 | ||
|
|
b0af0d9cd5 | ||
|
|
e6464929ff | ||
|
|
122053939d | ||
|
|
8429197b07 | ||
|
|
44f2081882 | ||
|
|
300b4a3706 | ||
|
|
81ef2db7f8 | ||
|
|
c41e8be3e8 | ||
|
|
41bab0ce0b | ||
|
|
5f26b9eeea | ||
|
|
1cca69ad23 | ||
|
|
410ed3949b | ||
|
|
efc6ef3075 | ||
|
|
63f7dd1d20 | ||
|
|
57b8c69983 | ||
|
|
aad060810a | ||
|
|
9222b00a6f | ||
|
|
ff61b22e7e | ||
|
|
577cb91343 | ||
|
|
1889386f64 | ||
|
|
5d7f082ebf | ||
|
|
db6327c4ff | ||
|
|
fd7f6b2b99 | ||
|
|
49435398a8 | ||
|
|
e101ac341b | ||
|
|
6cfc7b7c69 | ||
|
|
313acabc86 | ||
|
|
34cced872f | ||
|
|
ac09e3aaf9 | ||
|
|
9f2fd34e99 | ||
|
|
67b63d3084 | ||
|
|
4a31a7b84b | ||
|
|
538b601b1e | ||
|
|
588f064c25 | ||
|
|
d521e79662 | ||
|
|
ccddb9244d | ||
|
|
0547396213 | ||
|
|
6c85171091 | ||
|
|
a8f6b6c1da | ||
|
|
f899326189 | ||
|
|
0f4d1d2a74 | ||
|
|
941d5c08e3 | ||
|
|
db9f74158b | ||
|
|
b4c01349d1 | ||
|
|
165bbd3584 | ||
|
|
ffb253e0e9 | ||
|
|
e5e9fe456f | ||
|
|
c63589b204 | ||
|
|
11408c2656 | ||
|
|
7d4aed8819 | ||
|
|
609ffccd67 | ||
|
|
508369a59d | ||
|
|
748af1d8cb | ||
|
|
26a91cd5e1 | ||
|
|
48dd4d5913 | ||
|
|
d309ec249e | ||
|
|
72d46b7352 | ||
|
|
4613aae47d | ||
|
|
1bc4480d84 | ||
|
|
b5d76f73e8 | ||
|
|
a5c7913e77 | ||
|
|
34b914f509 | ||
|
|
5a3d75ca12 | ||
|
|
158d7b23d8 | ||
|
|
67949b4968 | ||
|
|
1fc40b3017 | ||
|
|
bb1a375484 | ||
|
|
bf5dd3b0a1 | ||
|
|
e4d4c62833 | ||
|
|
20ae903d7f | ||
|
|
f5f757e4bd | ||
|
|
13c011895d | ||
|
|
bd8d0e3392 | ||
|
|
5ad564d21b | ||
|
|
8f8775cb93 | ||
|
|
37695827aa | ||
|
|
7a72d209ea | ||
|
|
cda6b67bef | ||
|
|
066305b095 | ||
|
|
f2ba4b270f | ||
|
|
89695df012 | ||
|
|
b0566d3c6f | ||
|
|
5dda8c384f | ||
|
|
b04385a340 | ||
|
|
d374ea6ea6 | ||
|
|
01a2820390 | ||
|
|
c89c1a03da | ||
|
|
873408270e | ||
|
|
8fec8f35bc | ||
|
|
141c846fe2 | ||
|
|
cb569ff14d | ||
|
|
1497469016 | ||
|
|
e356a6d33b | ||
|
|
38ac4c5980 | ||
|
|
ed3ee64e4b | ||
|
|
12aea2901d | ||
|
|
5ff56467ea | ||
|
|
3a8718a4b0 | ||
|
|
8c15855fc3 | ||
|
|
37c4a7b690 | ||
|
|
b735e7c34d | ||
|
|
5f85c3b3b8 | ||
|
|
5d9cb9fa21 | ||
|
|
643d56958d | ||
|
|
f378d6f040 | ||
|
|
bb57794388 | ||
|
|
a9ca49b8a2 | ||
|
|
c1b473294e | ||
|
|
e3e4bdfe09 | ||
|
|
bfbeace2e2 | ||
|
|
efcf46ce8a | ||
|
|
2085715965 | ||
|
|
d227db7b7b | ||
|
|
2af67ad355 | ||
|
|
f100854423 | ||
|
|
92331d7a33 | ||
|
|
9a5bcb9099 | ||
|
|
8eb6bb2a95 | ||
|
|
2aa65ccab3 | ||
|
|
be1577a3e7 | ||
|
|
c8e1b3bf29 | ||
|
|
e17b986628 | ||
|
|
5f19918ca0 | ||
|
|
2959ad0e70 | ||
|
|
a76eec7bb7 | ||
|
|
068b2a0dcd | ||
|
|
316b7e5653 | ||
|
|
00fc1da33c | ||
|
|
9ef93df54f | ||
|
|
fd9fdf6399 | ||
|
|
8fa1701e06 | ||
|
|
4abe83f8a9 | ||
|
|
0a7564acb6 | ||
|
|
db0f7cfbae | ||
|
|
1724885371 | ||
|
|
a97e9ea8b1 | ||
|
|
9d30e97526 | ||
|
|
b91330a27a | ||
|
|
744bc9ebe9 | ||
|
|
89ed9e6d7f | ||
|
|
b007e7f54a | ||
|
|
6651a6df42 | ||
|
|
3f29b165aa | ||
|
|
b13b91face | ||
|
|
63c14fe2d5 | ||
|
|
14e74ed02d | ||
|
|
7e30750618 | ||
|
|
4d1dd16be5 | ||
|
|
fa49cf5eba | ||
|
|
26b39fc1c6 | ||
|
|
0d36e368ea | ||
|
|
859f265c68 | ||
|
|
3219f520ba | ||
|
|
97e27b6caf | ||
|
|
09da83a72b | ||
|
|
d13b210e2f | ||
|
|
09fb672718 | ||
|
|
9797ad0e17 | ||
|
|
8b3d61ac36 | ||
|
|
7161c9547a | ||
|
|
60d4362a87 | ||
|
|
1836e0c8fc | ||
|
|
d3344aeb34 | ||
|
|
cfeb093fa6 | ||
|
|
a469b3ffcc | ||
|
|
14b3a3fdd8 | ||
|
|
94367ce387 | ||
|
|
5be518aa50 | ||
|
|
d059a8da9e | ||
|
|
1dcacbef7a | ||
|
|
a25edeccf7 | ||
|
|
315f73c77d | ||
|
|
666288fccc | ||
|
|
0ccf61c2a9 | ||
|
|
c16b1b27a3 | ||
|
|
ed9ba60be6 | ||
|
|
24d047e3d8 | ||
|
|
9671079ffb | ||
|
|
688892523c | ||
|
|
b02c341f62 | ||
|
|
3e9bcada1e | ||
|
|
93d4bd6438 | ||
|
|
5146498b33 | ||
|
|
72da4f39a8 | ||
|
|
a2b2fb804b | ||
|
|
3eac80e666 | ||
|
|
718d2122a4 | ||
|
|
310c6c90a3 | ||
|
|
9d80f62d58 | ||
|
|
77032fc989 | ||
|
|
64e6086f0c | ||
|
|
3aa58fdc8f | ||
|
|
93bc6ba615 | ||
|
|
36690d63cb | ||
|
|
9896e9799a | ||
|
|
27afc82b79 | ||
|
|
1c8f01ce7b | ||
|
|
4038ccff0d | ||
|
|
5b41bc2f59 | ||
|
|
014ba760b5 | ||
|
|
96a91ccf09 | ||
|
|
347fbd2a48 | ||
|
|
29723052ab | ||
|
|
86415d675b | ||
|
|
8fc4a0dc48 | ||
|
|
e14670cdda | ||
|
|
4d73488f0c | ||
|
|
46e62b24cf | ||
|
|
17c3041fe9 | ||
|
|
d5ae381528 | ||
|
|
e2e09527ec | ||
|
|
3ce1afbcc9 | ||
|
|
1f077d7ec2 | ||
|
|
adf3d0347b | ||
|
|
7ed8b16a53 | ||
|
|
9f7c162107 | ||
|
|
fb15f8cde6 | ||
|
|
45ecfcc6bb | ||
|
|
c6f947e470 | ||
|
|
adf5caf18a | ||
|
|
0b8068e13d | ||
|
|
f143d2e214 | ||
|
|
2e802301ae | ||
|
|
7305c721a6 | ||
|
|
b299f3d6aa | ||
|
|
e09cd6c16c | ||
|
|
b7df8b7319 | ||
|
|
c92b5942fc | ||
|
|
fe729ec762 | ||
|
|
915673798e | ||
|
|
9527fe4f26 | ||
|
|
e8a8b3f664 | ||
|
|
d6a829abc2 | ||
|
|
1a36cd0317 | ||
|
|
75005ccf81 | ||
|
|
fd6c600531 | ||
|
|
6996c2501e | ||
|
|
efbd9bdb56 | ||
|
|
0d34213647 | ||
|
|
870b85d71b | ||
|
|
86ba6b6f86 | ||
|
|
02be3cd0c4 | ||
|
|
1b756ef9a0 | ||
|
|
ceda06f9ae | ||
|
|
068eba015b | ||
|
|
7ae6b2df05 | ||
|
|
6765d5ad26 | ||
|
|
35cfd6bec9 | ||
|
|
90f66baf85 | ||
|
|
5edfed78f2 | ||
|
|
fd6a3e5a17 | ||
|
|
14a4b1b4b4 | ||
|
|
5743c0bb72 | ||
|
|
acca1b6a91 | ||
|
|
355265cd1e | ||
|
|
6ec8d143fa | ||
|
|
8ae327e8f5 | ||
|
|
c03a61f613 | ||
|
|
89928c753c | ||
|
|
a56fcc0fba | ||
|
|
43c60bcdbc | ||
|
|
a3fa12f0e4 | ||
|
|
d696556097 | ||
|
|
6a45151741 | ||
|
|
34e2fbefb9 | ||
|
|
f7cede4713 | ||
|
|
610b20c1ff | ||
|
|
fb19e10cdc | ||
|
|
2f1756ccf2 | ||
|
|
ce632a25cf | ||
|
|
ec10c37468 | ||
|
|
5ee3e140ed | ||
|
|
888f5f8bb6 | ||
|
|
9114dd5992 | ||
|
|
a126494c12 | ||
|
|
79ba804c88 | ||
|
|
e2cbe11a5f | ||
|
|
05748bf8ff | ||
|
|
f8c98bf6bf | ||
|
|
f4496bb23a | ||
|
|
c93766bb48 | ||
|
|
a1ea3f74b3 | ||
|
|
06aaa7c680 | ||
|
|
65e8bfc93e | ||
|
|
ff5e12655f | ||
|
|
1065004fa3 | ||
|
|
6d90d734f4 | ||
|
|
6c8757f230 | ||
|
|
40e37b1798 | ||
|
|
8e1fd4474f | ||
|
|
bd87585396 | ||
|
|
e9e935d6c4 | ||
|
|
2f2c2b4222 | ||
|
|
9749a272ec | ||
|
|
b76a50238e | ||
|
|
a4f3963a5a | ||
|
|
d52bd65d21 | ||
|
|
fb51f42f35 | ||
|
|
c910a715bd | ||
|
|
9040f9b82a | ||
|
|
fc0ec0d754 | ||
|
|
b3569174b6 | ||
|
|
0cae624995 | ||
|
|
cbf184342b | ||
|
|
ce123a7f1a | ||
|
|
0c5daa7173 | ||
|
|
bc20a34a49 | ||
|
|
d5b6a426a9 | ||
|
|
4c78e93143 | ||
|
|
5f184e9e5e | ||
|
|
2201b0395d | ||
|
|
51818044b1 | ||
|
|
30943010e6 | ||
|
|
dd5ca10226 | ||
|
|
a56b058858 | ||
|
|
eade72e2c6 | ||
|
|
e9bc9747b8 | ||
|
|
eb0cdda0f9 | ||
|
|
552adf3200 | ||
|
|
eba25fcc4d | ||
|
|
673cd0fcd1 | ||
|
|
b941b5571f | ||
|
|
ca026b41c0 | ||
|
|
29a683a815 | ||
|
|
69dbd20ea5 | ||
|
|
427ee026ac | ||
|
|
0a537c6830 | ||
|
|
89682a2ee4 | ||
|
|
78b00a18cc | ||
|
|
192702daf9 | ||
|
|
fcee735578 | ||
|
|
2ba49e84bb | ||
|
|
262376aa75 | ||
|
|
4c8d2266ec | ||
|
|
bb98bf03aa | ||
|
|
19c3efc9e9 | ||
|
|
7164721ee0 | ||
|
|
74b16809ec | ||
|
|
220723d25f | ||
|
|
fdb03c9626 | ||
|
|
a81bbb9192 | ||
|
|
7a4aff8e4b | ||
|
|
2810632f4a | ||
|
|
2d0dd067b8 | ||
|
|
3ab25f5ff1 | ||
|
|
39bebea5f7 | ||
|
|
57681dcd3d | ||
|
|
168ce549f7 | ||
|
|
9ec94441f3 | ||
|
|
53e7b99605 | ||
|
|
abfe476cb9 | ||
|
|
bbca200ceb | ||
|
|
cb21cab117 | ||
|
|
1f80845a7a | ||
|
|
20088ef82b | ||
|
|
1e0b1a3607 | ||
|
|
24e8455c73 | ||
|
|
e42a732e93 | ||
|
|
0f2b94307f | ||
|
|
d333cb5199 | ||
|
|
a6db4f20ad | ||
|
|
9ed9472c01 | ||
|
|
f7fcde8312 | ||
|
|
6660c850f3 | ||
|
|
8a08bdf9f0 | ||
|
|
87807e22e0 | ||
|
|
0eb39abdb4 | ||
|
|
a499ebc158 | ||
|
|
9467e6c032 | ||
|
|
9d849a0ced | ||
|
|
2ca400ab16 | ||
|
|
4183067c77 | ||
|
|
5eb4691973 | ||
|
|
d14dfbf360 | ||
|
|
493a5ad02a | ||
|
|
481beff028 | ||
|
|
f1f7e438b4 | ||
|
|
00f84c9d8e | ||
|
|
f75b9c6c86 | ||
|
|
31bc6d5773 | ||
|
|
51dc1450d3 | ||
|
|
fcbea08c87 | ||
|
|
8d60a87aa1 | ||
|
|
956aa64519 | ||
|
|
fd1cb6ca23 | ||
|
|
37082ae436 | ||
|
|
bb47ca3d2e | ||
|
|
0dd3c84b24 | ||
|
|
848fca7e1b | ||
|
|
2500f99722 | ||
|
|
c7737c444f | ||
|
|
4d1a7ed69b | ||
|
|
626d5df67e | ||
|
|
e4c369deec | ||
|
|
307209e73f | ||
|
|
dc84935ee6 | ||
|
|
998c1f52ca | ||
|
|
2766758c66 | ||
|
|
258d1d82f3 | ||
|
|
46aaadb76a | ||
|
|
ea7a618810 | ||
|
|
c0e503b31f | ||
|
|
55f5a41752 | ||
|
|
b0be82be86 | ||
|
|
96a9bdb700 | ||
|
|
74e6d39c24 | ||
|
|
61dfa00222 | ||
|
|
476281db2b | ||
|
|
f32e31c73d | ||
|
|
ea72279080 | ||
|
|
16ba56af84 | ||
|
|
f13ddde988 | ||
|
|
67dc10dfe9 | ||
|
|
5fd216adc2 | ||
|
|
6f0268f6c0 | ||
|
|
2996dfb33a | ||
|
|
c92f2cd4ba | ||
|
|
8164d5c1ad | ||
|
|
d9d8d85f6e | ||
|
|
d49720703f | ||
|
|
2362a9b4dd | ||
|
|
a8265a5286 | ||
|
|
9ea7431b73 | ||
|
|
37e6f320fe | ||
|
|
c0c0d48edf | ||
|
|
284cccbe17 | ||
|
|
81a9a94264 | ||
|
|
dccf101554 | ||
|
|
a01c06bbc7 | ||
|
|
db43cf1b30 | ||
|
|
2f561b5604 | ||
|
|
5a30f036ff | ||
|
|
768b9ffd09 | ||
|
|
8732e50047 | ||
|
|
d6e0024c96 | ||
|
|
9759e86921 | ||
|
|
982c692c40 | ||
|
|
0c3ce7836c | ||
|
|
7ef86c5707 | ||
|
|
f62b88b930 | ||
|
|
03a326c841 | ||
|
|
4df4cafd70 | ||
|
|
4b9539cc6d | ||
|
|
87135c90bd | ||
|
|
853d416b2f | ||
|
|
bfd14b87bd | ||
|
|
88aba4e169 | ||
|
|
99e2fcb2e8 | ||
|
|
1f138ab68c | ||
|
|
99ded7454e | ||
|
|
f82cacac6d | ||
|
|
a548f61ea6 | ||
|
|
bfae715076 | ||
|
|
358e25b7c2 | ||
|
|
2c3fa54933 | ||
|
|
00cdd5833e | ||
|
|
52b1164e58 | ||
|
|
657bc9cdf0 | ||
|
|
ec6bcd41b0 | ||
|
|
1721cce040 | ||
|
|
e41a5ad6b0 | ||
|
|
ee1eca9e66 | ||
|
|
d049369172 | ||
|
|
6280a68d51 | ||
|
|
32054dc4f6 | ||
|
|
831c631048 | ||
|
|
e23711bcce | ||
|
|
440bff57d0 | ||
|
|
7345cc81c1 | ||
|
|
164ab26069 | ||
|
|
4b6ace80d3 | ||
|
|
653127a0f7 | ||
|
|
bf3a1e20fc | ||
|
|
d7a44e7589 | ||
|
|
6c0d583557 | ||
|
|
13f0fb25da | ||
|
|
818aca9ec8 | ||
|
|
1c7fb476b0 | ||
|
|
93843ed733 | ||
|
|
0973313703 | ||
|
|
bfbfbe8b11 | ||
|
|
8c62d9fe78 | ||
|
|
d5558f55ed | ||
|
|
a96ad6bd07 | ||
|
|
00d9482a99 | ||
|
|
0f90e2a30f | ||
|
|
3eed636404 | ||
|
|
a67f88381f | ||
|
|
808fd856d1 | ||
|
|
5b9b532458 | ||
|
|
9fba9bd6b7 | ||
|
|
c5ece144d0 | ||
|
|
b64e2e11db | ||
|
|
0ccd5714f9 | ||
|
|
e2dfc3eb20 | ||
|
|
40eeb9b7cb | ||
|
|
8fa62a0908 | ||
|
|
446eba8bc9 | ||
|
|
18579c0647 | ||
|
|
2bb94e24eb | ||
|
|
0d37e08638 | ||
|
|
ca89c5feca | ||
|
|
729c2adb3f | ||
|
|
a21f49cb02 | ||
|
|
ef697c4864 | ||
|
|
2652dea09a | ||
|
|
efa9312fca | ||
|
|
074ee70025 | ||
|
|
77117e48e3 | ||
|
|
da112d3417 | ||
|
|
ddaaf34dbd | ||
|
|
373e35324e | ||
|
|
09b2f27749 | ||
|
|
7e9f18bf24 | ||
|
|
ab3be26790 | ||
|
|
5c67a1cb12 | ||
|
|
e28ab19ed4 | ||
|
|
59f8334cfd | ||
|
|
718bec4bbc | ||
|
|
2d731cb24b | ||
|
|
1905936950 | ||
|
|
c362bc673c | ||
|
|
4da0a752ef | ||
|
|
221ee6a1c2 | ||
|
|
2e60ecec87 | ||
|
|
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 |
@@ -28,7 +28,9 @@ LICENSE
|
|||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
dist
|
dist
|
||||||
.git
|
.git
|
||||||
migrations/
|
server/migrations/
|
||||||
config/
|
config/
|
||||||
build.ts
|
build.ts
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
|
Dockerfile*
|
||||||
|
drizzle.config.ts
|
||||||
|
|||||||
@@ -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"
|
||||||
518
.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@v6
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Start EC2 instances
|
||||||
|
run: |
|
||||||
|
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
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,27 +62,209 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- 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@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
||||||
with:
|
with:
|
||||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- 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@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Log in to Docker Hub
|
||||||
|
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- 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
|
||||||
|
|
||||||
@@ -81,36 +289,21 @@ jobs:
|
|||||||
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Update install/main.go
|
|
||||||
run: |
|
|
||||||
PANGOLIN_VERSION=${{ env.TAG }}
|
|
||||||
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }}
|
|
||||||
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
|
|
||||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$PANGOLIN_VERSION\"/" install/main.go
|
|
||||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$GERBIL_VERSION\"/" install/main.go
|
|
||||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$BADGER_VERSION\"/" install/main.go
|
|
||||||
echo "Updated install/main.go with Pangolin version $PANGOLIN_VERSION, Gerbil version $GERBIL_VERSION, and Badger version $BADGER_VERSION"
|
|
||||||
cat install/main.go
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Build installer
|
- name: Build installer
|
||||||
working-directory: install
|
working-directory: install
|
||||||
run: |
|
run: |
|
||||||
make go-build-release
|
make go-build-release \
|
||||||
|
PANGOLIN_VERSION=${{ env.TAG }} \
|
||||||
|
GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }} \
|
||||||
|
BADGER_VERSION=${{ env.LATEST_BADGER_TAG }}
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Upload artifacts from /install/bin
|
- name: Upload artifacts from /install/bin
|
||||||
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
|
uses: actions/upload-artifact@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 +314,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 }}
|
||||||
echo "Copying ${{ env.DOCKERHUB_IMAGE }}:${TAG} -> ${{ env.GHCR_IMAGE }}:${TAG}"
|
MAJOR_TAG=$(echo $TAG | cut -d. -f1)
|
||||||
skopeo copy --all --retry-times 3 \
|
MINOR_TAG=$(echo $TAG | cut -d. -f1,2)
|
||||||
docker://$DOCKERHUB_IMAGE:$TAG \
|
|
||||||
docker://$GHCR_IMAGE:$TAG
|
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}"
|
||||||
|
skopeo copy --all --retry-times 3 \
|
||||||
|
docker://$DOCKERHUB_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@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.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,26 +432,155 @@ 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"
|
||||||
echo "Resolved digest: ${REF}"
|
if [[ "$TAG" == *"-rc."* ]]; then
|
||||||
|
IS_RC="true"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "==> cosign sign (keyless) --recursive ${REF}"
|
# Define image variants to sign
|
||||||
cosign sign --recursive "${REF}"
|
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
|
||||||
|
|
||||||
echo "==> cosign sign (key) --recursive ${REF}"
|
# Sign each image variant for both registries
|
||||||
cosign sign --key env://COSIGN_PRIVATE_KEY --recursive "${REF}"
|
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
|
||||||
|
|
||||||
echo "==> cosign verify (public key) ${REF}"
|
# Wrap the entire tag processing in error handling
|
||||||
cosign verify --key env://COSIGN_PUBLIC_KEY "${REF}" -o text
|
(
|
||||||
|
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 "==> cosign verify (keyless policy) ${REF}"
|
echo "==> cosign sign (keyless) --recursive ${REF}"
|
||||||
cosign verify \
|
cosign sign --recursive "${REF}"
|
||||||
--certificate-oidc-issuer "${issuer}" \
|
|
||||||
--certificate-identity-regexp "${id_regex}" \
|
echo "==> cosign sign (key) --recursive ${REF}"
|
||||||
"${REF}" -o text
|
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}"
|
||||||
|
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}"
|
||||||
|
if retry_verify "cosign verify --certificate-oidc-issuer '${issuer}' --certificate-identity-regexp '${id_regex}' '${REF}' -o text"; then
|
||||||
|
VERIFIED_INDEX_KEYLESS=true
|
||||||
|
else
|
||||||
|
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@v6
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Stop EC2 instances
|
||||||
|
run: |
|
||||||
|
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- 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@v6
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Start EC2 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"
|
||||||
160
.github/workflows/saas.yml
vendored
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
name: SAAS Pipeline
|
||||||
|
|
||||||
|
# CI/CD workflow for building, publishing, mirroring, signing container images and building release binaries.
|
||||||
|
# Actions are pinned to specific SHAs to reduce supply-chain risk. This workflow triggers on tag push events.
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write # for GHCR push
|
||||||
|
id-token: write # for Cosign Keyless (OIDC) Signing
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "[0-9]+.[0-9]+.[0-9]+-s.[0-9]+"
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-run:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: write-all
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Start EC2 instances
|
||||||
|
run: |
|
||||||
|
aws ec2 start-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
echo "EC2 instances started"
|
||||||
|
|
||||||
|
|
||||||
|
release-arm:
|
||||||
|
name: Build and Release (ARM64)
|
||||||
|
runs-on: [self-hosted, linux, arm64, us-east-1]
|
||||||
|
needs: [pre-run]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
needs.pre-run.result == 'success'
|
||||||
|
}}
|
||||||
|
# Job-level timeout to avoid runaway or stuck runs
|
||||||
|
timeout-minutes: 120
|
||||||
|
env:
|
||||||
|
# Target images
|
||||||
|
AWS_IMAGE: ${{ secrets.aws_account_id }}.dkr.ecr.us-east-1.amazonaws.com/${{ github.event.repository.name }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
|
- name: Download MaxMind GeoLite2 databases
|
||||||
|
env:
|
||||||
|
MAXMIND_LICENSE_KEY: ${{ secrets.MAXMIND_LICENSE_KEY }}
|
||||||
|
run: |
|
||||||
|
echo "Downloading MaxMind GeoLite2 databases..."
|
||||||
|
|
||||||
|
# Download GeoLite2-Country
|
||||||
|
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
||||||
|
-o GeoLite2-Country.tar.gz
|
||||||
|
|
||||||
|
# Download GeoLite2-ASN
|
||||||
|
curl -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-ASN&license_key=${MAXMIND_LICENSE_KEY}&suffix=tar.gz" \
|
||||||
|
-o GeoLite2-ASN.tar.gz
|
||||||
|
|
||||||
|
# Extract the .mmdb files
|
||||||
|
tar -xzf GeoLite2-Country.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
||||||
|
tar -xzf GeoLite2-ASN.tar.gz --strip-components=1 --wildcards '*.mmdb'
|
||||||
|
|
||||||
|
# Verify files exist
|
||||||
|
if [ ! -f "GeoLite2-Country.mmdb" ]; then
|
||||||
|
echo "ERROR: Failed to download GeoLite2-Country.mmdb"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "GeoLite2-ASN.mmdb" ]; then
|
||||||
|
echo "ERROR: Failed to download GeoLite2-ASN.mmdb"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Clean up tar files
|
||||||
|
rm -f GeoLite2-Country.tar.gz GeoLite2-ASN.tar.gz
|
||||||
|
|
||||||
|
echo "MaxMind databases downloaded successfully"
|
||||||
|
ls -lh GeoLite2-*.mmdb
|
||||||
|
|
||||||
|
- name: Monitor storage space
|
||||||
|
run: |
|
||||||
|
THRESHOLD=75
|
||||||
|
USED_SPACE=$(df / | grep / | awk '{ print $5 }' | sed 's/%//g')
|
||||||
|
echo "Used space: $USED_SPACE%"
|
||||||
|
if [ "$USED_SPACE" -ge "$THRESHOLD" ]; then
|
||||||
|
echo "Used space is below the threshold of 75% free. Running Docker system prune."
|
||||||
|
echo y | docker system prune -a
|
||||||
|
else
|
||||||
|
echo "Storage space is above the threshold. No action needed."
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Login to Amazon ECR
|
||||||
|
id: login-ecr
|
||||||
|
uses: aws-actions/amazon-ecr-login@v2
|
||||||
|
|
||||||
|
- name: Extract tag name
|
||||||
|
id: get-tag
|
||||||
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Update version in package.json
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||||
|
cat server/lib/consts.ts
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Build and push Docker images (Docker Hub - ARM64)
|
||||||
|
run: |
|
||||||
|
TAG=${{ env.TAG }}
|
||||||
|
make build-saas tag=$TAG
|
||||||
|
echo "Built & pushed ARM64 images to: ${{ env.AWS_IMAGE }}:${TAG}"
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
post-run:
|
||||||
|
needs: [pre-run, release-arm]
|
||||||
|
if: >-
|
||||||
|
${{
|
||||||
|
always() &&
|
||||||
|
needs.pre-run.result == 'success' &&
|
||||||
|
(needs.release-arm.result == 'success' || needs.release-arm.result == 'skipped' || needs.release-arm.result == 'failure')
|
||||||
|
}}
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: write-all
|
||||||
|
steps:
|
||||||
|
- name: Configure AWS credentials
|
||||||
|
uses: aws-actions/configure-aws-credentials@v6
|
||||||
|
with:
|
||||||
|
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }}
|
||||||
|
role-duration-seconds: 3600
|
||||||
|
aws-region: ${{ secrets.AWS_REGION }}
|
||||||
|
|
||||||
|
- name: Verify AWS identity
|
||||||
|
run: aws sts get-caller-identity
|
||||||
|
|
||||||
|
- name: Stop EC2 instances
|
||||||
|
run: |
|
||||||
|
aws ec2 stop-instances --instance-ids ${{ secrets.EC2_INSTANCE_ID_ARM_RUNNER }}
|
||||||
|
echo "EC2 instances stopped"
|
||||||
2
.github/workflows/stale-bot.yml
vendored
@@ -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@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0
|
||||||
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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
|
|
||||||
- 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
|
||||||
|
}
|
||||||
107
Dockerfile
@@ -1,70 +1,91 @@
|
|||||||
FROM node:25-alpine AS builder
|
FROM node:24-slim AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG BUILD=oss
|
RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/*
|
||||||
ARG DATABASE=sqlite
|
|
||||||
|
|
||||||
RUN apk add --no-cache curl tzdata python3 make g++
|
|
||||||
|
|
||||||
# COPY package.json package-lock.json ./
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
|
FROM base AS builder-dev
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
ARG BUILD=oss
|
||||||
RUN echo "export const driver: \"pg\" | \"sqlite\" = \"$DATABASE\";" >> server/db/index.ts
|
ARG DATABASE=sqlite
|
||||||
|
|
||||||
RUN echo "export const build = \"$BUILD\" as \"saas\" | \"enterprise\" | \"oss\";" > server/build.ts
|
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \
|
||||||
|
npm run set:$DATABASE && \
|
||||||
|
npm run set:$BUILD && \
|
||||||
|
npm run db:generate && \
|
||||||
|
npm run build && \
|
||||||
|
npm run build:cli && \
|
||||||
|
test -f dist/server.mjs
|
||||||
|
|
||||||
# Copy the appropriate TypeScript configuration based on build type
|
# Create placeholder files for MaxMind databases to avoid COPY errors
|
||||||
RUN if [ "$BUILD" = "oss" ]; then cp tsconfig.oss.json tsconfig.json; \
|
# Real files should be present for saas builds, placeholders for oss builds
|
||||||
elif [ "$BUILD" = "saas" ]; then cp tsconfig.saas.json tsconfig.json; \
|
RUN touch /app/GeoLite2-Country.mmdb /app/GeoLite2-ASN.mmdb
|
||||||
elif [ "$BUILD" = "enterprise" ]; then cp tsconfig.enterprise.json tsconfig.json; \
|
|
||||||
fi
|
|
||||||
|
|
||||||
# if the build is oss then remove the server/private directory
|
FROM base AS builder
|
||||||
RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi
|
|
||||||
|
|
||||||
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema --out init; fi
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
RUN mkdir -p dist
|
FROM node:24-slim AS runner
|
||||||
RUN npm run next:build
|
|
||||||
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 apt-get update && apt-get install -y curl tzdata && rm -rf /var/lib/apt/lists/*
|
||||||
# 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
|
||||||
|
|
||||||
|
# Copy MaxMind databases for SaaS builds
|
||||||
|
ARG BUILD=oss
|
||||||
|
|
||||||
|
RUN mkdir -p ./maxmind
|
||||||
|
|
||||||
|
# Copy MaxMind databases (placeholders exist for oss builds, real files for saas)
|
||||||
|
COPY --from=builder-dev /app/GeoLite2-Country.mmdb ./maxmind/GeoLite2-Country.mmdb
|
||||||
|
COPY --from=builder-dev /app/GeoLite2-ASN.mmdb ./maxmind/GeoLite2-ASN.mmdb
|
||||||
|
|
||||||
|
# Remove MaxMind databases for non-saas builds (keep only for saas)
|
||||||
|
RUN if [ "$BUILD" != "saas" ]; then rm -rf ./maxmind; fi
|
||||||
|
|
||||||
|
# OCI Image Labels - Build Args for dynamic values
|
||||||
|
ARG VERSION="dev"
|
||||||
|
ARG REVISION=""
|
||||||
|
ARG CREATED=""
|
||||||
|
ARG LICENSE="AGPL-3.0"
|
||||||
|
|
||||||
|
# Derive title and description based on BUILD type
|
||||||
|
ARG IMAGE_TITLE="Pangolin"
|
||||||
|
ARG IMAGE_DESCRIPTION="Identity-aware VPN and proxy for remote access to anything, anywhere"
|
||||||
|
|
||||||
|
# OCI Image Labels
|
||||||
|
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/fosrl/pangolin" \
|
||||||
|
org.opencontainers.image.url="https://github.com/fosrl/pangolin" \
|
||||||
|
org.opencontainers.image.documentation="https://docs.pangolin.net" \
|
||||||
|
org.opencontainers.image.vendor="Fossorial" \
|
||||||
|
org.opencontainers.image.licenses="${LICENSE}" \
|
||||||
|
org.opencontainers.image.title="${IMAGE_TITLE}" \
|
||||||
|
org.opencontainers.image.description="${IMAGE_DESCRIPTION}" \
|
||||||
|
org.opencontainers.image.version="${VERSION}" \
|
||||||
|
org.opencontainers.image.revision="${REVISION}" \
|
||||||
|
org.opencontainers.image.created="${CREATED}"
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
CMD ["npm", "run", "start"]
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
FROM node:22-alpine
|
FROM node:24-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk add --no-cache python3 make g++
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
121
cli/commands/generateOrgCaKeys.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { CommandModule } from "yargs";
|
||||||
|
import { db, orgs } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { encrypt } from "@server/lib/crypto";
|
||||||
|
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
|
import { generateCA } from "@server/lib/sshCA";
|
||||||
|
import fs from "fs";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
type GenerateOrgCaKeysArgs = {
|
||||||
|
orgId: string;
|
||||||
|
secret?: string;
|
||||||
|
force?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateOrgCaKeys: CommandModule<{}, GenerateOrgCaKeysArgs> = {
|
||||||
|
command: "generate-org-ca-keys",
|
||||||
|
describe:
|
||||||
|
"Generate SSH CA public/private key pair for an organization and store them in the database (private key encrypted with server secret)",
|
||||||
|
builder: (yargs) => {
|
||||||
|
return yargs
|
||||||
|
.option("orgId", {
|
||||||
|
type: "string",
|
||||||
|
demandOption: true,
|
||||||
|
describe: "The organization ID"
|
||||||
|
})
|
||||||
|
.option("secret", {
|
||||||
|
type: "string",
|
||||||
|
describe:
|
||||||
|
"Server secret used to encrypt the CA private key. If omitted, read from config file (config.yml or config.yaml)."
|
||||||
|
})
|
||||||
|
.option("force", {
|
||||||
|
type: "boolean",
|
||||||
|
default: false,
|
||||||
|
describe:
|
||||||
|
"Overwrite existing CA keys for the org if they already exist"
|
||||||
|
});
|
||||||
|
},
|
||||||
|
handler: async (argv: {
|
||||||
|
orgId: string;
|
||||||
|
secret?: string;
|
||||||
|
force?: boolean;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
const { orgId, force } = argv;
|
||||||
|
let secret = argv.secret;
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
const configPath = fs.existsSync(configFilePath1)
|
||||||
|
? configFilePath1
|
||||||
|
: fs.existsSync(configFilePath2)
|
||||||
|
? configFilePath2
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!configPath) {
|
||||||
|
console.error(
|
||||||
|
"Error: No server secret provided and config file not found. " +
|
||||||
|
"Expected config.yml or config.yaml in the config directory, or pass --secret."
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const configContent = fs.readFileSync(configPath, "utf8");
|
||||||
|
const config = yaml.load(configContent) as {
|
||||||
|
server?: { secret?: string };
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!config?.server?.secret) {
|
||||||
|
console.error(
|
||||||
|
"Error: No server.secret in config file. Pass --secret or set server.secret in config."
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
secret = config.server.secret;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [org] = await db
|
||||||
|
.select({
|
||||||
|
orgId: orgs.orgId,
|
||||||
|
sshCaPrivateKey: orgs.sshCaPrivateKey,
|
||||||
|
sshCaPublicKey: orgs.sshCaPublicKey
|
||||||
|
})
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, orgId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
console.error(`Error: Organization with orgId "${orgId}" not found.`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (org.sshCaPrivateKey != null || org.sshCaPublicKey != null) {
|
||||||
|
if (!force) {
|
||||||
|
console.error(
|
||||||
|
"Error: This organization already has CA keys. Use --force to overwrite."
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ca = generateCA(`pangolin-ssh-ca-${orgId}`);
|
||||||
|
const encryptedPrivateKey = encrypt(ca.privateKeyPem, secret);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(orgs)
|
||||||
|
.set({
|
||||||
|
sshCaPrivateKey: encryptedPrivateKey,
|
||||||
|
sshCaPublicKey: ca.publicKeyOpenSSH
|
||||||
|
})
|
||||||
|
.where(eq(orgs.orgId, orgId));
|
||||||
|
|
||||||
|
console.log("SSH CA keys generated and stored for org:", orgId);
|
||||||
|
console.log("\nPublic key (OpenSSH format):");
|
||||||
|
console.log(ca.publicKeyOpenSSH);
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error generating org CA keys:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
10
cli/index.ts
@@ -4,10 +4,20 @@ 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";
|
||||||
|
import { generateOrgCaKeys } from "./commands/generateOrgCaKeys";
|
||||||
|
|
||||||
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)
|
||||||
|
.command(generateOrgCaKeys)
|
||||||
.demandCommand()
|
.demandCommand()
|
||||||
.help().argv;
|
.help().argv;
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.pangolin.net/self-host/advanced/config-file
|
# https://docs.pangolin.net/
|
||||||
|
|
||||||
app:
|
|
||||||
dashboard_url: http://localhost:3002
|
|
||||||
log_level: debug
|
|
||||||
|
|
||||||
domains:
|
|
||||||
domain1:
|
|
||||||
base_domain: example.com
|
|
||||||
|
|
||||||
server:
|
|
||||||
secret: my_secret_key
|
|
||||||
|
|
||||||
gerbil:
|
gerbil:
|
||||||
base_endpoint: example.com
|
start_port: 51820
|
||||||
|
base_endpoint: "{{.DashboardDomain}}"
|
||||||
|
|
||||||
orgs:
|
app:
|
||||||
block_size: 24
|
dashboard_url: "https://{{.DashboardDomain}}"
|
||||||
subnet_group: 100.90.137.0/20
|
log_level: "info"
|
||||||
|
telemetry:
|
||||||
|
anonymous_usage: true
|
||||||
|
|
||||||
|
domains:
|
||||||
|
domain1:
|
||||||
|
base_domain: "{{.BaseDomain}}"
|
||||||
|
|
||||||
|
server:
|
||||||
|
secret: "{{.Secret}}"
|
||||||
|
cors:
|
||||||
|
origins: ["https://{{.DashboardDomain}}"]
|
||||||
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
|
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||||
|
credentials: false
|
||||||
|
|
||||||
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,19 +1,19 @@
|
|||||||
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}"],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
parser: tseslint.parser,
|
parser: tseslint.parser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
ecmaVersion: "latest",
|
ecmaVersion: "latest",
|
||||||
sourceType: "module",
|
sourceType: "module",
|
||||||
ecmaFeatures: {
|
ecmaFeatures: {
|
||||||
jsx: true
|
jsx: true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
semi: "error",
|
||||||
|
"prefer-const": "warn"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
"semi": "error",
|
|
||||||
"prefer-const": "warn"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
@@ -1,41 +1,24 @@
|
|||||||
all: update-versions go-build-release put-back
|
all: go-build-release
|
||||||
dev-all: dev-update-versions dev-build dev-clean
|
|
||||||
|
# Build with version injection via ldflags
|
||||||
|
# Versions can be passed via: make go-build-release PANGOLIN_VERSION=x.x.x GERBIL_VERSION=x.x.x BADGER_VERSION=x.x.x
|
||||||
|
# Or fetched automatically if not provided (requires curl and jq)
|
||||||
|
|
||||||
|
PANGOLIN_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name')
|
||||||
|
GERBIL_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name')
|
||||||
|
BADGER_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name')
|
||||||
|
|
||||||
|
LDFLAGS = -X main.pangolinVersion=$(PANGOLIN_VERSION) \
|
||||||
|
-X main.gerbilVersion=$(GERBIL_VERSION) \
|
||||||
|
-X main.badgerVersion=$(BADGER_VERSION)
|
||||||
|
|
||||||
go-build-release:
|
go-build-release:
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
|
@echo "Building with versions - Pangolin: $(PANGOLIN_VERSION), Gerbil: $(GERBIL_VERSION), Badger: $(BADGER_VERSION)"
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/installer_linux_arm64
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_amd64
|
||||||
|
CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_arm64
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -f bin/installer_linux_amd64
|
rm -f bin/installer_linux_amd64
|
||||||
rm -f bin/installer_linux_arm64
|
rm -f bin/installer_linux_arm64
|
||||||
|
|
||||||
update-versions:
|
.PHONY: all go-build-release clean
|
||||||
@echo "Fetching latest versions..."
|
|
||||||
cp main.go main.go.bak && \
|
|
||||||
$(MAKE) dev-update-versions
|
|
||||||
|
|
||||||
put-back:
|
|
||||||
mv main.go.bak main.go
|
|
||||||
|
|
||||||
dev-update-versions:
|
|
||||||
if [ -z "$(tag)" ]; then \
|
|
||||||
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name'); \
|
|
||||||
else \
|
|
||||||
PANGOLIN_VERSION=$(tag); \
|
|
||||||
fi && \
|
|
||||||
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
|
|
||||||
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
|
|
||||||
echo "Latest versions - Pangolin: $$PANGOLIN_VERSION, Gerbil: $$GERBIL_VERSION, Badger: $$BADGER_VERSION" && \
|
|
||||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$$PANGOLIN_VERSION\"/" main.go && \
|
|
||||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$$GERBIL_VERSION\"/" main.go && \
|
|
||||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \
|
|
||||||
echo "Updated main.go with latest versions"
|
|
||||||
|
|
||||||
dev-build: go-build-release
|
|
||||||
|
|
||||||
dev-clean:
|
|
||||||
@echo "Restoring version values ..."
|
|
||||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"replaceme\"/" main.go && \
|
|
||||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"replaceme\"/" main.go && \
|
|
||||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"replaceme\"/" main.go
|
|
||||||
@echo "Restored version strings in main.go"
|
|
||||||
|
|||||||
@@ -118,19 +118,19 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse source Docker Compose YAML
|
// Parse source Docker Compose YAML
|
||||||
var sourceCompose map[string]interface{}
|
var sourceCompose map[string]any
|
||||||
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
|
if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil {
|
||||||
return fmt.Errorf("error parsing source Docker Compose file: %w", err)
|
return fmt.Errorf("error parsing source Docker Compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse destination Docker Compose YAML
|
// Parse destination Docker Compose YAML
|
||||||
var destCompose map[string]interface{}
|
var destCompose map[string]any
|
||||||
if err := yaml.Unmarshal(destData, &destCompose); err != nil {
|
if err := yaml.Unmarshal(destData, &destCompose); err != nil {
|
||||||
return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
|
return fmt.Errorf("error parsing destination Docker Compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get services section from source
|
// Get services section from source
|
||||||
sourceServices, ok := sourceCompose["services"].(map[string]interface{})
|
sourceServices, ok := sourceCompose["services"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("services section not found in source file or has invalid format")
|
return fmt.Errorf("services section not found in source file or has invalid format")
|
||||||
}
|
}
|
||||||
@@ -142,10 +142,10 @@ func copyDockerService(sourceFile, destFile, serviceName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get or create services section in destination
|
// Get or create services section in destination
|
||||||
destServices, ok := destCompose["services"].(map[string]interface{})
|
destServices, ok := destCompose["services"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
// If services section doesn't exist, create it
|
// If services section doesn't exist, create it
|
||||||
destServices = make(map[string]interface{})
|
destServices = make(map[string]any)
|
||||||
destCompose["services"] = destServices
|
destCompose["services"] = destServices
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,13 +187,12 @@ func backupConfig() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func MarshalYAMLWithIndent(data interface{}, indent int) ([]byte, error) {
|
func MarshalYAMLWithIndent(data any, indent int) ([]byte, error) {
|
||||||
buffer := new(bytes.Buffer)
|
buffer := new(bytes.Buffer)
|
||||||
encoder := yaml.NewEncoder(buffer)
|
encoder := yaml.NewEncoder(buffer)
|
||||||
encoder.SetIndent(indent)
|
encoder.SetIndent(indent)
|
||||||
|
|
||||||
err := encoder.Encode(data)
|
if err := encoder.Encode(data); err != nil {
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +208,7 @@ func replaceInFile(filepath, oldStr, newStr string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Replace the string
|
// Replace the string
|
||||||
newContent := strings.Replace(string(content), oldStr, newStr, -1)
|
newContent := strings.ReplaceAll(string(content), oldStr, newStr)
|
||||||
|
|
||||||
// Write the modified content back to the file
|
// Write the modified content back to the file
|
||||||
err = os.WriteFile(filepath, []byte(newContent), 0644)
|
err = os.WriteFile(filepath, []byte(newContent), 0644)
|
||||||
@@ -228,28 +227,28 @@ func CheckAndAddTraefikLogVolume(composePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse YAML into a generic map
|
// Parse YAML into a generic map
|
||||||
var compose map[string]interface{}
|
var compose map[string]any
|
||||||
if err := yaml.Unmarshal(data, &compose); err != nil {
|
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||||
return fmt.Errorf("error parsing compose file: %w", err)
|
return fmt.Errorf("error parsing compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get services section
|
// Get services section
|
||||||
services, ok := compose["services"].(map[string]interface{})
|
services, ok := compose["services"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("services section not found or invalid")
|
return fmt.Errorf("services section not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get traefik service
|
// Get traefik service
|
||||||
traefik, ok := services["traefik"].(map[string]interface{})
|
traefik, ok := services["traefik"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("traefik service not found or invalid")
|
return fmt.Errorf("traefik service not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check volumes
|
// Check volumes
|
||||||
logVolume := "./config/traefik/logs:/var/log/traefik"
|
logVolume := "./config/traefik/logs:/var/log/traefik"
|
||||||
var volumes []interface{}
|
var volumes []any
|
||||||
|
|
||||||
if existingVolumes, ok := traefik["volumes"].([]interface{}); ok {
|
if existingVolumes, ok := traefik["volumes"].([]any); ok {
|
||||||
// Check if volume already exists
|
// Check if volume already exists
|
||||||
for _, v := range existingVolumes {
|
for _, v := range existingVolumes {
|
||||||
if v.(string) == logVolume {
|
if v.(string) == logVolume {
|
||||||
@@ -295,13 +294,13 @@ func MergeYAML(baseFile, overlayFile string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse base YAML into a map
|
// Parse base YAML into a map
|
||||||
var baseMap map[string]interface{}
|
var baseMap map[string]any
|
||||||
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
|
if err := yaml.Unmarshal(baseContent, &baseMap); err != nil {
|
||||||
return fmt.Errorf("error parsing base YAML: %v", err)
|
return fmt.Errorf("error parsing base YAML: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse overlay YAML into a map
|
// Parse overlay YAML into a map
|
||||||
var overlayMap map[string]interface{}
|
var overlayMap map[string]any
|
||||||
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
|
if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil {
|
||||||
return fmt.Errorf("error parsing overlay YAML: %v", err)
|
return fmt.Errorf("error parsing overlay YAML: %v", err)
|
||||||
}
|
}
|
||||||
@@ -324,8 +323,8 @@ func MergeYAML(baseFile, overlayFile string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// mergeMap recursively merges two maps
|
// mergeMap recursively merges two maps
|
||||||
func mergeMap(base, overlay map[string]interface{}) map[string]interface{} {
|
func mergeMap(base, overlay map[string]any) map[string]any {
|
||||||
result := make(map[string]interface{})
|
result := make(map[string]any)
|
||||||
|
|
||||||
// Copy all key-values from base map
|
// Copy all key-values from base map
|
||||||
for k, v := range base {
|
for k, v := range base {
|
||||||
@@ -336,8 +335,8 @@ func mergeMap(base, overlay map[string]interface{}) map[string]interface{} {
|
|||||||
for k, v := range overlay {
|
for k, v := range overlay {
|
||||||
// If both maps have the same key and both values are maps, merge recursively
|
// If both maps have the same key and both values are maps, merge recursively
|
||||||
if baseVal, ok := base[k]; ok {
|
if baseVal, ok := base[k]; ok {
|
||||||
if baseMap, isBaseMap := baseVal.(map[string]interface{}); isBaseMap {
|
if baseMap, isBaseMap := baseVal.(map[string]any); isBaseMap {
|
||||||
if overlayMap, isOverlayMap := v.(map[string]interface{}); isOverlayMap {
|
if overlayMap, isOverlayMap := v.(map[string]any); isOverlayMap {
|
||||||
result[k] = mergeMap(baseMap, overlayMap)
|
result[k] = mergeMap(baseMap, overlayMap)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,10 +9,15 @@ services:
|
|||||||
PARSERS: crowdsecurity/whitelists
|
PARSERS: crowdsecurity/whitelists
|
||||||
ENROLL_TAGS: docker
|
ENROLL_TAGS: docker
|
||||||
healthcheck:
|
healthcheck:
|
||||||
interval: 10s
|
test:
|
||||||
retries: 15
|
- CMD
|
||||||
timeout: 10s
|
- cscli
|
||||||
test: ["CMD", "cscli", "capi", "status"]
|
- lapi
|
||||||
|
- status
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
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:
|
||||||
@@ -38,9 +38,7 @@ services:
|
|||||||
image: docker.io/traefik:v3.6
|
image: docker.io/traefik:v3.6
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}} network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
|
||||||
{{end}}{{if not .InstallGerbil}}
|
|
||||||
ports:
|
ports:
|
||||||
- 443:443
|
- 443:443
|
||||||
- 80:80
|
- 80:80
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
http:
|
http:
|
||||||
middlewares:
|
middlewares:
|
||||||
|
badger:
|
||||||
|
plugin:
|
||||||
|
badger:
|
||||||
|
disableForwardAuth: true
|
||||||
redirect-to-https:
|
redirect-to-https:
|
||||||
redirectScheme:
|
redirectScheme:
|
||||||
scheme: https
|
scheme: https
|
||||||
@@ -13,6 +17,7 @@ http:
|
|||||||
- web
|
- web
|
||||||
middlewares:
|
middlewares:
|
||||||
- redirect-to-https
|
- redirect-to-https
|
||||||
|
- badger
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
# Next.js router (handles everything except API and WebSocket paths)
|
||||||
next-router:
|
next-router:
|
||||||
@@ -20,6 +25,8 @@ http:
|
|||||||
service: next-service
|
service: next-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -29,6 +36,8 @@ http:
|
|||||||
service: api-service
|
service: api-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
@@ -38,6 +47,8 @@ http:
|
|||||||
service: api-service
|
service: api-service
|
||||||
entryPoints:
|
entryPoints:
|
||||||
- websecure
|
- websecure
|
||||||
|
middlewares:
|
||||||
|
- badger
|
||||||
tls:
|
tls:
|
||||||
certResolver: letsencrypt
|
certResolver: letsencrypt
|
||||||
|
|
||||||
|
|||||||
@@ -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 &&
|
||||||
@@ -144,12 +144,13 @@ func installDocker() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func startDockerService() error {
|
func startDockerService() error {
|
||||||
if runtime.GOOS == "linux" {
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
return cmd.Run()
|
return cmd.Run()
|
||||||
} else if runtime.GOOS == "darwin" {
|
case "darwin":
|
||||||
// On macOS, Docker is usually started via the Docker Desktop application
|
// On macOS, Docker is usually started via the Docker Desktop application
|
||||||
fmt.Println("Please start Docker Desktop manually on macOS.")
|
fmt.Println("Please start Docker Desktop manually on macOS.")
|
||||||
return nil
|
return nil
|
||||||
@@ -210,6 +211,47 @@ func isDockerRunning() bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isPodmanRunning() bool {
|
||||||
|
cmd := exec.Command("podman", "info")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectContainerType detects whether the system is currently using Docker or Podman
|
||||||
|
// by checking which container runtime is running and has containers
|
||||||
|
func detectContainerType() SupportedContainer {
|
||||||
|
// Check if we have running containers with podman
|
||||||
|
if isPodmanRunning() {
|
||||||
|
cmd := exec.Command("podman", "ps", "-q")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||||
|
return Podman
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have running containers with docker
|
||||||
|
if isDockerRunning() {
|
||||||
|
cmd := exec.Command("docker", "ps", "-q")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err == nil && len(strings.TrimSpace(string(output))) > 0 {
|
||||||
|
return Docker
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no containers are running, check which one is installed and running
|
||||||
|
if isPodmanRunning() && isPodmanInstalled() {
|
||||||
|
return Podman
|
||||||
|
}
|
||||||
|
|
||||||
|
if isDockerRunning() && isDockerInstalled() {
|
||||||
|
return Docker
|
||||||
|
}
|
||||||
|
|
||||||
|
return Undefined
|
||||||
|
}
|
||||||
|
|
||||||
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
||||||
func executeDockerComposeCommandWithArgs(args ...string) error {
|
func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||||
var cmd *exec.Cmd
|
var cmd *exec.Cmd
|
||||||
@@ -261,7 +303,7 @@ func pullContainers(containerType SupportedContainer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// startContainers starts the containers using the appropriate command.
|
// startContainers starts the containers using the appropriate command.
|
||||||
@@ -284,7 +326,7 @@ func startContainers(containerType SupportedContainer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// stopContainers stops the containers using the appropriate command.
|
// stopContainers stops the containers using the appropriate command.
|
||||||
@@ -306,7 +348,7 @@ func stopContainers(containerType SupportedContainer) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
// restartContainer restarts a specific container using the appropriate command.
|
// restartContainer restarts a specific container using the appropriate command.
|
||||||
@@ -328,5 +370,5 @@ func restartContainer(container string, containerType SupportedContainer) error
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
return fmt.Errorf("unsupported container type: %s", containerType)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,18 @@ func installCrowdsec(config Config) error {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
os.MkdirAll("config/crowdsec/db", 0755)
|
if err := os.MkdirAll("config/crowdsec/db", 0755); err != nil {
|
||||||
os.MkdirAll("config/crowdsec/acquis.d", 0755)
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
os.MkdirAll("config/traefik/logs", 0755)
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll("config/crowdsec/acquis.d", 0755); err != nil {
|
||||||
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll("config/traefik/logs", 0755); err != nil {
|
||||||
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
|
if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil {
|
||||||
fmt.Printf("Error copying docker service: %v\n", err)
|
fmt.Printf("Error copying docker service: %v\n", err)
|
||||||
@@ -93,7 +102,7 @@ func installCrowdsec(config Config) error {
|
|||||||
|
|
||||||
if checkIfTextInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK") {
|
if checkIfTextInFile("config/traefik/dynamic_config.yml", "PUT_YOUR_BOUNCER_KEY_HERE_OR_IT_WILL_NOT_WORK") {
|
||||||
fmt.Println("Failed to replace bouncer key! Please retrieve the key and replace it in the config/traefik/dynamic_config.yml file using the following command:")
|
fmt.Println("Failed to replace bouncer key! Please retrieve the key and replace it in the config/traefik/dynamic_config.yml file using the following command:")
|
||||||
fmt.Println(" docker exec crowdsec cscli bouncers add traefik-bouncer")
|
fmt.Printf(" %s exec crowdsec cscli bouncers add traefik-bouncer\n", config.InstallationContainerType)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -117,7 +126,7 @@ func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Execute the command to get the API key
|
// Execute the command to get the API key
|
||||||
cmd := exec.Command("docker", "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
|
cmd := exec.Command(string(containerType), "exec", "crowdsec", "cscli", "bouncers", "add", "traefik-bouncer", "-o", "raw")
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
cmd.Stdout = &out
|
cmd.Stdout = &out
|
||||||
|
|
||||||
@@ -153,34 +162,34 @@ func CheckAndAddCrowdsecDependency(composePath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Parse YAML into a generic map
|
// Parse YAML into a generic map
|
||||||
var compose map[string]interface{}
|
var compose map[string]any
|
||||||
if err := yaml.Unmarshal(data, &compose); err != nil {
|
if err := yaml.Unmarshal(data, &compose); err != nil {
|
||||||
return fmt.Errorf("error parsing compose file: %w", err)
|
return fmt.Errorf("error parsing compose file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get services section
|
// Get services section
|
||||||
services, ok := compose["services"].(map[string]interface{})
|
services, ok := compose["services"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("services section not found or invalid")
|
return fmt.Errorf("services section not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get traefik service
|
// Get traefik service
|
||||||
traefik, ok := services["traefik"].(map[string]interface{})
|
traefik, ok := services["traefik"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("traefik service not found or invalid")
|
return fmt.Errorf("traefik service not found or invalid")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get dependencies
|
// Get dependencies
|
||||||
dependsOn, ok := traefik["depends_on"].(map[string]interface{})
|
dependsOn, ok := traefik["depends_on"].(map[string]any)
|
||||||
if ok {
|
if ok {
|
||||||
// Append the new block for crowdsec
|
// Append the new block for crowdsec
|
||||||
dependsOn["crowdsec"] = map[string]interface{}{
|
dependsOn["crowdsec"] = map[string]any{
|
||||||
"condition": "service_healthy",
|
"condition": "service_healthy",
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// No dependencies exist, create it
|
// No dependencies exist, create it
|
||||||
traefik["depends_on"] = map[string]interface{}{
|
traefik["depends_on"] = map[string]any{
|
||||||
"crowdsec": map[string]interface{}{
|
"crowdsec": map[string]any{
|
||||||
"condition": "service_healthy",
|
"condition": "service_healthy",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,36 @@ module installer
|
|||||||
go 1.24.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.37.0
|
github.com/charmbracelet/huh v0.8.0
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
|
golang.org/x/term v0.40.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 (
|
||||||
|
github.com/atotto/clipboard v0.1.4 // indirect
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
|
github.com/catppuccin/go v0.3.0 // indirect
|
||||||
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.6 // indirect
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||||
|
github.com/charmbracelet/x/ansi v0.9.3 // indirect
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13 // indirect
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
|
github.com/muesli/termenv v0.16.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,7 +1,80 @@
|
|||||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ=
|
||||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE=
|
||||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||||
|
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||||
|
github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY=
|
||||||
|
github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws=
|
||||||
|
github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU=
|
||||||
|
github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||||
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||||
|
github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY=
|
||||||
|
github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||||
|
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||||
|
github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0=
|
||||||
|
github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k=
|
||||||
|
github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||||
|
github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U=
|
||||||
|
github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ=
|
||||||
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA=
|
||||||
|
github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4=
|
||||||
|
github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||||
|
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||||
|
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||||
|
github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI=
|
||||||
|
github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4=
|
||||||
|
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||||
|
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||||
|
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||||
|
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||||
|
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||||
|
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||||
|
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||||
|
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
|
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
||||||
|
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
271
install/input.go
@@ -1,74 +1,235 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"os"
|
||||||
"syscall"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
"golang.org/x/term"
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
// pangolinTheme is the custom theme using brand colors
|
||||||
|
var pangolinTheme = ThemePangolin()
|
||||||
|
|
||||||
|
// isAccessibleMode checks if we should use accessible mode (simple prompts)
|
||||||
|
// This is true for: non-TTY, TERM=dumb, or ACCESSIBLE env var set
|
||||||
|
func isAccessibleMode() bool {
|
||||||
|
// Check if stdin is not a terminal (piped input, CI, etc.)
|
||||||
|
if !term.IsTerminal(int(os.Stdin.Fd())) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Check for dumb terminal
|
||||||
|
if os.Getenv("TERM") == "dumb" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
// Check for explicit accessible mode request
|
||||||
|
if os.Getenv("ACCESSIBLE") != "" {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleAbort checks if the error is a user abort (Ctrl+C) and exits if so
|
||||||
|
func handleAbort(err error) {
|
||||||
|
if err != nil && errors.Is(err, huh.ErrUserAborted) {
|
||||||
|
fmt.Println("\nInstallation cancelled.")
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runField runs a single field with the Pangolin theme, handling accessible mode
|
||||||
|
func runField(field huh.Field) error {
|
||||||
|
if isAccessibleMode() {
|
||||||
|
return field.RunAccessible(os.Stdout, os.Stdin)
|
||||||
|
}
|
||||||
|
form := huh.NewForm(huh.NewGroup(field)).WithTheme(pangolinTheme)
|
||||||
|
return form.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func readString(prompt string, defaultValue string) string {
|
||||||
|
var value string
|
||||||
|
|
||||||
|
title := prompt
|
||||||
if defaultValue != "" {
|
if defaultValue != "" {
|
||||||
fmt.Printf("%s (default: %s): ", prompt, defaultValue)
|
title = fmt.Sprintf("%s (default: %s)", prompt, defaultValue)
|
||||||
} else {
|
|
||||||
fmt.Print(prompt + ": ")
|
|
||||||
}
|
}
|
||||||
input, _ := reader.ReadString('\n')
|
|
||||||
input = strings.TrimSpace(input)
|
input := huh.NewInput().
|
||||||
if input == "" {
|
Title(title).
|
||||||
return defaultValue
|
Value(&value)
|
||||||
|
|
||||||
|
// If no default value, this field is required
|
||||||
|
if defaultValue == "" {
|
||||||
|
input = input.Validate(func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return fmt.Errorf("this field is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return input
|
|
||||||
}
|
|
||||||
|
|
||||||
func readStringNoDefault(reader *bufio.Reader, prompt string) string {
|
err := runField(input)
|
||||||
fmt.Print(prompt + ": ")
|
handleAbort(err)
|
||||||
input, _ := reader.ReadString('\n')
|
|
||||||
return strings.TrimSpace(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readPassword(prompt string, reader *bufio.Reader) string {
|
if value == "" {
|
||||||
if term.IsTerminal(int(syscall.Stdin)) {
|
value = defaultValue
|
||||||
fmt.Print(prompt + ": ")
|
|
||||||
// Read password without echo if we're in a terminal
|
|
||||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
|
||||||
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
input := strings.TrimSpace(string(password))
|
|
||||||
if input == "" {
|
|
||||||
return readPassword(prompt, reader)
|
|
||||||
}
|
|
||||||
return input
|
|
||||||
} else {
|
|
||||||
// Fallback to reading from stdin if not in a terminal
|
|
||||||
return readString(reader, prompt, "")
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
// Print the answer so it remains visible in terminal history (skip in accessible mode as it already shows)
|
||||||
defaultStr := "no"
|
if !isAccessibleMode() {
|
||||||
if defaultValue {
|
fmt.Printf("%s: %s\n", prompt, value)
|
||||||
defaultStr = "yes"
|
|
||||||
}
|
}
|
||||||
input := readString(reader, prompt+" (yes/no)", defaultStr)
|
|
||||||
return strings.ToLower(input) == "yes"
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
|
|
||||||
input := readStringNoDefault(reader, prompt+" (yes/no)")
|
|
||||||
return strings.ToLower(input) == "yes"
|
|
||||||
}
|
|
||||||
|
|
||||||
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
|
||||||
input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
|
|
||||||
if input == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
value := defaultValue
|
|
||||||
fmt.Sscanf(input, "%d", &value)
|
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readStringNoDefault(prompt string) string {
|
||||||
|
var value string
|
||||||
|
|
||||||
|
for {
|
||||||
|
input := huh.NewInput().
|
||||||
|
Title(prompt).
|
||||||
|
Value(&value).
|
||||||
|
Validate(func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return fmt.Errorf("this field is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
err := runField(input)
|
||||||
|
handleAbort(err)
|
||||||
|
|
||||||
|
if value != "" {
|
||||||
|
// Print the answer so it remains visible in terminal history
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
fmt.Printf("%s: %s\n", prompt, value)
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readPassword(prompt string) string {
|
||||||
|
var value string
|
||||||
|
|
||||||
|
for {
|
||||||
|
input := huh.NewInput().
|
||||||
|
Title(prompt).
|
||||||
|
Value(&value).
|
||||||
|
EchoMode(huh.EchoModePassword).
|
||||||
|
Validate(func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return fmt.Errorf("password is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
err := runField(input)
|
||||||
|
handleAbort(err)
|
||||||
|
|
||||||
|
if value != "" {
|
||||||
|
// Print confirmation without revealing the password
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
fmt.Printf("%s: %s\n", prompt, "********")
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBool(prompt string, defaultValue bool) bool {
|
||||||
|
var value = defaultValue
|
||||||
|
|
||||||
|
confirm := huh.NewConfirm().
|
||||||
|
Title(prompt).
|
||||||
|
Value(&value).
|
||||||
|
Affirmative("Yes").
|
||||||
|
Negative("No")
|
||||||
|
|
||||||
|
err := runField(confirm)
|
||||||
|
handleAbort(err)
|
||||||
|
|
||||||
|
// Print the answer so it remains visible in terminal history
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
answer := "No"
|
||||||
|
if value {
|
||||||
|
answer = "Yes"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %s\n", prompt, answer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func readBoolNoDefault(prompt string) bool {
|
||||||
|
var value bool
|
||||||
|
|
||||||
|
confirm := huh.NewConfirm().
|
||||||
|
Title(prompt).
|
||||||
|
Value(&value).
|
||||||
|
Affirmative("Yes").
|
||||||
|
Negative("No")
|
||||||
|
|
||||||
|
err := runField(confirm)
|
||||||
|
handleAbort(err)
|
||||||
|
|
||||||
|
// Print the answer so it remains visible in terminal history
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
answer := "No"
|
||||||
|
if value {
|
||||||
|
answer = "Yes"
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %s\n", prompt, answer)
|
||||||
|
}
|
||||||
|
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
func readInt(prompt string, defaultValue int) int {
|
||||||
|
var value string
|
||||||
|
|
||||||
|
title := fmt.Sprintf("%s (default: %d)", prompt, defaultValue)
|
||||||
|
|
||||||
|
input := huh.NewInput().
|
||||||
|
Title(title).
|
||||||
|
Value(&value).
|
||||||
|
Validate(func(s string) error {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
_, err := strconv.Atoi(s)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("please enter a valid number")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
err := runField(input)
|
||||||
|
handleAbort(err)
|
||||||
|
|
||||||
|
if value == "" {
|
||||||
|
// Print the answer so it remains visible in terminal history
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
fmt.Printf("%s: %d\n", prompt, defaultValue)
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := strconv.Atoi(value)
|
||||||
|
if err != nil {
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
fmt.Printf("%s: %d\n", prompt, defaultValue)
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print the answer so it remains visible in terminal history
|
||||||
|
if !isAccessibleMode() {
|
||||||
|
fmt.Printf("%s: %d\n", prompt, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
178
install/main.go
@@ -1,12 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"crypto/rand"
|
||||||
"embed"
|
"embed"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math/rand"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -19,11 +19,17 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
// Version variables injected at build time via -ldflags
|
||||||
|
var (
|
||||||
|
pangolinVersion string
|
||||||
|
gerbilVersion string
|
||||||
|
badgerVersion string
|
||||||
|
)
|
||||||
|
|
||||||
func loadVersions(config *Config) {
|
func loadVersions(config *Config) {
|
||||||
config.PangolinVersion = "replaceme"
|
config.PangolinVersion = pangolinVersion
|
||||||
config.GerbilVersion = "replaceme"
|
config.GerbilVersion = gerbilVersion
|
||||||
config.BadgerVersion = "replaceme"
|
config.BadgerVersion = badgerVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:embed config/*
|
//go:embed config/*
|
||||||
@@ -49,13 +55,14 @@ type Config struct {
|
|||||||
DoCrowdsecInstall bool
|
DoCrowdsecInstall bool
|
||||||
EnableGeoblocking bool
|
EnableGeoblocking bool
|
||||||
Secret string
|
Secret string
|
||||||
|
IsEnterprise bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedContainer string
|
type SupportedContainer string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
Docker SupportedContainer = "docker"
|
Docker SupportedContainer = "docker"
|
||||||
Podman SupportedContainer = "podman"
|
Podman SupportedContainer = "podman"
|
||||||
Undefined SupportedContainer = "undefined"
|
Undefined SupportedContainer = "undefined"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -80,14 +87,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
var alreadyInstalled = false
|
var alreadyInstalled = false
|
||||||
|
|
||||||
// check if there is already a config file
|
// check if there is already a config file
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
if _, err := os.Stat("config/config.yml"); err != nil {
|
||||||
config = collectUserInput(reader)
|
config = collectUserInput()
|
||||||
|
|
||||||
loadVersions(&config)
|
loadVersions(&config)
|
||||||
config.DoCrowdsecInstall = false
|
config.DoCrowdsecInstall = false
|
||||||
@@ -100,7 +105,10 @@ func main() {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
if err := moveFile("config/docker-compose.yml", "docker-compose.yml"); err != nil {
|
||||||
|
fmt.Printf("Error moving docker-compose.yml: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\nConfiguration files created successfully!")
|
fmt.Println("\nConfiguration files created successfully!")
|
||||||
|
|
||||||
@@ -115,13 +123,17 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("\n=== Starting installation ===")
|
fmt.Println("\n=== Starting installation ===")
|
||||||
|
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
if readBool("Would you like to install and start the containers?", true) {
|
||||||
|
|
||||||
config.InstallationContainerType = podmanOrDocker(reader)
|
config.InstallationContainerType = podmanOrDocker()
|
||||||
|
|
||||||
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
||||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
if readBool("Docker is not installed. Would you like to install it?", true) {
|
||||||
installDocker()
|
if err := installDocker(); err != nil {
|
||||||
|
fmt.Printf("Error installing Docker: %v\n", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// try to start docker service but ignore errors
|
// try to start docker service but ignore errors
|
||||||
if err := startDockerService(); err != nil {
|
if err := startDockerService(); err != nil {
|
||||||
fmt.Println("Error starting Docker service:", err)
|
fmt.Println("Error starting Docker service:", err)
|
||||||
@@ -130,7 +142,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
|
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
|
||||||
fmt.Println("Waiting for Docker to start...")
|
fmt.Println("Waiting for Docker to start...")
|
||||||
for i := 0; i < 5; i++ {
|
for range 5 {
|
||||||
if isDockerRunning() {
|
if isDockerRunning() {
|
||||||
fmt.Println("Docker is running!")
|
fmt.Println("Docker is running!")
|
||||||
break
|
break
|
||||||
@@ -165,7 +177,7 @@ func main() {
|
|||||||
fmt.Println("\n=== MaxMind Database Update ===")
|
fmt.Println("\n=== MaxMind Database Update ===")
|
||||||
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
|
if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil {
|
||||||
fmt.Println("MaxMind GeoLite2 Country database found.")
|
fmt.Println("MaxMind GeoLite2 Country database found.")
|
||||||
if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) {
|
if readBool("Would you like to update the MaxMind database to the latest version?", false) {
|
||||||
if err := downloadMaxMindDatabase(); err != nil {
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
fmt.Printf("Error updating MaxMind database: %v\n", err)
|
fmt.Printf("Error updating MaxMind database: %v\n", err)
|
||||||
fmt.Println("You can try updating it manually later if needed.")
|
fmt.Println("You can try updating it manually later if needed.")
|
||||||
@@ -173,13 +185,13 @@ func main() {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("MaxMind GeoLite2 Country database not found.")
|
fmt.Println("MaxMind GeoLite2 Country database not found.")
|
||||||
if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
|
if readBool("Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) {
|
||||||
if err := downloadMaxMindDatabase(); err != nil {
|
if err := downloadMaxMindDatabase(); err != nil {
|
||||||
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
fmt.Printf("Error downloading MaxMind database: %v\n", err)
|
||||||
fmt.Println("You can try downloading it manually later if needed.")
|
fmt.Println("You can try downloading it manually later if needed.")
|
||||||
}
|
}
|
||||||
// Now you need to update your config file accordingly to enable geoblocking
|
// Now you need to update your config file accordingly to enable geoblocking
|
||||||
fmt.Println("Please remember to update your config/config.yml file to enable geoblocking! \n")
|
fmt.Print("Please remember to update your config/config.yml file to enable geoblocking! \n\n")
|
||||||
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
|
// add maxmind_db_path: "./config/GeoLite2-Country.mmdb" under server
|
||||||
fmt.Println("Add the following line under the 'server' section:")
|
fmt.Println("Add the following line under the 'server' section:")
|
||||||
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
|
fmt.Println(" maxmind_db_path: \"./config/GeoLite2-Country.mmdb\"")
|
||||||
@@ -190,11 +202,11 @@ func main() {
|
|||||||
if !checkIsCrowdsecInstalledInCompose() {
|
if !checkIsCrowdsecInstalledInCompose() {
|
||||||
fmt.Println("\n=== CrowdSec Install ===")
|
fmt.Println("\n=== CrowdSec Install ===")
|
||||||
// check if crowdsec is installed
|
// check if crowdsec is installed
|
||||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
if readBool("Would you like to install CrowdSec?", false) {
|
||||||
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
|
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
|
||||||
|
|
||||||
// BUG: crowdsec installation will be skipped if the user chooses to install on the first installation.
|
// BUG: crowdsec installation will be skipped if the user chooses to install on the first installation.
|
||||||
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
|
if readBool("Are you willing to manage CrowdSec?", false) {
|
||||||
if config.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml")
|
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -209,8 +221,8 @@ func main() {
|
|||||||
|
|
||||||
parsedURL, err := url.Parse(appConfig.DashboardURL)
|
parsedURL, err := url.Parse(appConfig.DashboardURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error parsing URL: %v\n", err)
|
fmt.Printf("Error parsing URL: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
config.DashboardDomain = parsedURL.Hostname()
|
config.DashboardDomain = parsedURL.Hostname()
|
||||||
@@ -223,12 +235,21 @@ func main() {
|
|||||||
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail)
|
||||||
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
fmt.Printf("Badger Version: %s\n", config.BadgerVersion)
|
||||||
|
|
||||||
if !readBool(reader, "Are these values correct?", true) {
|
if !readBool("Are these values correct?", true) {
|
||||||
config = collectUserInput(reader)
|
config = collectUserInput()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
config.InstallationContainerType = podmanOrDocker(reader)
|
// Try to detect container type from existing installation
|
||||||
|
detectedType := detectContainerType()
|
||||||
|
if detectedType == Undefined {
|
||||||
|
// If detection fails, prompt the user
|
||||||
|
fmt.Println("Unable to detect container type from existing installation.")
|
||||||
|
config.InstallationContainerType = podmanOrDocker()
|
||||||
|
} else {
|
||||||
|
config.InstallationContainerType = detectedType
|
||||||
|
fmt.Printf("Detected container type: %s\n", config.InstallationContainerType)
|
||||||
|
}
|
||||||
|
|
||||||
config.DoCrowdsecInstall = true
|
config.DoCrowdsecInstall = true
|
||||||
err := installCrowdsec(config)
|
err := installCrowdsec(config)
|
||||||
@@ -242,7 +263,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 ===")
|
||||||
|
|
||||||
@@ -266,8 +287,8 @@ func main() {
|
|||||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
||||||
}
|
}
|
||||||
|
|
||||||
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
func podmanOrDocker() SupportedContainer {
|
||||||
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
inputContainer := readString("Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
||||||
|
|
||||||
chosenContainer := Docker
|
chosenContainer := Docker
|
||||||
if strings.EqualFold(inputContainer, "docker") {
|
if strings.EqualFold(inputContainer, "docker") {
|
||||||
@@ -279,16 +300,17 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if chosenContainer == Podman {
|
switch chosenContainer {
|
||||||
|
case Podman:
|
||||||
if !isPodmanInstalled() {
|
if !isPodmanInstalled() {
|
||||||
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
|
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
if err := exec.Command("bash", "-c", "cat /etc/sysctl.d/99-podman.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start=' || cat /etc/sysctl.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
||||||
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
||||||
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
||||||
approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true)
|
approved := readBool("The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system\". Approve?", true)
|
||||||
if approved {
|
if approved {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
fmt.Println("You need to run the installer as root for such a configuration.")
|
fmt.Println("You need to run the installer as root for such a configuration.")
|
||||||
@@ -299,8 +321,8 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
// container low-range ports as unprivileged ports.
|
// container low-range ports as unprivileged ports.
|
||||||
// Linux only.
|
// Linux only.
|
||||||
|
|
||||||
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil {
|
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system"); err != nil {
|
||||||
fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err)
|
fmt.Printf("Error configuring unprivileged ports: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -310,7 +332,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
fmt.Println("Unprivileged ports have been configured.")
|
fmt.Println("Unprivileged ports have been configured.")
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if chosenContainer == Docker {
|
case Docker:
|
||||||
// check if docker is not installed and the user is root
|
// check if docker is not installed and the user is root
|
||||||
if !isDockerInstalled() {
|
if !isDockerInstalled() {
|
||||||
if os.Geteuid() != 0 {
|
if os.Geteuid() != 0 {
|
||||||
@@ -325,7 +347,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
} else {
|
default:
|
||||||
// This shouldn't happen unless there's a third container runtime.
|
// This shouldn't happen unless there's a third container runtime.
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -333,33 +355,35 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
|||||||
return chosenContainer
|
return chosenContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectUserInput(reader *bufio.Reader) Config {
|
func collectUserInput() Config {
|
||||||
config := Config{}
|
config := Config{}
|
||||||
|
|
||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== Basic Configuration ===")
|
fmt.Println("\n=== Basic Configuration ===")
|
||||||
|
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
config.IsEnterprise = readBoolNoDefault("Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.")
|
||||||
|
|
||||||
|
config.BaseDomain = readString("Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
|
|
||||||
// Set default dashboard domain after base domain is collected
|
// Set default dashboard domain after base domain is collected
|
||||||
defaultDashboardDomain := ""
|
defaultDashboardDomain := ""
|
||||||
if config.BaseDomain != "" {
|
if config.BaseDomain != "" {
|
||||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
||||||
}
|
}
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
config.DashboardDomain = readString("Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
config.LetsEncryptEmail = readString("Enter email for Let's Encrypt certificates", "")
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
config.InstallGerbil = readBool("Do you want to use Gerbil to allow tunneled connections", true)
|
||||||
|
|
||||||
// Email configuration
|
// Email configuration
|
||||||
fmt.Println("\n=== Email Configuration ===")
|
fmt.Println("\n=== Email Configuration ===")
|
||||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
config.EnableEmail = readBool("Enable email functionality (SMTP)", false)
|
||||||
|
|
||||||
if config.EnableEmail {
|
if config.EnableEmail {
|
||||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
config.EmailSMTPHost = readString("Enter SMTP host", "")
|
||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
config.EmailSMTPPort = readInt("Enter SMTP port (default 587)", 587)
|
||||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
config.EmailSMTPUser = readString("Enter SMTP username", "")
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
config.EmailSMTPPass = readPassword("Enter SMTP password")
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
config.EmailNoReply = readString("Enter no-reply email address (often the same as SMTP username)", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
@@ -371,13 +395,17 @@ 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
|
||||||
|
|
||||||
fmt.Println("\n=== Advanced Configuration ===")
|
fmt.Println("\n=== Advanced Configuration ===")
|
||||||
|
|
||||||
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
config.EnableIPv6 = readBool("Is your server IPv6 capable?", true)
|
||||||
config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true)
|
config.EnableGeoblocking = readBool("Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true)
|
||||||
|
|
||||||
if config.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
fmt.Println("Error: Dashboard Domain name is required")
|
fmt.Println("Error: Dashboard Domain name is required")
|
||||||
@@ -388,10 +416,18 @@ func collectUserInput(reader *bufio.Reader) Config {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createConfigFiles(config Config) error {
|
func createConfigFiles(config Config) error {
|
||||||
os.MkdirAll("config", 0755)
|
if err := os.MkdirAll("config", 0755); err != nil {
|
||||||
os.MkdirAll("config/letsencrypt", 0755)
|
return fmt.Errorf("failed to create config directory: %v", err)
|
||||||
os.MkdirAll("config/db", 0755)
|
}
|
||||||
os.MkdirAll("config/logs", 0755)
|
if err := os.MkdirAll("config/letsencrypt", 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create letsencrypt directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll("config/db", 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create db directory: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll("config/logs", 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create logs directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Walk through all embedded files
|
// Walk through all embedded files
|
||||||
err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, err error) error {
|
err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, err error) error {
|
||||||
@@ -545,22 +581,24 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai
|
|||||||
fmt.Println("To get your setup token, you need to:")
|
fmt.Println("To get your setup token, you need to:")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("1. Start the containers")
|
fmt.Println("1. Start the containers")
|
||||||
if containerType == Docker {
|
switch containerType {
|
||||||
|
case Docker:
|
||||||
fmt.Println(" docker compose up -d")
|
fmt.Println(" docker compose up -d")
|
||||||
} else if containerType == Podman {
|
case Podman:
|
||||||
fmt.Println(" podman-compose up -d")
|
fmt.Println(" podman-compose up -d")
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("2. Wait for the Pangolin container to start and generate the token")
|
fmt.Println("2. Wait for the Pangolin container to start and generate the token")
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("3. Check the container logs for the setup token")
|
fmt.Println("3. Check the container logs for the setup token")
|
||||||
if containerType == Docker {
|
switch containerType {
|
||||||
|
case Docker:
|
||||||
fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
||||||
} else if containerType == Podman {
|
case Podman:
|
||||||
fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
||||||
} else {
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("")
|
fmt.Println("")
|
||||||
fmt.Println("4. Look for output like")
|
fmt.Println("4. Look for output like")
|
||||||
fmt.Println(" === SETUP TOKEN GENERATED ===")
|
fmt.Println(" === SETUP TOKEN GENERATED ===")
|
||||||
@@ -576,17 +614,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 {
|
||||||
@@ -627,10 +660,7 @@ func checkPortsAvailable(port int) error {
|
|||||||
addr := fmt.Sprintf(":%d", port)
|
addr := fmt.Sprintf(":%d", port)
|
||||||
ln, err := net.Listen("tcp", addr)
|
ln, err := net.Listen("tcp", addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(
|
return fmt.Errorf("ERROR: port %d is occupied or cannot be bound: %w", port, err)
|
||||||
"ERROR: port %d is occupied or cannot be bound: %w\n\n",
|
|
||||||
port, err,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if closeErr := ln.Close(); closeErr != nil {
|
if closeErr := ln.Close(); closeErr != nil {
|
||||||
fmt.Fprintf(os.Stderr,
|
fmt.Fprintf(os.Stderr,
|
||||||
|
|||||||
51
install/theme.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/charmbracelet/huh"
|
||||||
|
"github.com/charmbracelet/lipgloss"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Pangolin brand colors (converted from oklch to hex)
|
||||||
|
var (
|
||||||
|
// Primary orange/amber - oklch(0.6717 0.1946 41.93)
|
||||||
|
primaryColor = lipgloss.AdaptiveColor{Light: "#D97706", Dark: "#F59E0B"}
|
||||||
|
// Muted foreground
|
||||||
|
mutedColor = lipgloss.AdaptiveColor{Light: "#737373", Dark: "#A3A3A3"}
|
||||||
|
// Success green
|
||||||
|
successColor = lipgloss.AdaptiveColor{Light: "#16A34A", Dark: "#22C55E"}
|
||||||
|
// Error red - oklch(0.577 0.245 27.325)
|
||||||
|
errorColor = lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#EF4444"}
|
||||||
|
// Normal text
|
||||||
|
normalFg = lipgloss.AdaptiveColor{Light: "#171717", Dark: "#FAFAFA"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// ThemePangolin returns a huh theme using Pangolin brand colors
|
||||||
|
func ThemePangolin() *huh.Theme {
|
||||||
|
t := huh.ThemeBase()
|
||||||
|
|
||||||
|
// Focused state styles
|
||||||
|
t.Focused.Base = t.Focused.Base.BorderForeground(primaryColor)
|
||||||
|
t.Focused.Title = t.Focused.Title.Foreground(primaryColor).Bold(true)
|
||||||
|
t.Focused.Description = t.Focused.Description.Foreground(mutedColor)
|
||||||
|
t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errorColor)
|
||||||
|
t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errorColor)
|
||||||
|
t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(primaryColor)
|
||||||
|
t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(primaryColor)
|
||||||
|
t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(primaryColor)
|
||||||
|
t.Focused.Option = t.Focused.Option.Foreground(normalFg)
|
||||||
|
t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(primaryColor)
|
||||||
|
t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(successColor).SetString("✓ ")
|
||||||
|
t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(mutedColor).SetString(" ")
|
||||||
|
t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(lipgloss.Color("#FFFFFF")).Background(primaryColor)
|
||||||
|
t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "#E5E5E5", Dark: "#404040"})
|
||||||
|
t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(primaryColor)
|
||||||
|
t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(primaryColor)
|
||||||
|
|
||||||
|
// Blurred state inherits from focused but with hidden border
|
||||||
|
t.Blurred = t.Focused
|
||||||
|
t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder())
|
||||||
|
t.Blurred.Title = t.Blurred.Title.Foreground(mutedColor).Bold(false)
|
||||||
|
t.Blurred.TextInput.Prompt = t.Blurred.TextInput.Prompt.Foreground(mutedColor)
|
||||||
|
|
||||||
|
return t
|
||||||
|
}
|
||||||
4485
messages/zh-TW.json
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
11730
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.1",
|
||||||
"@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.8",
|
||||||
"@react-email/render": "^1.3.2",
|
"@react-email/render": "2.0.4",
|
||||||
"@react-email/tailwind": "1.2.2",
|
"@react-email/tailwind": "2.0.5",
|
||||||
"@simplewebauthn/browser": "^13.2.2",
|
"@simplewebauthn/browser": "13.2.2",
|
||||||
"@simplewebauthn/server": "^13.2.2",
|
"@simplewebauthn/server": "13.2.3",
|
||||||
"@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.6",
|
||||||
"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.3",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nextjs-toploader": "^3.9.17",
|
"nextjs-toploader": "3.9.17",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-fetch": "3.3.2",
|
"nodemailer": "8.0.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.19.0",
|
||||||
"posthog-node": "^5.11.2",
|
"posthog-node": "5.26.0",
|
||||||
"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.2",
|
||||||
"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.5.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.11",
|
||||||
"@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(
|
||||||
message: `Your password must meet the following conditions:
|
/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[~!`@#$%^&*()_\-+={}[\]|\\:;"'<>,.\/?]).*$/,
|
||||||
|
{
|
||||||
|
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.`
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -3,7 +3,14 @@ import {
|
|||||||
encodeHexLowerCase
|
encodeHexLowerCase
|
||||||
} from "@oslojs/encoding";
|
} from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
import { resourceSessions, Session, sessions, User, users } from "@server/db";
|
import {
|
||||||
|
resourceSessions,
|
||||||
|
safeRead,
|
||||||
|
Session,
|
||||||
|
sessions,
|
||||||
|
User,
|
||||||
|
users
|
||||||
|
} from "@server/db";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { eq, inArray } from "drizzle-orm";
|
import { eq, inArray } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
@@ -54,11 +61,15 @@ export async function validateSessionToken(
|
|||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const result = await db
|
|
||||||
.select({ user: users, session: sessions })
|
const result = await safeRead((db) =>
|
||||||
.from(sessions)
|
db
|
||||||
.innerJoin(users, eq(sessions.userId, users.userId))
|
.select({ user: users, session: sessions })
|
||||||
.where(eq(sessions.sessionId, sessionId));
|
.from(sessions)
|
||||||
|
.innerJoin(users, eq(sessions.userId, users.userId))
|
||||||
|
.where(eq(sessions.sessionId, sessionId))
|
||||||
|
);
|
||||||
|
|
||||||
if (result.length < 1) {
|
if (result.length < 1) {
|
||||||
return { session: null, user: null };
|
return { session: null, user: null };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,7 +1,7 @@
|
|||||||
import { encodeHexLowerCase } from "@oslojs/encoding";
|
import { encodeHexLowerCase } from "@oslojs/encoding";
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
import { sha256 } from "@oslojs/crypto/sha2";
|
||||||
import { resourceSessions, ResourceSession } from "@server/db";
|
import { resourceSessions, ResourceSession } from "@server/db";
|
||||||
import { db } from "@server/db";
|
import { db, safeRead } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
@@ -66,15 +66,17 @@ export async function validateResourceSessionToken(
|
|||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
const result = await db
|
const result = await safeRead((db) =>
|
||||||
.select()
|
db
|
||||||
.from(resourceSessions)
|
.select()
|
||||||
.where(
|
.from(resourceSessions)
|
||||||
and(
|
.where(
|
||||||
eq(resourceSessions.sessionId, sessionId),
|
and(
|
||||||
eq(resourceSessions.resourceId, resourceId)
|
eq(resourceSessions.sessionId, sessionId),
|
||||||
|
eq(resourceSessions.resourceId, resourceId)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result.length < 1) {
|
if (result.length < 1) {
|
||||||
return { resourceSession: null };
|
return { resourceSession: null };
|
||||||
@@ -85,7 +87,7 @@ export async function validateResourceSessionToken(
|
|||||||
if (Date.now() >= resourceSession.expiresAt) {
|
if (Date.now() >= resourceSession.expiresAt) {
|
||||||
await db
|
await db
|
||||||
.delete(resourceSessions)
|
.delete(resourceSessions)
|
||||||
.where(eq(resourceSessions.sessionId, resourceSessions.sessionId));
|
.where(eq(resourceSessions.sessionId, sessionId));
|
||||||
return { resourceSession: null };
|
return { resourceSession: null };
|
||||||
} else if (
|
} else if (
|
||||||
Date.now() >=
|
Date.now() >=
|
||||||
@@ -179,7 +181,7 @@ export function serializeResourceSessionCookie(
|
|||||||
return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${domain}`;
|
return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${domain}`;
|
||||||
} else {
|
} else {
|
||||||
if (expiresAt === undefined) {
|
if (expiresAt === undefined) {
|
||||||
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=$domain}`;
|
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${domain}`;
|
||||||
}
|
}
|
||||||
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${domain}`;
|
return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${domain}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,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,28 +6,28 @@ 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(",").map(
|
||||||
process.env.POSTGRES_REPLICA_CONNECTION_STRINGS.split(
|
(conn) => ({
|
||||||
","
|
|
||||||
).map((conn) => ({
|
|
||||||
connection_string: conn.trim()
|
connection_string: conn.trim()
|
||||||
}));
|
})
|
||||||
config.postgres.replicas = replicas;
|
);
|
||||||
}
|
config.postgres.replicas = replicas;
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
"Postgres configuration is missing in the configuration file."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!config.postgres) {
|
||||||
|
throw new Error(
|
||||||
|
"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,6 @@
|
|||||||
export * from "./driver";
|
export * from "./driver";
|
||||||
|
export * from "./logsDriver";
|
||||||
|
export * from "./safeRead";
|
||||||
export * from "./schema/schema";
|
export * from "./schema/schema";
|
||||||
export * from "./schema/privateSchema";
|
export * from "./schema/privateSchema";
|
||||||
|
export * from "./migrate";
|
||||||
|
|||||||
87
server/db/pg/logsDriver.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
import { readConfigFile } from "@server/lib/readConfigFile";
|
||||||
|
import { withReplicas } from "drizzle-orm/pg-core";
|
||||||
|
import { build } from "@server/build";
|
||||||
|
import { db as mainDb, primaryDb as mainPrimaryDb } from "./driver";
|
||||||
|
|
||||||
|
function createLogsDb() {
|
||||||
|
// Only use separate logs database in SaaS builds
|
||||||
|
if (build !== "saas") {
|
||||||
|
return mainDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = readConfigFile();
|
||||||
|
|
||||||
|
// Merge configs, prioritizing private config
|
||||||
|
const logsConfig = config.postgres_logs;
|
||||||
|
|
||||||
|
// Check environment variable first
|
||||||
|
let connectionString = process.env.POSTGRES_LOGS_CONNECTION_STRING;
|
||||||
|
let replicaConnections: Array<{ connection_string: string }> = [];
|
||||||
|
|
||||||
|
if (!connectionString && logsConfig) {
|
||||||
|
connectionString = logsConfig.connection_string;
|
||||||
|
replicaConnections = logsConfig.replicas || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS is set, use it
|
||||||
|
if (process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS) {
|
||||||
|
replicaConnections =
|
||||||
|
process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS.split(",").map(
|
||||||
|
(conn) => ({
|
||||||
|
connection_string: conn.trim()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no logs database is configured, fall back to main database
|
||||||
|
if (!connectionString) {
|
||||||
|
return mainDb;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create separate connection pool for logs database
|
||||||
|
const poolConfig = logsConfig?.pool || config.postgres?.pool;
|
||||||
|
const primaryPool = new Pool({
|
||||||
|
connectionString,
|
||||||
|
max: poolConfig?.max_connections || 20,
|
||||||
|
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||||
|
connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000
|
||||||
|
});
|
||||||
|
|
||||||
|
const replicas = [];
|
||||||
|
|
||||||
|
if (!replicaConnections.length) {
|
||||||
|
replicas.push(
|
||||||
|
DrizzlePostgres(primaryPool, {
|
||||||
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
for (const conn of replicaConnections) {
|
||||||
|
const replicaPool = new Pool({
|
||||||
|
connectionString: conn.connection_string,
|
||||||
|
max: poolConfig?.max_replica_connections || 20,
|
||||||
|
idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000,
|
||||||
|
connectionTimeoutMillis:
|
||||||
|
poolConfig?.connection_timeout_ms || 5000
|
||||||
|
});
|
||||||
|
replicas.push(
|
||||||
|
DrizzlePostgres(replicaPool, {
|
||||||
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return withReplicas(
|
||||||
|
DrizzlePostgres(primaryPool, {
|
||||||
|
logger: process.env.QUERY_LOGGING == "true"
|
||||||
|
}),
|
||||||
|
replicas as any
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const logsDb = createLogsDb();
|
||||||
|
export default logsDb;
|
||||||
|
export const primaryLogsDb = logsDb.$primary;
|
||||||