Compare commits

..

452 commits

Author SHA1 Message Date
Yann Stepienik
7a3fdfb467 [release] v0.13.1 2023-11-28 10:15:24 +00:00
Yann Stepienik
d91b0cea85 [skip ci] readme 2023-11-25 11:04:15 +00:00
Mikal S
6c1dd0dbf6
[skip ci] Update readme.md (#146)
* Update readme.md

I noticed a spelling mistake and proceeded to look through the whole read-me file. There are actually a lot of other less grammar related mistakes I would want to change, but I tried to keep it strictly to grammar mistakes… for now. The exception is the disclaimer that **should not** be hidden away in a scroll-able code block. You can hide it at the bottom of the section, but please don't try to hide it out of view.

* Sign .clabot so I can merge this PR.

I still don't know why I need to sign this CLA when I haven't even contributed any code, but sure, I'll do it. Also, to the person reading these commit messages; hello.
2023-11-25 11:02:36 +00:00
Yann Stepienik
281c8c1ea4 [release] v0.13.0 2023-11-25 11:00:56 +00:00
Yann Stepienik
cc7aaf17f2 [release] v0.13.0-unstable11 2023-11-25 00:40:18 +00:00
Yann Stepienik
6e06daa531 [release] v0.13.0-unstable10 2023-11-24 23:57:04 +00:00
Yann Stepienik
eae11eb3ce [release] v0.13.0-unstable9 2023-11-24 13:44:13 +00:00
Yann Stepienik
9085b2587c [release] v0.13.0-unstable8 2023-11-24 13:03:23 +00:00
Yann Stepienik
49d7dd7d4a [release] v0.13.0-unstable7 2023-11-23 21:11:38 +00:00
Yann Stepienik
9d133e40e6 [release] v0.13.0-unstable6 2023-11-23 20:37:30 +00:00
Yann Stepienik
23ab2f7a97 [release] v0.13.0-unstable5 2023-11-23 20:26:05 +00:00
Yann Stepienik
20c1d18ac8 [release] v0.13.0-unstable4 2023-11-21 20:55:18 +00:00
Yann Stepienik
9aaba072e8 [release] v0.13.0-unstable3 2023-11-21 20:21:13 +00:00
Yann Stepienik
3e77d19f84 [release] v0.13.0-unstable2 2023-11-21 19:59:54 +00:00
Yann Stepienik
768dc1b06c [release] v0.13.0-unstable1 2023-11-21 19:48:24 +00:00
Yann Stepienik
4884c95a50 [release] v0.13.0-unstable0 2023-11-21 19:32:02 +00:00
Yann Stepienik
320b29df55 [release] v0.12.6 2023-11-15 15:50:12 +00:00
Yann Stepienik
9644600f0d [release] v0.12.6-unstable 2023-11-15 15:32:26 +00:00
Yann Stepienik
3d2932f385 [skip ci] readme 2023-11-15 11:07:26 +00:00
Yann Stepienik
7ade6fcb9c [release] v0.12.5 2023-11-14 11:16:30 +00:00
Yann Stepienik
ec66cc7692 [release] v0.12.4 2023-11-13 15:22:56 +00:00
Yann Stepienik
585780e26f [release] v0.12.3 2023-11-13 14:17:02 +00:00
Yann Stepienik
411a9cc57a [skip ci] changelog 2023-11-12 13:50:12 +00:00
Yann Stepienik
fb5cc7bc3c [skip ci] changelog 2023-11-09 12:00:13 +00:00
Yann Stepienik
9edf3e5092 [release] v0.12.2 2023-11-09 11:51:11 +00:00
Hoang Nguyen
df0f19bec7
[release] v0.12.2
* Mitigate open redirect vulnerability in login page

Current implementation won't allow full url redirection within local origin, and will allow open redirection with href like "//google.com".
Comparing redirect url's origin with current origin will ensure the two share the same protocol, hostname, and port.

* Update .clabot

Add catmandx to list of contributors for creating pull request
2023-11-09 11:40:26 +00:00
Yann Stepienik
62e0050147 [release] v0.12.1 2023-11-08 18:55:17 +00:00
Yann Stepienik
456a74100c [release] v0.12.0 2023-11-08 12:45:38 +00:00
Yann Stepienik
bb5b166d36 [skip ci] changelog 2023-11-08 12:41:38 +00:00
Yann Stepienik
161b39521b [release] v0.12.0 2023-11-08 12:37:20 +00:00
Yann Stepienik
253f95c186 [release] v0.12.0-unstable51 2023-11-07 22:21:16 +00:00
Yann Stepienik
0faa1133fc [release] v0.12.0-unstable50 2023-11-07 20:02:49 +00:00
Yann Stepienik
2c9c28253f
[skip ci] Update changelog.md 2023-11-07 18:31:42 +00:00
Yann Stepienik
2505c930f7 [release] v0.12.0-unstable49 2023-11-07 18:29:30 +00:00
Someone
714d560774
[skip ci] Add AutoFocus on Token field for 2FA Authentication (#119)
* Allow Insecure TLS like self-signed certificate for SMTP server

* Set AutoFocus on Token field for 2FA Authentication

Set AutoFocus on Token field for 2FA Authentication
2023-11-07 18:27:48 +00:00
Someone
923fa48a97
[skip ci] Allow Insecure TLS like self-signed certificate for SMTP server (#118) 2023-11-07 18:27:27 +00:00
Yann Stepienik
dccb7533a2 [release] v0.12.0-unstable48 2023-11-07 17:00:26 +00:00
Yann Stepienik
9ec6784b26 [release] v0.12.0-unstable47 2023-11-07 16:26:51 +00:00
Yann Stepienik
2daf467650 [release] v0.12.0-unstable46 2023-11-06 21:24:05 +00:00
Yann Stepienik
902c823d7d [release] v0.12.0-unstable45 2023-11-06 21:09:09 +00:00
Yann Stepienik
133589b1af [release] v0.12.0-unstable44 2023-11-06 20:11:15 +00:00
Yann Stepienik
73196dadf6 [release] v0.12.0-unstable44 2023-11-06 20:10:59 +00:00
Yann Stepienik
050fe7484b [release] v0.12.0-unstable43 2023-11-06 19:57:04 +00:00
Yann Stepienik
34bc76dbf8 [release] v0.12.0-unstable42 2023-11-05 20:34:17 +00:00
Yann Stepienik
aa963bb89f [release] v0.12.0-unstable41 2023-11-05 15:16:57 +00:00
Yann Stepienik
3abd0ee6ea [release] v0.12.0-unstable40 2023-11-03 17:54:07 +00:00
Yann Stepienik
57e6680668 [release] v0.12.0-unstable39 2023-11-03 16:24:28 +00:00
Yann Stepienik
6d275216de [release] v0.12.0-unstable38 2023-11-03 15:03:16 +00:00
Yann Stepienik
cc7cab467d [release] v0.12.0-unstable37 2023-11-03 14:07:05 +00:00
Yann Stepienik
798505f7a7 [release] v0.12.0-unstable36 2023-11-03 11:42:12 +00:00
Yann Stepienik
19d69d6dd2 [release] v0.12.0-unstable35 2023-11-02 23:22:08 +00:00
Yann Stepienik
733745944a [release] v0.12.0-unstable34 2023-11-02 22:00:01 +00:00
Yann Stepienik
5b8622ff57 [release] v0.12.0-unstable33 2023-11-02 16:52:27 +00:00
Yann Stepienik
6a5f9b74fe [release] v0.12.0-unstable32 2023-11-02 10:39:47 +00:00
Yann Stepienik
bdfcf0a2bf [release] v0.12.0-unstable31 2023-11-01 21:17:16 +00:00
Yann Stepienik
0daf229b56 [release] v0.12.0-unstable30 2023-11-01 20:39:29 +00:00
Yann Stepienik
53db1b883d cp 2023-11-01 13:58:25 +00:00
Yann Stepienik
d68f9df66f [release] v0.12.0-unstable29 2023-10-31 22:09:53 +00:00
Yann Stepienik
ca67ebbb45 [release] v0.12.0-unstable28 2023-10-31 21:09:26 +00:00
Yann Stepienik
6f0e37c524 [release] v0.12.0-unstable27 2023-10-31 20:19:14 +00:00
Yann Stepienik
ba67c90714 [release] v0.12.0-unstable26 2023-10-31 19:59:39 +00:00
Yann Stepienik
f6b322a92a [release] v0.12.0-unstable25 2023-10-31 19:35:50 +00:00
Yann Stepienik
10fe2d7f25 [release] v0.12.0-unstable24 2023-10-31 19:05:36 +00:00
Yann Stepienik
d247708d73 [release] v0.12.0-unstable23 2023-10-30 23:05:21 +00:00
Yann Stepienik
b8ad765d1b [release] v0.12.0-unstable22 2023-10-30 22:43:04 +00:00
Yann Stepienik
b50d17eae3 [release] v0.12.0-unstable21 2023-10-30 22:35:43 +00:00
Yann Stepienik
b5f4ce08ea [release] v0.12.0-unstable20 2023-10-30 22:24:54 +00:00
Yann Stepienik
15d8fdd404 [release] v0.12.0-unstable19 2023-10-29 16:19:43 +00:00
Yann Stepienik
4556bca285 [release] v0.12.0-unstable18 2023-10-29 16:06:43 +00:00
Yann Stepienik
4a83715d39 [release] v0.12.0-unstable17 2023-10-29 15:59:53 +00:00
Yann Stepienik
a578ec8987 [release] v0.12.0-unstable16 2023-10-29 15:09:40 +00:00
Yann Stepienik
7c1238b492 [release] v0.12.0-unstable16 2023-10-29 15:08:21 +00:00
Yann Stepienik
e380c80591 [release] v0.12.0-unstable15 2023-10-29 13:28:59 +00:00
Yann Stepienik
d7cf669583 [release] v0.12.0-unstable14 2023-10-29 13:13:34 +00:00
Yann Stepienik
af4ecbef41 [release] v0.12.0-unstable13 2023-10-29 13:01:12 +00:00
Yann Stepienik
5c1b7e5d74 [release] v0.12.0-unstable12 2023-10-28 17:12:11 +01:00
Yann Stepienik
1075f832c9 [release] v0.12.0-unstable12 2023-10-28 17:02:31 +01:00
Yann Stepienik
0f49682376 [release] v0.12.0-unstable11 2023-10-28 16:18:29 +01:00
Yann Stepienik
379c574358 [release] v0.12.0-unstable10 2023-10-28 16:10:39 +01:00
Yann Stepienik
2c5ec16653 [release] v0.12.0-unstable9 2023-10-28 15:51:35 +01:00
Yann Stepienik
1d650b3c4e [release] v0.12.0-unstable8 2023-10-28 15:46:25 +01:00
Yann Stepienik
d3a4d615b2 [release] v0.12.0-unstable7 2023-10-28 15:37:25 +01:00
Yann Stepienik
9d1cd315bc [release] v0.12.0-unstable6 2023-10-28 15:07:57 +01:00
Yann Stepienik
5e26d6ad90 [release] v0.12.0-unstable6 2023-10-28 15:05:12 +01:00
Yann Stepienik
33590ce369 [release] v0.12.0-unstable5 2023-10-28 13:44:14 +01:00
Yann Stepienik
8a547e1b9c [release] v0.12.0-unstable4 2023-10-26 16:23:02 +01:00
Yann Stepienik
6aa8a9e5fa [release] v0.12.0-unstable3 2023-10-26 15:51:41 +01:00
Yann Stepienik
6ba22dc4cb [release] v0.12.0-unstable2 2023-10-26 15:40:07 +01:00
Yann Stepienik
af30f256f1 [release] v0.12.0-unstable 2023-10-25 21:48:56 +01:00
Yann Stepienik
d80c37e714 [release] v0.12.0-unstable 2023-10-25 21:17:19 +01:00
Yann Stepienik
8a5e261bd1 [release] v0.11.3 2023-10-24 12:04:02 +01:00
Yann Stepienik
72436c7b78 [release] v0.11.2 2023-10-24 11:03:13 +01:00
Yann Stepienik
e80b202b6c [release] v0.11.2 2023-10-24 11:01:10 +01:00
Yann Stepienik
4d5a148278 [release] v0.11.1 2023-10-22 14:56:57 +01:00
Yann Stepienik
14bc328a66 [release] v0.11.0 2023-10-22 12:38:24 +01:00
Yann Stepienik
498112cb16 [release] v0.11.0-unstable5 2023-10-22 12:25:54 +01:00
Yann Stepienik
7f5f87e2b6 [release] v0.11.0-unstable5 2023-10-22 12:18:24 +01:00
Yann Stepienik
37c7a61db9 [release] v0.11.0-unstable4 2023-10-22 12:06:10 +01:00
Yann Stepienik
22a25ee1e8 [release] v0.11.0-unstable3 2023-10-21 14:48:38 +01:00
Yann Stepienik
2440a0a301 [release] v0.11.0-unstable2 2023-10-21 14:10:02 +01:00
Yann Stepienik
a854cd9bd7 [release] v0.11.0-unstable2 2023-10-21 13:49:30 +01:00
Yann Stepienik
7e40039901 [release] v0.11.0-unstable 2023-10-21 13:24:28 +01:00
Yann Stepienik
f0b810f480 [release] v0.10.4 2023-10-14 23:46:46 +01:00
Yann Stepienik
1fdeb258d0 [release] v0.10.4-unstable4 2023-10-14 23:39:17 +01:00
Yann Stepienik
1ce42346bc [release] v0.10.4-unstable4 2023-10-14 23:34:08 +01:00
Yann Stepienik
1a41872613 [release] v0.10.4-unstable3 2023-10-14 23:18:46 +01:00
Yann Stepienik
8d127c3d3f [release] v0.10.4-unstable2 2023-10-14 23:14:49 +01:00
Yann Stepienik
3f39200214 [release] v0.10.4-unstable 2023-10-14 22:37:18 +01:00
Yann Stepienik
744b98b3db [release] v0.10.3 2023-10-13 15:44:16 +01:00
Yann Stepienik
dd1305290a [skip ci] changelog 2023-10-09 10:50:55 +01:00
Yann Stepienik
93ca8bd79d [release] v0.10.2 2023-10-09 10:50:27 +01:00
Yann Stepienik
3dc4866775 [skip ci] changelog 2023-10-07 22:58:58 +01:00
Yann Stepienik
4166e2d2d1 [release] v0.10.1 2023-10-07 22:58:02 +01:00
Yann Stepienik
112d1aaa99 [release] v0.10.1-unstable3 2023-10-07 22:55:18 +01:00
Yann Stepienik
a732130ca1 [release] v0.10.1-unstable3 2023-10-07 22:48:44 +01:00
Yann Stepienik
2bdc2952d6 [release] v0.10.1-unstable2 2023-10-07 22:18:17 +01:00
Yann Stepienik
df27afb694 [release] v0.10.1-unstable 2023-10-07 22:03:28 +01:00
Yann Stepienik
50dadfd9e8 [skip ci] readme 2023-10-07 16:10:40 +01:00
Yann Stepienik
0442951a71 [skip ci] v0.10.0 2023-10-07 16:02:01 +01:00
Yann Stepienik
4b3c7d8be9 [release] v0.10.0 2023-10-07 15:55:37 +01:00
Yann Stepienik
f796c521ef Merge branch 'unstable' 2023-10-07 15:54:47 +01:00
Yann Stepienik
4436a56297 [release] v0.10.0-unstable31 2023-10-07 15:54:07 +01:00
Yann Stepienik
c779d47537 [release] v0.10.0-unstable30 2023-10-07 15:54:07 +01:00
Yann Stepienik
dd61551566 [release] v0.10.0-unstable29 2023-10-07 15:54:07 +01:00
Yann Stepienik
9fe6e2c95e [release] v0.10.0-unstable28 2023-10-07 15:54:06 +01:00
Yann Stepienik
2220b03847 [release] v0.10.0-unstable27 2023-10-07 15:54:06 +01:00
Yann Stepienik
443f584cb3 [release] v0.10.0-unstable26 2023-10-07 15:54:06 +01:00
Yann Stepienik
b57969fd6d [release] v0.10.0-unstable25 2023-10-07 15:54:06 +01:00
Yann Stepienik
e0bf591081 [release] v0.10.0-unstable25 2023-10-07 15:54:06 +01:00
Yann Stepienik
a9f83a37b4 [release] v0.10.0-unstable24 2023-10-07 15:54:06 +01:00
Yann Stepienik
a3df4704c4 [release] v0.10.0-unstable23 2023-10-07 15:54:05 +01:00
Yann Stepienik
4f7421e641 [release] v0.10.0-unstable22 2023-10-07 15:54:05 +01:00
Yann Stepienik
1f7c588a2b [release] v0.10.0-unstable21 2023-10-07 15:54:05 +01:00
Yann Stepienik
3f7527b6ef [release] v0.10.0-unstable20 2023-10-07 15:54:05 +01:00
Yann Stepienik
cf8a29c1aa [release] v0.10.0-unstable19 2023-10-07 15:54:04 +01:00
Yann Stepienik
8d359ca008 [release] v0.10.0-unstable18 2023-10-07 15:54:04 +01:00
Yann Stepienik
c7dda3db6c [release] v0.10.0-unstable17 2023-10-07 15:54:04 +01:00
Yann Stepienik
9b033696e3 [release] v0.10.0-unstable16 2023-10-07 15:54:03 +01:00
Yann Stepienik
a6b96bc42a [release] v0.10.0-unstable15 2023-10-07 15:54:02 +01:00
Yann Stepienik
ef37940742 [release] v0.10.0-unstable14 2023-10-07 15:53:02 +01:00
Yann Stepienik
c912f16640 [skip ci] stuff 2023-10-07 15:53:02 +01:00
Yann Stepienik
a57dbc806b [release] v0.10.0-unstable13 2023-10-07 15:53:02 +01:00
Yann Stepienik
07e7f634b7 [release] v0.10.0-unstable12 2023-10-07 15:53:02 +01:00
Yann Stepienik
0af998d06c [release] v0.10.0-unstable12 2023-10-07 15:53:02 +01:00
Yann Stepienik
2e728b3ac3 [release] v0.10.0-unstable11 2023-10-07 15:53:01 +01:00
Yann Stepienik
0479504558 [release] v0.10.0-unstable10 2023-10-07 15:53:01 +01:00
Yann Stepienik
0142c6fb11 [release] v0.10.0-unstable9 2023-10-07 15:53:01 +01:00
Yann Stepienik
9206dbade8 [release] v0.10.0-unstable8 2023-10-07 15:53:01 +01:00
Yann Stepienik
735874006c [release] v0.10.0-unstable7 2023-10-07 15:53:00 +01:00
Yann Stepienik
ff90bdf51e [release] v0.10.0-unstable6 2023-10-07 15:53:00 +01:00
Yann Stepienik
da94e1dfee [release] v0.10.0-unstable5 2023-10-07 15:53:00 +01:00
Yann Stepienik
84335be674 [release] v0.10.0-unstable4 2023-10-07 15:53:00 +01:00
Yann Stepienik
486291871e [release] v0.10.0-unstable4 2023-10-07 15:52:59 +01:00
Yann Stepienik
bb11cbbe96 [release] v0.10.0-unstable3 2023-10-07 15:52:59 +01:00
Yann Stepienik
8e83fde02e [release] v0.10.0-unstable2 2023-10-07 15:52:59 +01:00
Yann Stepienik
f127e5a146 [release] v0.10.0-unstable1 2023-10-07 15:52:59 +01:00
Yann Stepienik
bc7aaa21d0 [release] v0.10.0-unstable 2023-10-07 15:52:54 +01:00
John Wesley
0046b5b65a
[skip ci] Fix link to documentation in nav (#88) 2023-09-08 20:44:56 +01:00
Yann Stepienik
525146a210 [release] v0.9.21 2023-08-26 14:50:07 +01:00
Yann Stepienik
75c0521ad9 [release] v0.9.20 2023-08-26 14:38:05 +01:00
Yann Stepienik
bbe3e7483e [release] v0.9.19 2023-08-11 15:50:15 +01:00
Yann Stepienik
7e38f5b78e [release] v0.9.19-unstable 2023-08-11 12:58:16 +01:00
Yann Stepienik
43a526fb61 [release] v0.9.19-unstable 2023-08-11 11:00:07 +01:00
Yann Stepienik
425747234b [release] v0.9.19-unstable 2023-08-11 10:46:22 +01:00
Yann Stepienik
ac560df6b3 [release] v0.9.19-unstable 2023-08-11 10:35:01 +01:00
Yann Stepienik
25707fafff [release] v0.9.19-unstable 2023-08-11 10:32:23 +01:00
Yann Stepienik
6398916769 [release] v0.9.19-unstable 2023-08-11 10:28:49 +01:00
Yann Stepienik
8265dedd95 [release] v0.9.19-unstable 2023-08-11 10:27:59 +01:00
Yann Stepienik
2fcf44fbca [release] v0.9.19-unstable 2023-08-11 10:26:53 +01:00
Yann Stepienik
245c00fecb [release] v0.9.19-unstable 2023-08-11 10:24:50 +01:00
Yann Stepienik
f7ee52dbb3 [release] v0.9.19-unstable 2023-08-11 10:18:24 +01:00
Yann Stepienik
60be289c76 [release] v0.9.18 2023-08-10 18:56:23 +01:00
Yann Stepienik
532795e938 [release] v0.9.17 2023-08-10 16:53:12 +01:00
Yann Stepienik
bc3eeb6bd6 [release] v0.9.16 2023-07-29 12:19:54 +01:00
Yann Stepienik
b63aad4960 [skip ci] using default 2023-07-28 10:34:49 +01:00
Yann Stepienik
0801799600 [release] v0.9.15 2023-07-28 10:28:15 +01:00
dependabot[bot]
3d8d411dd4
[skip ci] Bump github.com/docker/docker (#64)
Bumps [github.com/docker/docker](https://github.com/docker/docker) from 23.0.1+incompatible to 23.0.3+incompatible.
- [Release notes](https://github.com/docker/docker/releases)
- [Commits](https://github.com/docker/docker/compare/v23.0.1...v23.0.3)

---
updated-dependencies:
- dependency-name: github.com/docker/docker
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-07-28 09:32:34 +01:00
Yann Stepienik
6d78a129cc [skip ci] dependabot/codeql 2023-07-28 09:04:06 +01:00
Yann Stepienik
7c8750d240 [skip ci] thanks you! 2023-07-27 12:17:51 +01:00
Yann Stepienik
4975d764c6 [release] v0.9.14 2023-07-27 11:16:22 +01:00
Yann Stepienik
d86e4c04ca [release] 0.9.13 2023-07-25 12:06:44 +01:00
Yann Stepienik
f1b299e9a4 [release] 0.9.13 2023-07-25 12:00:21 +01:00
Yann Stepienik
658e99df44 [release] 0.9.13 2023-07-25 11:58:27 +01:00
Yann Stepienik
6dc5261828 [release] 0.9.12 2023-07-25 11:38:12 +01:00
Jogai
7cd0a7a67e
[skip ci] Reduce layers in docker (#58)
* Reduce layers in docker

* Reduce docker layers

* Sign contributor agreement
2023-07-24 19:39:33 +01:00
Yann Stepienik
83da4237a4 [release] 0.9.11 2023-07-24 14:38:29 +01:00
Yann Stepienik
26d5fe5767 [release] 0.9.11 2023-07-24 13:01:24 +01:00
Yann Stepienik
8e9649ec2c
[skip ci] Merge pull request #56 from jwr1/master
Fix environment vars and labels containing multiple equals
2023-07-24 12:56:33 +01:00
Johnwesley R
6417e850e4 Fix environment vars and labels containing multiple equals 2023-07-23 11:03:56 -04:00
Yann Stepienik
1f8fbfb0a2
Merge pull request #55 from jwr1/patch-1
Fix link to Other Setups page
2023-07-23 08:42:54 +01:00
John Wesley
d68a27dd67
Sign the Contributor License Agreement 2023-07-22 22:36:17 -04:00
John Wesley
561429483a
Fix link to Other Setups page 2023-07-22 22:32:08 -04:00
Yann Stepienik
0b751ca163 [release] 0.9.10 2023-07-22 15:46:43 +01:00
Yann Stepienik
59803821ef [release] 0.9.9 2023-07-19 11:27:48 +01:00
Yann Stepienik
52b600deee [skip ci] readme 2023-07-16 12:33:21 +01:00
Yann Stepienik
7798dd35fd [skip ci] readme 2023-07-16 12:28:57 +01:00
Yann Stepienik
2a6ac2ea08 [release] 0.9.8 2023-07-16 12:07:35 +01:00
Yann Stepienik
3a0ef7d775 [skip ci] thanks you! 2023-07-14 14:55:43 +01:00
Yann Stepienik
b168148733 [release] v0.9.7 2023-07-12 22:15:03 +01:00
Yann Stepienik
2447b25422 [release] v0.9.6 2023-07-10 13:41:49 +01:00
Yann Stepienik
7dc5ea5214 [release] v0.9.6 2023-07-10 13:41:30 +01:00
Yann Stepienik
43a9ddef08 [release] v0.9.5 2023-07-07 11:36:27 +01:00
Yann Stepienik
6976f4d99d [release] v0.9.4 2023-07-06 11:07:17 +01:00
Yann Stepienik
85682f7f05 [release] v0.9.3 2023-07-05 12:47:33 +01:00
Yann Stepienik
6d162c6987 [release] v0.9.2 2023-07-05 12:13:37 +01:00
Yann Stepienik
7ef0685a35 [release] v0.9.1 2023-07-04 21:57:39 +01:00
Yann Stepienik
a27dfcd5cb [skip ci] changelogs 2023-07-04 14:23:36 +01:00
Yann Stepienik
97c20d9b1f [release] v0.9.0 2023-07-04 14:19:40 +01:00
Yann Stepienik
cc3604b8f3 [release] v0.9.0-unstable13 2023-07-04 12:32:37 +01:00
Yann Stepienik
bba2b59816 [release] v0.9.0-unstable12 2023-07-04 12:13:52 +01:00
Yann Stepienik
d96d1aecd0 [release] v0.9.0-unstable11 2023-07-04 11:58:14 +01:00
Yann Stepienik
cb4319bee0 [release] v0.9.0-unstable10 2023-07-04 11:52:38 +01:00
Yann Stepienik
9960a37b03 [release] v0.9.0-unstable9 2023-07-02 16:12:31 +01:00
Yann Stepienik
2b488ddb51 [release] v0.9.0-unstable8 2023-07-02 15:47:29 +01:00
Yann Stepienik
3c9318aba0 [release] v0.9.0-unstable7 2023-07-02 15:40:12 +01:00
Yann Stepienik
1b22ca65fc [release] v0.9.0-unstable6 2023-07-02 15:28:14 +01:00
Yann Stepienik
a6c6e2b524 [release] v0.9.0-unstable5 2023-07-02 14:53:14 +01:00
Yann Stepienik
f2c495baa5 [release] v0.9.0-unstable4 2023-07-02 13:00:29 +01:00
Yann Stepienik
2185d2be7c [release] v0.9.0-unstable3 2023-07-01 17:06:12 +01:00
Yann Stepienik
b58855b428 [release] v0.9.0-unstable2 2023-07-01 16:46:25 +01:00
Yann Stepienik
d0eedf7cc1 [release] v0.9.0-unstable 2023-07-01 16:32:21 +01:00
Yann Stepienik
004c59b00d [release] v0.8.11 2023-06-27 14:59:41 +01:00
Yann Stepienik
b9bd5d671a [release] v0.8.10 2023-06-27 11:10:13 +01:00
Yann Stepienik
68f14b2efc [release] v0.8.9 2023-06-27 10:58:24 +01:00
Yann Stepienik
681e00db85 [release] v0.8.8 2023-06-26 13:43:49 +01:00
Yann Stepienik
dccbbc0885 [release] v0.8.7 2023-06-26 13:17:11 +01:00
Yann Stepienik
7f9b002f2b [release] v0.8.6 2023-06-24 23:02:30 +01:00
Yann Stepienik
50c911c2e3 [skip ci] readme 2023-06-24 11:05:07 +01:00
Yann Stepienik
867e12400e [release] v0.8.5 2023-06-23 23:27:21 +01:00
Yann Stepienik
6073f74150 [release] v0.8.4 2023-06-23 17:02:24 +01:00
Yann Stepienik
352cf44eda [release] v0.8.3 2023-06-23 15:38:58 +01:00
Yann Stepienik
691e0319d1 [skip ci] changelog 2023-06-23 15:34:14 +01:00
Yann Stepienik
a4c7eded55 [release] v0.8.3 2023-06-23 15:29:54 +01:00
Yann Stepienik
e874e53cdb [skip ci] thanks you! 2023-06-22 18:40:41 +01:00
Yann Stepienik
b27746f0ba [release] v0.8.2 2023-06-22 14:14:51 +01:00
Yann Stepienik
c3dd4f61f2 [release] v0.8.1 2023-06-22 13:18:28 +01:00
Yann Stepienik
71f888eb36 [skip ci] changelogs 2023-06-22 13:16:02 +01:00
Yann Stepienik
274cc38e7f [release] v0.8.1 2023-06-22 13:15:30 +01:00
Yann Stepienik
6a32ac8313 [skip ci] thanks you! 2023-06-21 22:43:40 +01:00
Yann Stepienik
4bba986b92 [release] v0.8.0 2023-06-21 16:21:00 +01:00
Yann Stepienik
4a024db90d [release] v0.8.0-unstable12 2023-06-21 16:04:49 +01:00
Yann Stepienik
490468ca6f [release] v0.8.0-unstable11 2023-06-21 14:54:19 +01:00
Yann Stepienik
571ffca7b5 [release] v0.8.0-unstable10 2023-06-21 10:26:17 +01:00
Yann Stepienik
760787df9f [release] v0.8.0-unstable9 2023-06-21 10:24:22 +01:00
Yann Stepienik
bbcaec9066 [release] v0.8.0-unstable8 2023-06-21 01:28:13 +01:00
Yann Stepienik
348fd02916 [release] v0.8.0-unstable7 2023-06-21 01:07:44 +01:00
Yann Stepienik
d29f691eac [release] v0.8.0-unstable5 2023-06-20 19:53:25 +01:00
Yann Stepienik
1a6ea9c997 [release] v0.8.0-unstable4 2023-06-20 19:46:24 +01:00
Yann Stepienik
6c8b85609c [release] v0.8.0-unstable3 2023-06-20 19:42:17 +01:00
Yann Stepienik
351d0327e5 [release] v0.8.0-unstable2 2023-06-20 18:45:04 +01:00
Yann Stepienik
5288f0d954 [release] v0.8.0-unstable 2023-06-20 18:34:06 +01:00
Yann Stepienik
6f1d395df3 [release] v0.7.10 2023-06-20 01:06:38 +01:00
Yann Stepienik
eec4b3cbdb [release] v0.7.9 2023-06-20 01:04:25 +01:00
Yann Stepienik
aa569f8c90 [release] v0.7.8 2023-06-20 00:12:20 +01:00
Yann Stepienik
602dce53f2 [skip ci] 2023-06-19 21:15:35 +01:00
Yann Stepienik
c40ab3addb
[skip ci] Update readme.md 2023-06-19 10:47:54 +01:00
Yann Stepienik
3f8e6a273e [release] v0.7.7 2023-06-18 11:25:48 +01:00
Yann Stepienik
10abffbf26 [release] v0.7.6 2023-06-18 11:15:58 +01:00
Yann Stepienik
bfe587b1ed [skip ci] v0.7.5 2023-06-18 10:35:27 +01:00
Yann Stepienik
ee87be7994 [release] v0.7.5 2023-06-18 10:35:07 +01:00
Yann Stepienik
245305e3a6 [release] v0.7.4 2023-06-18 10:26:49 +01:00
Yann Stepienik
891be69fb2 [release] v0.7.3 2023-06-18 10:19:42 +01:00
Yann Stepienik
97aff87071 [release] v0.7.3 2023-06-18 10:18:44 +01:00
Yann Stepienik
2028901fa6 [release] v0.7.2 2023-06-18 10:04:01 +01:00
Yann Stepienik
a8372855e0 [release] v0.7.2 2023-06-18 10:00:39 +01:00
Yann Stepienik
0be36afe71 [release] v0.7.1 2023-06-17 18:08:26 +01:00
Yann Stepienik
76b6c86b96 [release] v0.7.0 2023-06-16 12:21:00 +01:00
Yann Stepienik
5f358b3fc4 Update to 0.7.0-unstable14 2023-06-16 11:52:37 +01:00
Yann Stepienik
c372a3ca94 Update to 0.7.0-unstable13 2023-06-15 17:22:00 +01:00
Yann Stepienik
7d3b36213b Update to 0.7.0-unstable12 2023-06-15 17:12:16 +01:00
Yann Stepienik
ca27d88ec6 Update to 0.7.0-unstable11 2023-06-15 13:34:10 +01:00
Yann Stepienik
7e565a42d2 Update to 0.7.0-unstable10 2023-06-15 13:07:10 +01:00
Yann Stepienik
2304d269d8 Update to 0.7.0-unstable9 2023-06-14 12:58:13 +01:00
Yann Stepienik
02b1d50432 Update to 0.7.0-unstable8 2023-06-13 23:51:43 +01:00
Yann Stepienik
320b0fbbb4 Update to 0.7.0-unstable7 2023-06-13 23:41:29 +01:00
Yann Stepienik
9e5305eff5 [skip ci] changelogs 2023-06-13 23:07:45 +01:00
Yann Stepienik
9bef41a0ca Update to 0.7.0-unstable6 2023-06-13 22:39:04 +01:00
Yann Stepienik
aa9a72b787 Update to 0.7.0-unstable5 2023-06-13 22:30:11 +01:00
Yann Stepienik
8d113b5337 Update to 0.7.0-unstable4 2023-06-13 12:07:30 +01:00
Yann Stepienik
d4a2f24a50 Update to 0.7.0-unstable3 2023-06-13 11:15:01 +01:00
Yann Stepienik
2122e8068a [release] v0.7.0-unstable3 2023-06-13 02:03:44 +01:00
Yann Stepienik
6bc9e02e28 [release] v0.7.0-unstable3 2023-06-13 02:03:18 +01:00
Yann Stepienik
8d6329f8d5 [release] v0.7.0-unstable2 2023-06-09 19:48:31 +01:00
Yann Stepienik
e69c81fe7a [release] v0.7.0-unstable 2023-06-08 20:55:16 +01:00
Yann Stepienik
6c170e5145 [release] v0.6.4 2023-06-07 17:59:45 +01:00
Yann Stepienik
d6a257bb44 [release] v0.6.4-unstable 2023-06-07 17:52:15 +01:00
Yann Stepienik
c28a5c51e2 [release] v0.6.4-unstable 2023-06-07 17:40:17 +01:00
Yann Stepienik
2fdcd9ee70 [release] v0.6.3 2023-06-07 12:59:48 +01:00
Yann Stepienik
15397a75fe [release] v0.6.2 2023-06-07 11:36:01 +01:00
Yann Stepienik
978381c1e3
[skip ci] Merge pull request #21 from Rujios/master
fix arm64 mongo image name in run.go -- thanks @Rujios
2023-06-07 11:17:52 +01:00
Rujios
2172aa0a7d
Update run.go
change amd64 to arm64v8 on arm detect
2023-06-07 02:54:31 -05:00
Yann Stepienik
55dcdfd7f5 [release] v0.6.1 2023-06-06 20:50:34 +01:00
Yann Stepienik
1125569811 [release] v0.6.0 - OpenID 2023-06-06 18:05:12 +01:00
Yann Stepienik
1970f14eac [release] v0.6.0-unstable2 2023-06-05 22:00:21 +01:00
Yann Stepienik
ebe8423a2d [skip ci] changelogs 2023-06-04 17:08:27 +01:00
Yann Stepienik
40585de96b [skip ci] changelogs 2023-06-04 17:07:25 +01:00
Yann Stepienik
60bf7627bb [release] version 0.6.0-unstable 2023-06-04 15:41:26 +01:00
Yann Stepienik
0cdb11ba82 [release] v0.5.12 2023-05-28 12:51:52 +01:00
Yann Stepienik
fc3908ee8e [release] v0.5.12-unstable 2023-05-28 12:47:08 +01:00
Yann Stepienik
0fd787e4c0 [release] v0.5.12-unstable 2023-05-28 12:40:15 +01:00
Yann Stepienik
ad95b4acb5 [release] v0.5.12-unstable 2023-05-28 12:37:14 +01:00
Yann Stepienik
76a95daf5b [release] v0.5.12-unstable 2023-05-28 12:25:11 +01:00
Yann Stepienik
da883991d0 [release] v0.5.12-unstable 2023-05-28 12:16:50 +01:00
Yann Stepienik
68e3220e71 [release] v0.5.12-unstable 2023-05-28 12:07:47 +01:00
Yann Stepienik
ffddd9ef5f [release] v0.5.12-unstable 2023-05-28 12:03:38 +01:00
Yann Stepienik
12546db205 [release] v0.5.12-unstable 2023-05-28 11:57:09 +01:00
Yann Stepienik
c1a6626a0b [release] v0.5.12-unstable 2023-05-28 11:49:49 +01:00
Yann Stepienik
7a3032de08 [release] v0.5.12-unstable 2023-05-28 11:44:28 +01:00
Yann Stepienik
0ea9ae762e [release] v0.5.12-unstable 2023-05-27 18:41:58 +01:00
Yann Stepienik
34269131c7 [release] v0.5.12-unstable 2023-05-27 18:41:32 +01:00
Yann Stepienik
2b3b19eb15 [release] v0.5.12-unstable 2023-05-27 18:27:41 +01:00
Yann Stepienik
bc9d840e3f [release] v0.5.12-unstable 2023-05-27 18:27:06 +01:00
Yann Stepienik
3cb1f8e7a1 [release] v0.5.12-unstable 2023-05-27 18:25:22 +01:00
Yann Stepienik
40134af60a [release] v0.5.12-unstable 2023-05-27 18:24:58 +01:00
Yann Stepienik
5009ddd5d5 [release] v0.5.12-unstable 2023-05-27 18:19:48 +01:00
Yann Stepienik
2a6c091ff0 [release] v0.5.12-unstable 2023-05-27 18:16:09 +01:00
Yann Stepienik
dc4e362a55 [release] v0.5.12-unstable 2023-05-27 18:14:45 +01:00
Yann Stepienik
3a228a9831 [release] v0.5.12-unstable 2023-05-27 18:11:33 +01:00
Yann Stepienik
95fe730985 [skip ci] changelogs 2023-05-27 14:09:00 +01:00
Yann Stepienik
c97ebed936 [release] v0.5.11 2023-05-27 13:58:33 +01:00
Yann Stepienik
caddef01d1 [release] 0.5.10 2023-05-20 20:58:25 +01:00
Yann Stepienik
f0a7cc9c72 [release] 0.5.9 2023-05-19 21:16:15 +01:00
Yann Stepienik
3b50610a4d [release] 0.5.9 2023-05-19 21:15:42 +01:00
Yann Stepienik
9a11b80e6b [release] 0.5.8 2023-05-19 20:48:54 +01:00
Yann Stepienik
e814aff79b [release] 0.5.7 2023-05-19 20:41:27 +01:00
Yann Stepienik
2989f97610 [release] 0.5.7-unstable 2023-05-19 20:36:01 +01:00
Yann Stepienik
7a204021f1 [skip ci] changelogs 2023-05-19 18:04:56 +01:00
Yann Stepienik
3b9a7c3223 [release] version 0.5.6 2023-05-19 17:01:11 +01:00
Yann Stepienik
4a8f772544 [skip ci] changelogs 2023-05-19 16:23:41 +01:00
Yann Stepienik
a6098f0507 [release] version 0.5.5 2023-05-19 16:23:05 +01:00
Yann Stepienik
c8731e2fa7 [skip ci] readme 2023-05-19 01:18:50 +01:00
Yann Stepienik
50b218c0d8 [skip ci] readme 2023-05-19 01:18:22 +01:00
Yann Stepienik
5fb4ce9d99 [release] v0.5.4 - Add DNS challenge to installer 2023-05-18 19:30:26 +01:00
Yann Stepienik
84ceae0591 [release] v0.5.3 - Support CPU without AVX 2023-05-18 17:50:10 +01:00
Yann Stepienik
4e70cdfc60 [release] version 0.5.2 2023-05-18 13:42:47 +01:00
Yann Stepienik
913e88896b [release] version 0.5.1 2023-05-18 11:12:41 +01:00
Yann Stepienik
d39a3efc7a [release] version 0.5.0
- Add Terminal to containers
 - Add Create ServApp
 - Add support for importing Docker Compose
 - Improved icon fetching
 - Change Home background and style (especially fixing the awckward light theme)
 - Fixed 2 bugs with the smart shield, that made it too strict
 - Fixed issues that prevented from login in with different hostnames
 - Added more info on the shield when blocking someone
 - Fixed issue where the UI would have missing icon images
 - Fixed Homepage showing stopped containers
 - Fixed bug where you can't save changes on the URLs Screen
2023-05-18 10:50:42 +01:00
Yann Stepienik
9e660973d7 [release] version 0.5.0-unstable30 2023-05-17 20:00:49 +01:00
Yann Stepienik
574ead679d [release] version 0.5.0-unstable29 2023-05-17 13:33:00 +01:00
Yann Stepienik
c48926c367 [release] version 0.5.0-unstable28 2023-05-17 13:31:31 +01:00
Yann Stepienik
ec5289081c [release] version 0.5.0-unstable27 2023-05-17 13:01:27 +01:00
Yann Stepienik
98abbfe7bc [release] version 0.5.0-unstable26 2023-05-17 13:00:48 +01:00
Yann Stepienik
1722f3f832 [release] version 0.5.0-unstable25 2023-05-17 12:44:14 +01:00
Yann Stepienik
7134301f64 [release] version 0.5.0-unstable24 2023-05-16 23:58:06 +01:00
Yann Stepienik
aa0a4f8865 [release] version 0.5.0-unstable23 2023-05-16 23:49:28 +01:00
Yann Stepienik
a208be1c8a [release] version 0.5.0-unstable22 2023-05-16 23:41:31 +01:00
Yann Stepienik
550917bf6e [release] version 0.5.0-unstable20 2023-05-16 23:16:53 +01:00
Yann Stepienik
0f46cff353 [release] version 0.5.0-unstable20 2023-05-16 22:51:19 +01:00
Yann Stepienik
4beede621b [release] version 0.5.0-unstable19 2023-05-16 22:29:26 +01:00
Yann Stepienik
7352e1f61f [release] version 0.5.0-unstable18 2023-05-16 19:46:03 +01:00
Yann Stepienik
cc849b756d [release] version 0.5.0-unstable17 2023-05-16 19:38:10 +01:00
Yann Stepienik
ed48fa6fac [release] version 0.5.0-unstable16 2023-05-16 19:34:21 +01:00
Yann Stepienik
a1a16f5bc9 [release] version 0.5.0-unstable15 2023-05-16 19:26:41 +01:00
Yann Stepienik
4de4f572ea [release] version 0.5.0-unstable14 2023-05-16 19:17:20 +01:00
Yann Stepienik
dd202311ea [release] version 0.5.0-unstable13 2023-05-16 19:13:22 +01:00
Yann Stepienik
68fd353181 [release] version 0.5.0-unstable12 2023-05-16 18:30:04 +01:00
Yann Stepienik
467f84187f [release] version 0.5.0-unstable11 2023-05-16 18:08:01 +01:00
Yann Stepienik
e3503f4345 [release] version 0.5.0-unstable10 2023-05-15 20:45:14 +01:00
Yann Stepienik
364b56a317 [release] version 0.5.0-unstable9 2023-05-15 20:35:14 +01:00
Yann Stepienik
b94651811e [release] version 0.5.0-unstable8 2023-05-15 20:24:31 +01:00
Yann Stepienik
e5dde6e02c [release] version 0.5.0-unstable8 2023-05-15 20:24:24 +01:00
Yann Stepienik
1d10c66a22 [release] version 0.5.0-unstable7 2023-05-15 20:23:06 +01:00
Yann Stepienik
5545163768 [release] version 0.5.0-unstable6 2023-05-14 15:48:15 +01:00
Yann Stepienik
1ffbb8b39b [release] version 0.5.0-unstable5 2023-05-14 14:21:40 +01:00
Yann Stepienik
62ec6ad308 [release] version 0.5.0-unstable4 2023-05-14 13:23:04 +01:00
Yann Stepienik
3cbd88f4a6 [release] version 0.5.0-unstable3 2023-05-14 13:11:59 +01:00
Yann Stepienik
c670456d47 [release] version 0.5.0-unstable2 2023-05-13 18:38:39 +01:00
Yann Stepienik
8b4d738c2e [release] version 0.5.0-unstable 2023-05-11 19:15:05 +01:00
Yann Stepienik
b76f0650d8 [skip ci] changelog 2023-05-10 21:14:40 +01:00
Yann Stepienik
7234ed5411 [release] version 0.4.3 2023-05-10 21:07:58 +01:00
Yann Stepienik
68ef1af276 [release] version 0.4.2 2023-05-10 18:17:11 +01:00
Yann Stepienik
d17aeb5a47 [release] version 0.4.1
fix issue with UI and HTTP login
2023-05-09 17:03:16 +01:00
Yann Stepienik
1d85218c8d [skip ci] demo update 2023-05-08 16:31:08 +01:00
Yann Stepienik
b22c83333d [skip ci] demo update 2023-05-08 16:30:21 +01:00
Yann Stepienik
c0cba101fa [skip ci] doc 2023-05-08 16:08:45 +01:00
Yann Stepienik
d55e0f859f [release] v0.4.0
- Protect server against direct IP access
 - Improvements to installer to make it more robust
 - Fix bug where you can't complete the setup if you don't have a database
 - When re-creating a container to edit it, restore the previous container if the edit is not succesful
 - Stop / Start / Restart / Remove / Kill containers
 - List / Delete / Create Volumes - done
 - List / Delete / Create  Networks - done
 - Container Logs Viewer - done
 - Edit Container Details and Docker Settings - done
 - Set Labels / Env variable to containers - done
 - (De)Attach networks to containers - done
 - (De)Attach volumes  to containers - done
2023-05-08 16:04:24 +01:00
Yann Stepienik
01f10473c5 [release] v0.4.0-unstable11 2023-05-08 14:51:26 +01:00
Yann Stepienik
6a7cc3795d [release] v0.4.0-unstable11 2023-05-08 14:42:01 +01:00
Yann Stepienik
13639a02e1 [release] v0.4.0-unstable10 2023-05-08 14:38:12 +01:00
Yann Stepienik
1f97b7bf29 [release] v0.4.0-unstable9 2023-05-08 14:30:39 +01:00
Yann Stepienik
925070d0a3 [release] v0.4.0-unstable7 2023-05-08 14:18:43 +01:00
Yann Stepienik
b4080e14f8 [release] v0.4.0-unstable7 2023-05-08 14:15:53 +01:00
Yann Stepienik
08d7dbbe4e [release] v0.4.0-unstable6 2023-05-08 12:38:27 +01:00
Yann Stepienik
6a8e97b242 [release] v0.4.0-unstable5 2023-05-07 17:47:20 +01:00
Yann Stepienik
7e37cfb996 [release] v0.4.0-unstable4 2023-05-06 19:32:49 +01:00
Yann Stepienik
ac6fbe64e7 [release] v0.4.0-unstable3 2023-05-06 19:25:10 +01:00
Yann Stepienik
8ce9d52fbd [release] v0.4.0-unstable2 2023-05-05 19:06:59 +01:00
Yann Stepienik
241abdfca4 [release] v0.4.0-unstable2 2023-05-05 19:05:33 +01:00
Yann Stepienik
efab134d73 [release] v0.4.0-unstable 2023-05-04 18:41:54 +01:00
Yann Stepienik
dba97aca00 [release] v0.3.5 Let's Encrypt improvements 2023-05-04 18:15:28 +01:00
Yann Stepienik
09af871f5a Thanks you 2023-05-03 23:04:15 +01:00
Yann Stepienik
c4e72ade4c [release] v 0.3.4 - fix arm (trying smtg else) 2023-05-03 20:47:01 +01:00
Yann Stepienik
eb0cdfdb1a [release] v 0.3.4 - fix arm (trying smtg else) 2023-05-03 20:40:55 +01:00
Yann Stepienik
bfc43497bc [release] v 0.3.4 - fix arm (trying smtg else) 2023-05-03 20:37:31 +01:00
Yann Stepienik
a1c8e45441 [release] v 0.3.4 - fix arm (trying smtg else) 2023-05-03 20:35:11 +01:00
Yann Stepienik
7ca2a793e3 [release] v 0.3.4 - fix arm (trying smtg else) 2023-05-03 20:34:24 +01:00
Yann Stepienik
2bcc488f23 [release] v 0.3.4 - fix arm (again?) 2023-05-03 11:26:56 +01:00
Yann Stepienik
0f25ed6786 [release] v 0.3.4 - fix arm (again?) 2023-05-03 11:26:40 +01:00
Yann Stepienik
57fd773d0d [release] v 0.3.4 - fix arm (again?) 2023-05-03 11:13:11 +01:00
Yann Stepienik
ec08610e46 [release] v 0.3.3 - fix arm (again?) 2023-05-03 10:54:05 +01:00
Yann Stepienik
bad1b363f3 fix ci 2023-05-03 10:42:22 +01:00
Yann Stepienik
a1f8964e6b [release] v 0.3.2: fix ARM build 2023-05-03 10:41:34 +01:00
Yann Stepienik
329eb9787e [release] v 0.3.2: fix ARM build 2023-05-03 10:41:26 +01:00
Yann Stepienik
c9c78aec15 [release] v0.3.1
- fix overflowing text in home page
2023-05-02 00:54:23 +01:00
Yann Stepienik
89defa2c04 [release] v0.3.1
- fix overflowing text in home page
2023-05-02 00:54:07 +01:00
Yann Stepienik
684981516d
[skip ci] Add temporary licence file 2023-05-02 00:35:32 +01:00
Yann Stepienik
a164e952b5 [skip ci] readme 2023-05-01 20:43:53 +01:00
Yann Stepienik
e62c470bf2 [skip ci] readme 2023-05-01 19:04:58 +01:00
Yann Stepienik
5f909e4af7 [skip ci] readme 2023-05-01 18:53:17 +01:00
Yann Stepienik
8506e5933a [skip ci] readme 2023-05-01 18:47:10 +01:00
Yann Stepienik
ce1f6eff7d [skip ci] readme 2023-05-01 18:42:26 +01:00
Yann Stepienik
0e5d20d4e0 [skip ci] readme 2023-05-01 18:41:56 +01:00
Yann Stepienik
43b1ae9505 [skip ci] readme 2023-05-01 18:40:40 +01:00
Yann Stepienik
143f6c4583 [skip ci] readme 2023-05-01 18:39:58 +01:00
Yann Stepienik
0351859034 [skip ci] readme 2023-05-01 18:39:39 +01:00
Yann Stepienik
8c61d6a98f [skip ci] add demo 2023-05-01 18:33:58 +01:00
Yann Stepienik
420b0203be [release] Version 0.3.0
- Implement 2 FA
 - Implement SMTP to Send Email (password reset / invites)
 - Add homepage
 - DNS challenge for letsencrypt
 - Set Max nb simulatneous connections per user
 - Admin only routes (See in security tab)
 - Set Global Max nb simulatneous connections
 - Block based on geo-locations
 - Block common bots
 - Display nickname on invite page
 - Reset self-signed certificates when hostnames changes
 - Edit user emails
 - Show loading on user rows on actions
2023-05-01 15:58:34 +01:00
Yann Stepienik
f80c6a18bb v0.3.0-unstable19 2023-05-01 14:01:06 +01:00
Yann Stepienik
ef25b13cb4 v0.3.0-unstable18 2023-05-01 13:30:39 +01:00
Yann Stepienik
721ee3f91f v0.3.0-unstable17 2023-05-01 13:17:57 +01:00
Yann Stepienik
e15e2a0a71 v0.3.0-unstable16 2023-05-01 13:11:30 +01:00
Yann Stepienik
a932933550 v0.3.0-unstable15 2023-05-01 13:07:01 +01:00
Yann Stepienik
e93e45d4df v0.3.0-unstable14 2023-05-01 12:59:46 +01:00
Yann Stepienik
29fc924475 v0.3.0-unstable13 2023-05-01 11:05:35 +01:00
Yann Stepienik
06aac804a7 v0.3.0-unstable12 2023-05-01 11:04:06 +01:00
Yann Stepienik
868bc6edee v0.3.0-unstable11 2023-05-01 11:00:45 +01:00
Yann Stepienik
ac3a53507d v0.3.0-unstable10 2023-05-01 00:56:10 +01:00
Yann Stepienik
9d6773f27b v0.3.0-unstable9 2023-05-01 00:48:13 +01:00
Yann Stepienik
ceac360533 v0.3.0-unstable8 2023-05-01 00:46:43 +01:00
Yann Stepienik
c9cea9b6b9 v0.3.0-unstable7 2023-04-30 23:24:54 +01:00
Yann Stepienik
99736aa2a9 v0.3.0-unstable6 2023-04-30 21:36:51 +01:00
Yann Stepienik
229a14d4a2 v0.3.0-unstable5 2023-04-30 21:21:04 +01:00
Yann Stepienik
b84ee3e61c [release] v0.3.0-unstable2 SMTP
v0.3.0-unstable3

v0.3.0-unstable4

v0.3.0-unstable4
2023-04-30 17:36:19 +01:00
Yann Stepienik
3811e3131e [release] v0.3.0-unstable 2FA + geoblock 2023-04-30 13:03:14 +01:00
Yann Stepienik
ea48586705 [release] v0.2.0
- URL UI completely redone from scratch
 - Add new Smart Shield feature for easier protection without manual adjustments required
 - Add icons for self-hosted apps
 - Rewrite the restart function to allow the UI to gracefully wait for the server to restart
 - /login redirect now has query strings
 - prevent ports or network to scroll view
 - Fix URLs appearing on the wrong container because of nested names
 - Improve port display
 - Config API now reads the file directly to prevent overwritting changes between restarts
 - Warn user when there are config changes pending restart
 - Prevent login screen loop when being rate limited
 - Improve automatic hostname for new containers URLs
 - Fix minor bugs when host or prefix are false but values are set anyway
 - Edit should not reconnect bridge if force secure is true, for faster container restart
 - Improve network cleaning to prevent any issue with Docker Compose
 - Add Max Bandwith to routes to limit the amount of data that can be sent per seconds
 - Fix a bug where URLs target can't be edited if the container is in exited state
 - Fix bugs where the user would be editting the configuration on multiple tabs and end up in a bad state
 - Ensure route name is unique
2023-04-29 21:11:17 +01:00
Yann Stepienik
9aa2bc48ea v0.2.0-unstable4 2023-04-29 12:11:03 +01:00
Yann Stepienik
fd568de0d0 v0.2.0-unstable3 2023-04-28 20:46:49 +01:00
Yann Stepienik
803df9c684 v0.2.0-unstable2 2023-04-28 20:00:01 +01:00
Yann Stepienik
fc2ade528c v0.2.0-unstable2 2023-04-28 19:59:46 +01:00
Yann Stepienik
84f08a6042 v0.2.0-unstable2 2023-04-28 19:53:12 +01:00
Yann Stepienik
d80bbd91ed Stupid Linux 2023-04-28 19:39:31 +01:00
Yann Stepienik
fb05993989 Stupid Linux 2023-04-28 19:37:50 +01:00
Yann Stepienik
77c8201be0 v0.2.0-unstable
new UI
2023-04-28 19:32:38 +01:00
Yann Stepienik
1ad6edf50a v0.2.0-unstable
new UI
2023-04-28 19:28:01 +01:00
Yann Stepienik
963a1c7699 [skip ci] v0.2.0-unstable temp commit 2023-04-27 19:29:26 +01:00
Yann Stepienik
7c36e39ac0 [relase] v0.1.18-unstable4
- network fix
2023-04-20 19:35:06 +01:00
Yann Stepienik
95e86f289e [relase] v0.1.18-unstable3
- restore network clean up
2023-04-18 18:55:59 +01:00
Yann Stepienik
530ab120c0 [relase] v0.1.18-unstable2
- restore network clean up
2023-04-18 18:46:47 +01:00
Yann Stepienik
18432ca138 [relase] v0.1.18-unstable2
- restore network clean up
2023-04-18 18:39:39 +01:00
Yann Stepienik
1430c399c6 [skip ci] docker change 2023-04-18 17:09:02 +01:00
Yann Stepienik
d8cb10be75 [release] v0.1.18-unstable
- Smart Shield
2023-04-18 17:09:01 +01:00
Yann Stepienik
ac3c78d4b0 [release] v0.1.17-unstable
- Smart Shield
2023-04-18 17:09:01 +01:00
283 changed files with 44268 additions and 11239 deletions

View file

@ -1,17 +1,16 @@
version: 2
jobs:
build:
filters:
branches:
only:
- master
- unstable
machine: # executor type
image: ubuntu-2004:202010-01 # # recommended linux image - includes Ubuntu 20.04, docker 19.03.13, docker-compose 1.27.4
image: ubuntu-2204:2023.07.2
steps:
- checkout
- run:
name: Setup docker and buildx
command: docker buildx create --use
- run:
name: install dependencies
command: sudo apt-get install bash curl
@ -49,6 +48,30 @@ jobs:
name: Install dependencies
command: npm install
- run:
name: Download GeoLite2-Country database
command: |
curl -s -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=$MAX_TOKEN&suffix=tar.gz" -o GeoLite2-Country.tar.gz
tar -xzf GeoLite2-Country.tar.gz --strip-components 1 --wildcards "*.mmdb"
- run:
name: Download and Extract ARM Nebula Binary
command: |
curl -LO https://github.com/slackhq/nebula/releases/download/v1.7.2/nebula-linux-arm64.tar.gz
tar -xzvf nebula-linux-arm64.tar.gz
- run:
name: Rename ARM Nebula Binary
command: |
mv nebula nebula-arm
mv nebula-cert nebula-arm-cert
- run:
name: Download and Extract Nebula Binary
command: |
curl -LO https://github.com/slackhq/nebula/releases/download/v1.7.2/nebula-linux-amd64.tar.gz
tar -xzvf nebula-linux-amd64.tar.gz
- run:
name: Build UI
command: npm run client-build
@ -57,6 +80,65 @@ jobs:
name: Build and publish dockerfiles
command: sh docker.sh
buildarm:
machine:
image: ubuntu-2004:202101-01
resource_class: arm.medium
steps:
- checkout
- run:
name: install dependencies
command: sudo apt-get install bash curl
- run:
name: download Go
command: wget https://golang.org/dl/go1.20.2.linux-arm64.tar.gz
- run:
name: install Go
command: sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.20.2.linux-arm64.tar.gz
- run:
name: set Go path
command: echo 'export PATH=$PATH:/usr/local/go/bin' >> $BASH_ENV
- run: |
echo 'export NVM_DIR="/opt/circleci/.nvm"' >> $BASH_ENV
echo ' [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
- run: |
node -v
- run: |
nvm install v16
node -v
nvm alias default v16
- run: |
node -v
- run: docker login -u $DOCKER_LOGIN -p $DOCKER_PASSWORD
- run:
name: Install dependencies
command: npm install
- run:
name: Download GeoLite2-Country database
command: |
curl -s -L "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=$MAX_TOKEN&suffix=tar.gz" -o GeoLite2-Country.tar.gz
tar -xzf GeoLite2-Country.tar.gz --strip-components 1 --wildcards "*.mmdb"
- run:
name: Build UI
command: npm run client-build
- run:
name: Build and publish dockerfiles
command: sh docker.arm64.sh
workflows:
version: 2
build-all:
@ -65,4 +147,5 @@ workflows:
filters:
branches:
only:
- master
- master
- unstable

4
.clabot Normal file
View file

@ -0,0 +1,4 @@
{
"contributors": ["azukaar", "jwr1", "Jogai", "InterN0te", "catmandx", "revam"],
"message": "We require contributors to sign our [Contributor License Agreement](https://github.com/azukaar/Cosmos-Server/blob/master/cla.md). In order for us to review and merge your code, add yourself to the .clabot file as contributor, as a way of signing the CLA."
}

1
.dockerignore Normal file
View file

@ -0,0 +1 @@
node_modules

161
.github/dependabot.yml vendored Normal file
View file

@ -0,0 +1,161 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
enable-beta-ecosystems: true
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "npm dependencies"
- package-ecosystem: "bundler"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "bundler dependencies"
- package-ecosystem: "composer"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "composer dependencies"
- package-ecosystem: "maven"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "maven dependencies"
- package-ecosystem: "mix"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "mix dependencies"
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "cargo dependencies"
- package-ecosystem: "gradle"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "gradle dependencies"
- package-ecosystem: "nuget"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "nuget dependencies"
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "gomod dependencies"
- package-ecosystem: "docker"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "docker dependencies"
- package-ecosystem: "elm"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "elm dependencies"
- package-ecosystem: "gitsubmodule"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "gitsubmodule library"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "github-actions"
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "pip dependencies"
- package-ecosystem: "terraform"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "terraform dependencies"
- package-ecosystem: "pub"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "pub dependencies"
- package-ecosystem: "swift"
directory: "/"
schedule:
interval: "weekly"
target-branch: "library-updates"
allow:
- dependency-type: "all"
labels:
- "swift dependencies"

10
.gitignore vendored
View file

@ -10,4 +10,12 @@ config_dev.old.json
tests
todo.txt
LICENCE
tokens.json
tokens.json
.vscode
GeoLite2-Country.mmdb
dns-blacklist.txt
zz_test_config
nebula-arm
nebula-arm-cert
nebula
nebula-cert

256
LICENCE Normal file
View file

@ -0,0 +1,256 @@
Software: Cosmos-Server
License: Apache 2.0 with Commons Clause and Anti Tampering Clause
Licensor: Yann Stepienik
---------------------------------------------------------------------
“Commons Clause” License Condition v1.0
The Software is provided to you by the Licensor under the
License, as defined below, subject to the following condition.
Without limiting other conditions in the License, the grant
of rights under the License will not include, and the License
does not grant to you, the right to Sell the Software.
For purposes of the foregoing, “Sell” means practicing any or
all of the rights granted to you under the License to provide
to third parties, for a fee or other consideration (including
without limitation fees for hosting or consulting/ support
services related to the Software), a product or service whose
value derives, entirely or substantially, from the functionality
of the Software. Any license notice or attribution required by
the License must also include this Commons Clause License
Condition notice.
---------------------------------------------------------------------
"Anti Tampering Clause” License Condition v1.0
Notwithstanding any provision of the Apache License 2.0, if the User
(or any party receiving or distributing derivative works, services,
or anything of value from the User related to the Software), directly
or indirectly, seeks to tamper with, alter, circumvent, or avoid
compliance with any subscription, paywall, feature restriction, or any
other licensing mechanism built into the Software or its usage, the
License granted under the Apache License 2.0 shall automatically and
immediately terminate, and access to the Software shall be withdrawn
with immediate effect. Upon such termination, any and all rights
established under the Apache License 2.0 shall be null and void.
Tampering includes but is not limited to: (a) removing, disabling,
or circumventing any license key or other copy protection mechanism,
(b) redistributing parts or all of a feature that was intended
to be a paid feature, without keeping the restrictions, limitations,
or other licensing mechanisms with it(c) disabling, circumventing, or
avoiding any feature of the Software that is intended to enforce usage or
copy restrictions, or (d) providing or distributing any information
or code that enables disabling, circumvention, or avoidance of any
feature of the Software that is intended to enforce usage or copy
restrictions.
---------------------------------------------------------------------
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2020 n8n GmbH
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

BIN
Logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 348 KiB

BIN
banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

View file

@ -1,7 +0,0 @@
rm -rf build
env GOARCH=arm64 go build -o build/cosmos src/*.go
if [ $? -ne 0 ]; then
exit 1
fi
cp -r static build/
cp package.json build/

18
build.arm64.sh Normal file
View file

@ -0,0 +1,18 @@
#!/bin/bash
rm -rf build
env GOARCH=arm64 go build -o build/cosmos src/*.go
if [ $? -ne 0 ]; then
exit 1
fi
cp -r static build/
cp -r GeoLite2-Country.mmdb build/
cp -r Logo.png build/
mkdir build/images
cp client/src/assets/images/icons/cosmos_gray.png build/cosmos_gray.png
cp client/src/assets/images/icons/cosmos_gray.png cosmos_gray.png
echo '{' > build/meta.json
cat package.json | grep -E '"version"' >> build/meta.json
echo ' "buildDate": "'`date`'",' >> build/meta.json
echo ' "built from": "'`hostname`'"' >> build/meta.json
echo '}' >> build/meta.json

View file

@ -1,11 +1,32 @@
#!/bin/bash
echo " ---- Build Cosmos ----"
rm -rf build
env GOARCH=arm64 go build -o build/cosmos-arm64 src/*.go
if [ $? -ne 0 ]; then
exit 1
fi
go build -o build/cosmos src/*.go
if [ $? -ne 0 ]; then
exit 1
fi
echo " ---- Build complete, copy assets ----"
cp -r static build/
cp -r GeoLite2-Country.mmdb build/
cp nebula-arm-cert nebula-cert nebula-arm nebula build/
cp -r Logo.png build/
mkdir build/images
cp client/src/assets/images/icons/cosmos_gray.png build/cosmos_gray.png
cp client/src/assets/images/icons/cosmos_gray.png cosmos_gray.png
echo '{' > build/meta.json
cat package.json | grep -E '"version"' >> build/meta.json
echo ' "buildDate": "'`date`'",' >> build/meta.json
echo ' "built from": "'`hostname`'"' >> build/meta.json
echo '}' >> build/meta.json
echo '}' >> build/meta.json
echo " ---- copy complete ----"

View file

@ -1,3 +1,359 @@
## Version 0.13.1
- Fix a security issue with token (thansk @vncloudsco)
## Version 0.13.0
- Display container stacks as a group in the UI
- New Delete modal to delete services entirely
- Upload custom icons to containers
- improve backup file, by splitting cosmos out to a separate docker-compose.yml file
- Cosmos-networks now have specific names instead for generic names
- Fix issue where search bar reset when deleting volume/network
- Fix breadcrumbs in subpaths
- Remove graphs from non-admin UI to prevent errors
- Rewrite the overwriting container logic to fix race conditions
- Edit container user and devices from UI
- Fix bug where Cosmos Constellation's UDP ports by a TCP one
- Fix a bug with URL screen, where you can't delete a URL when there is a search
- Fix issue where negative network rate are reported
- Support array command and single device in docker-compose import
- Add default alerts... by default (was missing from the default config)
- disable few features liks Constellation, Backup and Monitoring when in install mode to reduce logs and prevent issues with the DB
## Version 0.12.6
- Fix a security issue with cross-domain APIs availability
## Version 0.12.5
- Added index on event date for faster query
## Version 0.12.4
- Fix crash with metrics not seeing any network interface
## Version 0.12.3
- Performance update for metrics saving
## Version 0.12.2
- Fix XSS vulnerability in the redirect function (thanks @catmandx)
## Version 0.12.1
- Fix a crash that would occasionally happen since 0.12 the DB is down
## Version 0.12.0
- New real time persisting and optimized metrics monitoring system (RAM, CPU, Network, disk, requests, errors, etc...)
- New Dashboard with graphs for metrics, including graphs in many screens such as home, routes and servapps
- New customizable alerts system based on metrics in real time, with included preset for anti-crypto mining and anti memory leak
- New events manager (improved logs with requests and advanced search)
- New notification system
- Added Marketplace UI to edit sources, with new display of 3rd party sources
- Added a notification when updating a container, renewing certs, etc...
- Certificates now renew sooner to avoid Let's Encrypt sending emails about expiring certificates
- Added option to disable routes without deleting them
- Improved icon loading speed, and added proper placeholder
- Marketplace now fetch faster (removed the domain indirection to directly fetch from github)
- Integrated a new docker-less mode of functioning for networking
- Added a dangerous IP detector that stops sending HTTP response to IPs that are abusing various shields features
- Added CORS headers to openID endpoints
- Added a button in the servapp page to easily download the docker backup
- Added Button to force reset HTTPS cert in settings
- Added lazyloading to URL and Servapp pages images
- Fixed annoying marketplace screenshot bug (you know what I'm talking about!)
- New color slider with reset buttons
- Redirect static folder to host if possible
- New Homescreen look
- Fixed blinking modals issues
- Add AutoFocus on Token field for 2FA Authentication (thanks @InterN0te)
- Allow Insecure TLS like self-signed certificate for SMTP server (thanks @InterN0te)
- Improve display of icons [fixes #121]
- Refactored Mongo connection code [fixes #111]
- Forward simultaneously TCP and UDP [fixes #122]
## Version 0.11.3
- Fix missing event subscriber on export
## Version 0.11.2
- Improve Docker exports logs
## Version 0.11.1
- fix issue exporting text user node
## Version 0.11.0
- Disable support for X-FORWARDED-FOR incoming header (needs further testing)
- Docker export feature for backups on every docker event
- Compose Import feature now supports skipping creating existing resources
- Compose Import now overwrite containers if they are differents
- Added support for cosmos-persistent-env, to persist password when overwriting containers (useful for encrypted or password protected volumes, like databases use)
- Fixed bug where import compose would try to revert a previously created volume when errors occurs
- Terminal for import now has colours
- Fix a bug where ARM CPU would not be able to start Constellation
## Version 0.10.4
- Encode OpenID .well-known to JSON
- Fix incompatibility with other apps using .well-known
- Secure the OpenID routes that missed the hardening
- Added some logs
## Version 0.10.3
- Add missing Constellation logs when creating certs
- Ignore empty links in cosmos-compose
## Version 0.10.2
- Fix port in host header
## Version 0.10.1
- Fix an issue where Constellation is stuck if creating a new network is interrupted
- Fix a logic issue with the whitelist inbound IPs
## Version 0.10.0
- Added Constellation
- DNS Challenge is now used for all certificates when enabled [breaking change]
- Rework headers for better compatibility
- Improve experience for non-admin users
- Fix bug with redirect on logout
- Added OverwriteHostHeader to routes to override the host header sent to the target app
- Added WhitelistInboundIPs to routes to filter incoming requests based on IP per URL
> **Note: If you use the ARM (:latest-arm) you need to manually update to using the :latest tag instead**
## Version 0.9.20 - 0.9.21
- Add option to disable CORS hardening (with empty value)
## Version 0.9.19
- Add country whitelist option to geoblocker
- No countries blocked by default anymore
- Merged ARM and AMD into a single docker tag (latest)
- Update to Debian 12
- Fix issue with Contradictory scheme headers
- Fix issue where non-admin users cant see Servapp on the homepage
## Version 0.9.18
- Typo with x-forwarded-host
## Version 0.9.17
- Upgraded to Lego 4.13.3 (support for Google Domain)
- Add VerboseForwardHeader to URL Config to allow to transfer more sensitive header to target app
- App DisableHeaderHardening to allow disabling header hardening for specific apps
## Version 0.9.16
- Small redirection bug fix
## Version 0.9.15
- Check background extension on upload is an image
- Update Docker for security patch
- Check redirect target is local
- Improve OpenID client secret generation
## Version 0.9.14
- Check network mode before pruning networks
## Version 0.9.13
- Fix issue with duplicated ports in network tab of servapps (because it shows the IPV4 and the IPV6 ports)
## Version 0.9.12
- Add integration to the `docker login` credentials store
- Smart-shield now works with different budgets per routes, so that requests on a permissive route don't count as requests on a strict route
- Fix an issue where users would never receive permanent bans from the shield
## Version 0.9.11
- Add support for port ranges in cosmos-compose
- Fix bug where multiple host port to the same container would override each other
- Port display on Servapp tab was inverted
- Fixed Network screen to support complex port mappings
- Add support for protocol in cosmos-compose port exposing logic
- Add support for relative bind path in docker-compose import
- Fix environment vars and labels containing multiple equals (@jwr1)
- Fix link to Other Setups page (@jwr1)
## Version 0.9.10
- Never ban gateway ips
- Prevent deleting networks if there's an error on disconnect
- Disabling network pruning now also disables cleaning up Cosmos networks
## Version 0.9.9
- Add new filters for routes based on method, query strings and headers (missing UI)
## Version 0.9.1 > 0.9.8
- Fix subdomain logic for composed TLDs
- Add option for custom wildcard domains
- Fix domain depupe logic
- Add import button in market
- Update LEGO
- Fix issue with hot-reloading between HTTP and HTTPS
- Fix loading bar in container overview page
- Flush Etag cache on restart
- Add timeout to icon fetching
- Bootstrap containers when adding new routes to them
- Remove headers from origin server to prevent duplicates
- Add licence
## Version 0.9.0
- Rewrote the entire HTTPS / DNS challenge system to be more robust and easier to use
- Let's Encrypt Certificate is now saved in the config file
- Cosmos will re-use previous certificate if renewal fails
- Self-Signed certificate will now renew on expiry
- If LE fails to renew, Cosmos will fallback to self-signed certificate
- If LE fails to renew, Cosmos will display a warning on the home page
- If certificate have more hostnames than required, Cosmos will not request a new certificate to prevent LE rate limiting issues
- No more restart needed when changing config, adding route, installing apps, etc...
- Change auto mapper to keep existing user definied ports
- When using a subdomain as the main Cosmos domain, UseWildcardCertificate will now request the root domain instead of *.sub.domain.com
- open id now supports multiple redirect uri (comma separated)
- add manual restart button in config
- New simpler Homepage style, with a toggle for expanded details homepage style in the config
- add a button on the first setup screen to perform a clean install
## version 0.8.1 -> 0.8.10
- Added new automatic Docker mapping feature (for people not using (sub)domains)
- Added guardrails to prevent Let's Encrypt from failing to initialize when adding wrong domains
- Add search bar on the marketplace
- App store image size issue
- Display more tags in the market
- Fixed wrong x-forwarded-proto header
- Add installer option for hostname prefix/suffix
- Fix minor issue with inconsistent password on market installer
- Fixed issue where home page was https:// links on http only servers
- Improved setup flow for setting up hostname and HTTPS
- Fixed auto-update on ARM based CPU
- Fix issue with email links
- HideFromDashboard option on routes
- Fix docker compose import issue with uppercase volumes
## Version 0.8.0
- Custmizable homepage / theme colors
- Auto-connect containers that have SERVAPP routes attached to them. aka. you do not need to "force secure" containers anymore
- Manually create smaller docker subnets when using force secure / links to not hit IP range limit
- Self-heal containers that have lost their network configurations
- Stop showing Docker not connected when first loading status in new installs
- Add a cosmos-icon label to containers to change the icon in the UI
- Add privacy settings to external links
- Force secure is now called "isolate network" to make it more clear, but does the same thing
- allow iframes in the same subdomain as the app to fix wordpress compatibility
## Version 0.7.1 -> 0.7.10
- Fix issue where multiple DBs get created at the setup
- Add more special characters to be used for password validation
- Add configurable default data path for binds
- Remove Redirects from home page
- Fix compat with non-HTTP protocol like WebDAV (for Nextcloud for example)
- Fix regression with DNS wildcards certificates
- Fix issue with the installer when changing both the labels and the volumes
- Fix regression where DNS keys don't appear in the config page after being changed
- Fix typo on "updating ServApp" message
## Version 0.7.0
- Add Cosmos App Market!
- Reforged the DNS CHallenge to be more user friendly. You can select your DNS provider in a list, and it will guide you through the process with the right fields to set (directly in the UI). No more env variables to set!
- Fix issue with docker compose timeout healthcheck as string, inverted ports, and supports for uid:gid syntax in user
- Fix for SELinux compatibility
- Fix false-negative error message on login screen when SMTP is disabled
## Version 0.6.1 - 0.6.4
- Workaround for Docker-compose race condition in Debian
- Fix ARM based MongDB image for older ARM Devices
- Fix issue with missing auth key with OpenID
## Version 0.6.0
- OpenID support!
- Add hostname check when adding new routes to Cosmos
- Add hostname check on new Install
- Fix missing save button for network mode
## Version 0.5.11
- Improve docker-compose import support for alternative syntaxes
- Improve docker service creation when using force secure label (fixes few containers not liking restarting too fast when created)
- Add toggle for using insecure HTTPS targets (fixes Unifi controller)
## Version 0.5.1 -> 0.5.10
- Add Wilcard certificates support
- Auto switch to Mongo 4 if CPU has no ADX
- Improve setup for certificates on new install
- Fix issue docker compose import labels and networks array
- Fix issue docker compose one-service syntax
- Fix issue with docker network mode not supporting hostname
- Fix an issue with the shield and the docker networking
- Fix issue with network namespace
- Fixed issue with a Docker bug preventing re-creating a container with a network mode as container (https://github.com/portainer/portainer/issues/2657)
- Silent error on favicon fetching
- Create Servapp step 1: make name / image required
## Version 0.5.0
- Add Terminal to containers
- Add "Create ServApp"
- Add support for importing Docker Compose
- Improved icon fetching
- Change Home background and style (especially fixing the awckward light theme)
- Fixed 2 bugs with the smart shield, that made it too strict
- Fixed issues that prevented from login in with different hostnames
- Added more info on the shield when blocking someone
- Fixed issue where the UI would have missing icon images
- Fixed Homepage showing stopped containers
- Fixed bug where you can't save changes on the URLs Screen
## Version 0.4.3
- Fix for exposing routes from the details page
## Version 0.4.2
- Fix when using custom port and logging in (Isssue #10)
## Version 0.4.1
- Fix small UI issues
- Fix HTTP login
## Version 0.4.0
- Protect server against direct IP access
- Improvements to installer to make it more robust
- Fix bug where you can't complete the setup if you don't have a database
- When re-creating a container to edit it, restore the previous container if the edit is not succesful
- Stop / Start / Restart / Remove / Kill containers
- List / Delete / Create Volumes
- List / Delete / Create Networks
- Container Logs Viewer
- Edit Container Details and Docker Settings
- Set Labels / Env variables on containers
- (De)Attach networks to containers
- (De)Attach volumes to containers
## Version 0.3.1 -> 0.3.5
- Fix UI issue with long name in home
- Fix ARM docker image
- Add more validation for Let's Encrypt
- Prevent browser from auto-filling password in config page
- Revert to HTTP when Let's Encrypt fails to initialize
## Version 0.3.0
- Implement 2 FA
- Implement SMTP to Send Email (password reset / invites)
- Add homepage
- DNS challenge for letsencrypt
- Set Max nb simulatneous connections per user
- Admin only routes (See in security tab)
- Set Global Max nb simulatneous connections
- Block based on geo-locations
- Block common bots
- Display nickname on invite page
- Reset self-signed certificates when hostnames changes
- Edit user emails
- Show loading on user rows on actions
## Version 0.2.0
- URL UI completely redone from scratch
- Add new "Smart Shield" feature for easier protection without manual adjustments required
- Add icons for self-hosted apps
- Rewrite the restart function to allow the UI to gracefully wait for the server to restart
- /login redirect now has query strings
- prevent ports or network to scroll view
- Fix URLs appearing on the wrong container because of nested names
- Improve port display
- Config API now reads the file directly to prevent overwritting changes between restarts
- Warn user when there are config changes pending restart
- Prevent login screen loop when being rate limited
- Improve automatic hostname for new containers URLs
- Fix minor bugs when host or prefix are false but values are set anyway
- Edit should not reconnect bridge if force secure is true, for faster container restart
- Improve network cleaning to prevent any issue with Docker Compose
- Add Max Bandwith to routes to limit the amount of data that can be sent per seconds
- Fix a bug where URLs target can't be edited if the container is in exited state
- Fix bugs where the user would be editting the configuration on multiple tabs and end up in a bad state
- Ensure route name is unique
## Version 0.1.16
- Fix search
@ -15,4 +371,4 @@
- Ports is now freetype, in case container does not expose any
- Container picker now tries to pick the best port as default
- Hostname now default to container name
- Additional UI improvements
- Additional UI improvements

21
cla.md Normal file
View file

@ -0,0 +1,21 @@
Cosmos Software Grant and Contributor License Agreement (“Agreement”)
This agreement is based on the Apache Software Foundation Contributor License Agreement. (v r190612)
Thank you for your interest in dba Cosmos (“Cosmos”). In order to clarify the intellectual property license granted with Contributions from any person or entity, Cosmos must have a Contributor License Agreement (CLA) on file that has been agreed to by each Contributor, indicating agreement to the license terms below. This license is for your protection as a Contributor as well as the protection of Cosmos and its users; it does not change your rights to use your own Contributions for any other purpose. This Agreement allows an individual to contribute to Cosmos on that individuals own behalf, or an entity (the “Corporation”) to submit Contributions to Cosmos, to authorize Contributions submitted by its designated employees to Cosmos, and to grant copyright and patent licenses thereto.
You accept and agree to the following terms and conditions for Your present and future Contributions submitted to Cosmos. Except for the license granted herein to Cosmos and recipients of software distributed by Cosmos, You reserve all right, title, and interest in and to Your Contributions.
Definitions. “You” (or “Your”) shall mean the copyright owner or legal entity authorized by the copyright owner that is making this Agreement with Cosmos. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, “control” means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. “Contribution” shall mean any work, as well as any modifications or additions to an existing work, that is intentionally submitted by You to Cosmos for inclusion in, or documentation of, any of the products owned or managed by Cosmos (the “Work”). For the purposes of this definition, “submitted” means any form of electronic, verbal, or written communication sent to Cosmos or its representatives, including but not limited to communication on electronic mailing lists, source code control systems (such as GitHub), and issue tracking systems that are managed by, or on behalf of, Cosmos for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as “Not a Contribution.”
Grant of Copyright License. Subject to the terms and conditions of this Agreement, You hereby grant to Yann Stepienik and to recipients of software distributed by Cosmos' owners a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works.
Grant of Patent License. Subject to the terms and conditions of this Agreement, You hereby grant to Yann Stepienik and to recipients of software distributed by Yann Stepienik a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) were submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your Contribution, or the Work to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed.
You represent that You are legally entitled to grant the above license. If You are an individual, and if Your employer(s) has rights to intellectual property that you create that includes Your Contributions, you represent that You have received permission to make Contributions on behalf of that employer, or that Your employer has waived such rights for your Contributions to Cosmos. If You are a Corporation, any individual who makes a contribution from an account associated with You will be considered authorized to Contribute on Your behalf.
You represent that each of Your Contributions is Your original creation (see section 7 for submissions on behalf of others).
You are not expected to provide support for Your Contributions,except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Unless required by applicable law or agreed to in writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE.
Should You wish to submit work that is not Your original creation, You may submit it to Cosmos separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the work as “Submitted on behalf of a third-party: [named here]”.

View file

@ -4,28 +4,88 @@ import * as React from 'react';
import ThemeCustomization from './themes';
import ScrollTop from './components/ScrollTop';
import Snackbar from '@mui/material/Snackbar';
import {Alert} from '@mui/material';
import {Alert, Box} from '@mui/material';
import logo from './assets/images/icons/cosmos.png';
import { setSnackit } from './api/wrap';
import * as API from './api';
import { setSnackit, snackit } from './api/wrap';
import { DisconnectOutlined } from '@ant-design/icons';
// ==============================|| APP - THEME, ROUTER, LOCAL ||============================== //
const LoadingAnimation = () => (
<div className="loader">
<div className="dot"></div>
<div className="dot"></div>
<div className="dot"></div>
</div>
);
export let SetPrimaryColor = () => {};
export let SetSecondaryColor = () => {};
export let GlobalPrimaryColor = '';
export let GlobalSecondaryColor = '';
const App = () => {
const [open, setOpen] = React.useState(false);
const [message, setMessage] = React.useState('');
setSnackit((message) => {
const [severity, setSeverity] = React.useState('error');
const [statusLoaded, setStatusLoaded] = React.useState(false);
const [PrimaryColor, setPrimaryColor] = React.useState(API.PRIMARY_COLOR);
const [SecondaryColor, setSecondaryColor] = React.useState(API.SECONDARY_COLOR);
const [timeoutError, setTimeoutError] = React.useState(false);
SetPrimaryColor = (color) => {
setPrimaryColor(color);
GlobalPrimaryColor = color;
}
SetSecondaryColor = (color) => {
setSecondaryColor(color);
GlobalSecondaryColor = color;
}
React.useEffect(() => {
const timeout = setTimeout(
() => {
setTimeoutError(true);
}, 10000
)
API.getStatus(true).then((r) => {
clearTimeout(timeout);
if(r == "NOT_AVAILABLE") {
setTimeoutError(true);
}
else if(r) {
setStatusLoaded(true);
}
setPrimaryColor(API.PRIMARY_COLOR);
setSecondaryColor(API.SECONDARY_COLOR);
}).catch(() => {
clearTimeout(timeout);
setStatusLoaded(true);
setPrimaryColor(API.PRIMARY_COLOR);
setSecondaryColor(API.SECONDARY_COLOR);
});
}, []);
setSnackit((message, severity='error') => {
setMessage(message);
setOpen(true);
setSeverity(severity);
})
return (
<ThemeCustomization>
return statusLoaded ?
<ThemeCustomization PrimaryColor={PrimaryColor} SecondaryColor={SecondaryColor}>
<Snackbar
open={open}
autoHideDuration={5000}
onClose={() => {setOpen(false)}}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert className={open ? 'shake' : ''} severity="error" sx={{ width: '100%' }}>
<Alert className={(open && severity == "error") ? 'shake' : ''} severity={severity} sx={{ width: '100%' }}>
{message}
</Alert>
</Snackbar>
@ -33,7 +93,18 @@ const App = () => {
<Routes />
</ScrollTop>
</ThemeCustomization>
)
: <div>
<Box sx={{ position: 'fixed', top: 0, bottom: 0, left: 0, right: 0, display: 'flex', justifyContent: 'center', alignItems: 'center'}}>
{/* <img src={logo} style={{ display:'inline', height: '200px'}} className='pulsing' /> */}
{!timeoutError && <LoadingAnimation />}
{timeoutError && <DisconnectOutlined style={{
fontSize: '200px',
color: 'red',
}}/>}
</Box>
</div>
}
export default App;

View file

@ -0,0 +1,31 @@
import wrap from './wrap';
function login(values) {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function me() {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function logout() {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
export {
login,
logout,
me
};

View file

@ -0,0 +1,78 @@
import configDemo from './demo.config.json';
interface Route {
Name: string;
}
type Operation = 'replace' | 'move_up' | 'move_down' | 'delete' | 'add';
function get() {
return new Promise((resolve, reject) => {
resolve(configDemo)
});
}
function set(values) {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function restart() {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function canSendEmail() {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
"data": {
"canSendEmail": true,
}
})
});
}
async function rawUpdateRoute(routeName: string, operation: Operation, newRoute?: Route): Promise<void> {
return new Promise((resolve, reject) => {
resolve()
});
}
async function replaceRoute(routeName: string, newRoute: Route): Promise<void> {
return rawUpdateRoute(routeName, 'replace', newRoute);
}
async function moveRouteUp(routeName: string): Promise<void> {
return rawUpdateRoute(routeName, 'move_up');
}
async function moveRouteDown(routeName: string): Promise<void> {
return rawUpdateRoute(routeName, 'move_down');
}
async function deleteRoute(routeName: string): Promise<void> {
return rawUpdateRoute(routeName, 'delete');
}
async function addRoute(newRoute: Route): Promise<void> {
return rawUpdateRoute("", 'add', newRoute);
}
export {
get,
set,
restart,
rawUpdateRoute,
replaceRoute,
moveRouteUp,
moveRouteDown,
deleteRoute,
addRoute,
canSendEmail,
};

View file

@ -1,35 +0,0 @@
import wrap from './wrap';
function get() {
return wrap(fetch('/cosmos/api/config', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function set(values) {
return wrap(fetch('/cosmos/api/config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(values),
}))
}
function restart() {
return wrap(fetch('/cosmos/api/restart', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
export {
get,
set,
restart
};

107
client/src/api/config.ts Normal file
View file

@ -0,0 +1,107 @@
import wrap from './wrap';
interface Route {
Name: string;
}
type Operation = 'replace' | 'move_up' | 'move_down' | 'delete' | 'add';
function get() {
return wrap(fetch('/cosmos/api/config', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function set(values) {
return wrap(fetch('/cosmos/api/config', {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(values),
}))
}
function restart() {
return fetch('/cosmos/api/restart', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
})
}
function canSendEmail() {
return fetch('/cosmos/api/can-send-email', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}).then((response) => {
return response.json();
});
}
async function rawUpdateRoute(routeName: string, operation: Operation, newRoute?: Route): Promise<void> {
const payload = {
routeName,
operation,
newRoute,
};
if (operation === 'replace') {
if (!newRoute) throw new Error('newRoute must be provided for replace operation');
}
return wrap(fetch('/cosmos/api/config', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
}));
}
async function replaceRoute(routeName: string, newRoute: Route): Promise<void> {
return rawUpdateRoute(routeName, 'replace', newRoute);
}
async function moveRouteUp(routeName: string): Promise<void> {
return rawUpdateRoute(routeName, 'move_up');
}
async function moveRouteDown(routeName: string): Promise<void> {
return rawUpdateRoute(routeName, 'move_down');
}
async function deleteRoute(routeName: string): Promise<void> {
return rawUpdateRoute(routeName, 'delete');
}
async function addRoute(newRoute: Route): Promise<void> {
return rawUpdateRoute("", 'add', newRoute);
}
function getBackup() {
return wrap(fetch('/cosmos/api/get-backup', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
export {
get,
set,
restart,
rawUpdateRoute,
replaceRoute,
moveRouteUp,
moveRouteDown,
deleteRoute,
addRoute,
canSendEmail,
getBackup,
};

View file

@ -0,0 +1,131 @@
import wrap from './wrap';
function list() {
return new Promise((resolve, reject) => {
resolve({
"data": [
{
"nickname": "admin",
"deviceName": "phone",
"publicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\naACf/...=\n-----END NEBULA X25519 PRIVATE KEY-----\n",
"ip": "192.168.201.4/24",
"isLighthouse": false,
"isRelay": true,
"publicHostname": "",
"port": "4242",
"blocked": false,
"fingerprint": "..."
},
{
"nickname": "admin",
"deviceName": "laptop",
"publicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\n78l4nDEB0+.../36YBQk7dkwg+.=\n-----END NEBULA X25519 PRIVATE KEY-----\n",
"ip": "192.168.201.5/24",
"isLighthouse": false,
"isRelay": true,
"publicHostname": "",
"port": "4242",
"blocked": false,
"fingerprint": "..."
},
{
"nickname": "Martha",
"deviceName": "pink phone",
"publicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\naACf/..=\n-----END NEBULA X25519 PRIVATE KEY-----\n",
"ip": "192.168.201.6/24",
"isLighthouse": false,
"isRelay": true,
"publicHostname": "",
"port": "4242",
"blocked": false,
"fingerprint": "..."
}
],
"status": "OK"
})
});
}
function addDevice(device) {
return new Promise((resolve, reject) => {
resolve({
"data": {
"CA": "-----BEGIN NEBULA CERTIFICATE-----\....\n+dfE+ikL8jUh/n+C+....\....\nZon/Dw==\n-----END NEBULA CERTIFICATE-----\n",
"Config": "constellation_api_key: ...\nconstellation_device_name: test\nconstellation_local_dns_overwrite: true\nconstellation_local_dns_overwrite_address: 192.168.201.1\nconstellation_public_hostname: \"\"\nfirewall:\n conntrack:\n default_timeout: 10m\n tcp_timeout: 12m\n udp_timeout: 3m\n inbound:\n - host: any\n port: any\n proto: any\n inbound_action: drop\n outbound:\n - host: any\n port: any\n proto: any\n outbound_action: drop\nlighthouse:\n am_lighthouse: false\n hosts:\n - 192.168.201.1\n interval: 60\nlisten:\n host: 0.0.0.0\n port: \"4242\"\nlogging:\n format: text\n level: info\npki:\n blocklist: []\n ca: |\n -----BEGIN NEBULA CERTIFICATE-----\n ...\n +dfE+ikL8jUh/n+C+...\n .\n Zon/Dw==\n -----END NEBULA CERTIFICATE-----\n cert: |\n -----BEGIN NEBULA CERTIFICATE-----\n CmIKBHRlc3QSCoeSo4UMgP7//..\n ...+QwZSiBxLdKhjkCH+.../..\n ./hfL+....\n ..==\n -----END NEBULA CERTIFICATE-----\n key: |\n -----BEGIN NEBULA X25519 PRIVATE KEY-----\n nS39dWX7uo1rhTvP2yl2XonGx3fWEkpk+43thNrMu7U=\n -----END NEBULA X25519 PRIVATE KEY-----\npunchy:\n punch: true\n respond: true\nrelay:\n am_relay: false\n relays:\n - 192.168.201.1\n use_relays: true\nstatic_host_map:\n 192.168.201.1:\n - vpn.domain.com:4242\ntun:\n dev: nebula1\n disabled: false\n drop_local_broadcast: false\n drop_multicast: false\n mtu: 1300\n routes: []\n tx_queue: 500\n unsafe_routes: []\n",
"DeviceName": "test",
"IP": "192.168.201.7/24",
"IsLighthouse": false,
"IsRelay": true,
"LighthousesList": [],
"Nickname": "admin",
"Port": "4242",
"PrivateKey": "-----BEGIN NEBULA CERTIFICATE-----\...//w8o3ZaFqQYwhdGFuAY6IGXmYRCr3z932Y....w\..==\n-----END NEBULA CERTIFICATE-----\n",
"PublicHostname": "",
"PublicKey": "-----BEGIN NEBULA X25519 PRIVATE KEY-----\nnS39dWX...hTvP......+43thNrMu7U=\n-----END NEBULA X25519 PRIVATE KEY-----\n"
},
"status": "OK"
})
});
}
function restart() {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function reset() {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function getConfig() {
return new Promise((resolve, reject) => {
resolve({
"data": "pki:\n ca: /config/ca.crt\n cert: /config/cosmos.crt\n key: /config/cosmos.key\n blocklist: []\nstatic_host_map:\n 192.168.201.1:\n - vpn.domain.com:4242\nlighthouse:\n am_lighthouse: true\n interval: 60\n hosts: []\nlisten:\n host: 0.0.0.0\n port: 4242\npunchy:\n punch: true\n respond: true\nrelay:\n am_relay: true\n use_relays: true\n relays: []\ntun:\n disabled: false\n dev: nebula1\n drop_local_broadcast: false\n drop_multicast: false\n tx_queue: 500\n mtu: 1300\n routes: []\n unsafe_routes: []\nlogging:\n level: info\n format: text\nfirewall:\n outbound_action: drop\n inbound_action: drop\n conntrack:\n tcp_timeout: 12m\n udp_timeout: 3m\n default_timeout: 10m\n outbound:\n - port: any\n proto: any\n host: any\n inbound:\n - port: any\n proto: any\n host: any\n",
"status": "OK"
})
});
}
function getLogs() {
return new Promise((resolve, reject) => {
resolve({
"data": "Some logs...",
"status": "OK"
})
});
}
function connect(file) {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function block(nickname, devicename, block) {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
export {
list,
addDevice,
restart,
getConfig,
getLogs,
reset,
connect,
block,
};

View file

@ -0,0 +1,110 @@
import wrap from './wrap';
function list() {
return wrap(fetch('/cosmos/api/constellation/devices', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function addDevice(device) {
return wrap(fetch('/cosmos/api/constellation/devices', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(device),
}))
}
function restart() {
return wrap(fetch('/cosmos/api/constellation/restart', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
function reset() {
return wrap(fetch('/cosmos/api/constellation/reset', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
function getConfig() {
return wrap(fetch('/cosmos/api/constellation/config', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
function getLogs() {
return wrap(fetch('/cosmos/api/constellation/logs', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
function connect(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => {
fetch('/cosmos/api/constellation/connect', {
method: 'POST',
headers: {
'Content-Type': 'text/plain',
},
body: reader.result,
})
.then(response => {
// Add additional response handling here if needed.
resolve(response);
})
.catch(error => {
// Handle the error.
reject(error);
});
};
reader.onerror = () => {
reject(new Error('Failed to read the file.'));
};
reader.readAsText(file);
});
}
function block(nickname, devicename, block) {
return wrap(fetch(`/cosmos/api/constellation/block`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
nickname, devicename, block
}),
}))
}
export {
list,
addDevice,
restart,
getConfig,
getLogs,
reset,
connect,
block,
};

View file

@ -0,0 +1,240 @@
{
"data": {
"Id": "9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a",
"Created": "2023-05-07T16:14:53.9905374Z",
"Path": "/init",
"Args": [],
"State": {
"Status": "running",
"Running": true,
"Paused": false,
"Restarting": false,
"OOMKilled": false,
"Dead": false,
"Pid": 6587,
"ExitCode": 0,
"Error": "",
"StartedAt": "2023-05-08T15:17:06.69445Z",
"FinishedAt": "2023-05-07T17:40:51.0172716Z"
},
"Image": "sha256:ef697cc05e50849d738bd9e83a269db982997be7b7d8f851c84640e1c9a8c1c4",
"ResolvConfPath": "/var/lib/docker/containers/9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a/resolv.conf",
"HostnamePath": "/var/lib/docker/containers/9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a/hostname",
"HostsPath": "/var/lib/docker/containers/9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a/hosts",
"LogPath": "/var/lib/docker/containers/9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a/9219b6ce3b47d27cd53ad015fd33d2e77dd0b247b2ee5760bc21f4031194305a-json.log",
"Name": "/jellyfin",
"RestartCount": 0,
"Driver": "overlay2",
"Platform": "linux",
"MountLabel": "",
"ProcessLabel": "",
"AppArmorProfile": "",
"ExecIDs": null,
"HostConfig": {
"Binds": null,
"ContainerIDFile": "",
"LogConfig": {
"Type": "json-file",
"Config": {}
},
"NetworkMode": "default",
"PortBindings": {},
"RestartPolicy": {
"Name": "no",
"MaximumRetryCount": 0
},
"AutoRemove": false,
"VolumeDriver": "",
"VolumesFrom": null,
"ConsoleSize": [
0,
0
],
"CapAdd": null,
"CapDrop": null,
"CgroupnsMode": "private",
"Dns": [],
"DnsOptions": [],
"DnsSearch": [],
"ExtraHosts": null,
"GroupAdd": null,
"IpcMode": "private",
"Cgroup": "",
"Links": null,
"OomScoreAdj": 0,
"PidMode": "",
"Privileged": false,
"PublishAllPorts": false,
"ReadonlyRootfs": false,
"SecurityOpt": null,
"UTSMode": "",
"UsernsMode": "",
"ShmSize": 67108864,
"Runtime": "runc",
"Isolation": "",
"CpuShares": 0,
"Memory": 0,
"NanoCpus": 0,
"CgroupParent": "",
"BlkioWeight": 0,
"BlkioWeightDevice": [],
"BlkioDeviceReadBps": null,
"BlkioDeviceWriteBps": null,
"BlkioDeviceReadIOps": null,
"BlkioDeviceWriteIOps": null,
"CpuPeriod": 0,
"CpuQuota": 0,
"CpuRealtimePeriod": 0,
"CpuRealtimeRuntime": 0,
"CpusetCpus": "",
"CpusetMems": "",
"Devices": [],
"DeviceCgroupRules": null,
"DeviceRequests": null,
"MemoryReservation": 0,
"MemorySwap": 0,
"MemorySwappiness": null,
"OomKillDisable": null,
"PidsLimit": null,
"Ulimits": null,
"CpuCount": 0,
"CpuPercent": 0,
"IOMaximumIOps": 0,
"IOMaximumBandwidth": 0,
"MaskedPaths": [
"/proc/asound",
"/proc/acpi",
"/proc/kcore",
"/proc/keys",
"/proc/latency_stats",
"/proc/timer_list",
"/proc/timer_stats",
"/proc/sched_debug",
"/proc/scsi",
"/sys/firmware"
],
"ReadonlyPaths": [
"/proc/bus",
"/proc/fs",
"/proc/irq",
"/proc/sys",
"/proc/sysrq-trigger"
]
},
"GraphDriver": {
"Data": {
"LowerDir": "/var/lib/docker/overlay2/51d2becddce22ab73b3ce9bf4acecc98ba3e32e131d0ba7a038598988e58f5e1-init/diff:/var/lib/docker/overlay2/62b6f245e00f4a30491f842e6baf28c8a5162a47a37b1ecb52f8cf3946cd87fc/diff:/var/lib/docker/overlay2/cc6b084bfce23f3e2d19f4b0724265185127583488fbdf9d319bba41af794f46/diff:/var/lib/docker/overlay2/0ce4d4e04f10459993b1887a2f7410571e0e155736baa9f14be6d7bcacfc66cf/diff:/var/lib/docker/overlay2/5af33d768817f5c4c5f120d2b203f3e4fcb70ec9ececdccd36ab4bc678de6c00/diff:/var/lib/docker/overlay2/5e30ac5dd70a3275bd17a906bc95372995ac449346f7bc0e534307e4b0847ee6/diff:/var/lib/docker/overlay2/67570d82c710f15a56f1272c435ae7123056a372cd8ba37430ca9cd74d71ff79/diff:/var/lib/docker/overlay2/ca55695aef06ad37a18d3d773826dec5e00fac8185a69e59705e81af7eaa5b09/diff",
"MergedDir": "/var/lib/docker/overlay2/51d2becddce22ab73b3ce9bf4acecc98ba3e32e131d0ba7a038598988e58f5e1/merged",
"UpperDir": "/var/lib/docker/overlay2/51d2becddce22ab73b3ce9bf4acecc98ba3e32e131d0ba7a038598988e58f5e1/diff",
"WorkDir": "/var/lib/docker/overlay2/51d2becddce22ab73b3ce9bf4acecc98ba3e32e131d0ba7a038598988e58f5e1/work"
},
"Name": "overlay2"
},
"Mounts": [
{
"Type": "volume",
"Name": "MyVolume",
"Source": "/var/lib/docker/volumes/MyVolume/_data",
"Destination": "/config",
"Driver": "local",
"Mode": "",
"RW": true,
"Propagation": ""
}
],
"Config": {
"Hostname": "/jellyfin",
"Domainname": "",
"User": "",
"AttachStdin": false,
"AttachStdout": false,
"AttachStderr": false,
"ExposedPorts": {
"8096/tcp": {},
"8920/tcp": {}
},
"Tty": false,
"OpenStdin": false,
"StdinOnce": false,
"Env": [
"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
"HOME=/root",
"LANGUAGE=en_US.UTF-8",
"LANG=en_US.UTF-8",
"TERM=xterm",
"S6_CMD_WAIT_FOR_SERVICES_MAXTIME=0",
"S6_VERBOSITY=1",
"S6_STAGE2_HOOK=/docker-mods",
"LSIO_FIRST_PARTY=true",
"NVIDIA_DRIVER_CAPABILITIES=compute,video,utility"
],
"Cmd": null,
"Image": "lscr.io/linuxserver/jellyfin:latest",
"Volumes": {
"/config": {}
},
"WorkingDir": "/",
"Entrypoint": [
"/init"
],
"OnBuild": null,
"Labels": {
"build_version": "Linuxserver.io version:- 10.8.9-1-ls203 Build-date:- 2023-03-09T03:55:22+01:00",
"hello": "world",
"maintainer": "thelamer 2",
"org.opencontainers.image.authors": "linuxserver.io",
"org.opencontainers.image.created": "2023-03-09T03:55:22+01:00",
"org.opencontainers.image.description": "[Jellyfin](https://jellyfin.github.io/) is a Free Software Media System that puts you in control of managing and streaming your media. It is an alternative to the proprietary Emby and Plex, to provide media from a dedicated server to end-user devices via multiple apps. Jellyfin is descended from Emby's 3.5.2 release and ported to the .NET Core framework to enable full cross-platform support. There are no strings attached, no premium licenses or features, and no hidden agendas: just a team who want to build something better and work together to achieve it.",
"org.opencontainers.image.documentation": "https://docs.linuxserver.io/images/docker-jellyfin",
"org.opencontainers.image.licenses": "GPL-3.0-only",
"org.opencontainers.image.ref.name": "ba55178b00972a2fbc8e7eec83e47039bfc07cae",
"org.opencontainers.image.revision": "ba55178b00972a2fbc8e7eec83e47039bfc07cae",
"org.opencontainers.image.source": "https://github.com/linuxserver/docker-jellyfin",
"org.opencontainers.image.title": "Jellyfin",
"org.opencontainers.image.url": "https://github.com/linuxserver/docker-jellyfin/packages",
"org.opencontainers.image.vendor": "linuxserver.io",
"org.opencontainers.image.version": "10.8.9-1-ls203"
}
},
"NetworkSettings": {
"Bridge": "",
"SandboxID": "89ef2aa8a50584797af8ef19750429337f78b8a072eb71ea3cc8a332011199d9",
"HairpinMode": false,
"LinkLocalIPv6Address": "",
"LinkLocalIPv6PrefixLen": 0,
"Ports": {
"8096/tcp": null,
"8920/tcp": null
},
"SandboxKey": "/var/run/docker/netns/89ef2aa8a505",
"SecondaryIPAddresses": null,
"SecondaryIPv6Addresses": null,
"EndpointID": "7a376127fcbc669a14fef2e56fbfd761130456a144f548030f2293dcb5d150c8",
"Gateway": "172.17.0.1",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"IPAddress": "172.17.0.4",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"MacAddress": "02:42:ac:11:00:04",
"Networks": {
"bridge": {
"IPAMConfig": null,
"Links": null,
"Aliases": null,
"NetworkID": "d1d39f3e484527f0a22f72275309a6853ea5e84b64c726d75ab3735e5b316eeb",
"EndpointID": "7a376127fcbc669a14fef2e56fbfd761130456a144f548030f2293dcb5d150c8",
"Gateway": "172.17.0.1",
"IPAddress": "172.17.0.4",
"IPPrefixLen": 16,
"IPv6Gateway": "",
"GlobalIPv6Address": "",
"GlobalIPv6PrefixLen": 0,
"MacAddress": "02:42:ac:11:00:04",
"DriverOpts": null
}
}
}
},
"status": "OK"
}

View file

@ -0,0 +1,515 @@
{
"data": {
"LoggingLevel": "INFO",
"MongoDB": "mongodb://cosmos-asda:fdhgaiodfiaushdfiuahsdf@cosmos-mongo-ASD:27017",
"DisableUserManagement": false,
"NewInstall": false,
"HTTPConfig": {
"TLSCert": "",
"TLSKey": "",
"TLSKeyHostsCached": null,
"AuthPrivateKey": "",
"AuthPublicKey": "-----BEGIN PUBLIC KEY-----\nMCowBQYDhf9sadf089a7sdgf8gsd8f7ga89sdfgXVoUD9HYk=\n-----END PUBLIC KEY-----\n",
"GenerateMissingAuthCert": true,
"HTTPSCertificateMode": "LETSENCRYPT",
"DNSChallengeProvider": "",
"HTTPPort": "80",
"HTTPSPort": "443",
"ProxyConfig": {
"Routes": [
{
"Name": "Jellyfin",
"Description": "Expose Jellyfin to the internet",
"UseHost": true,
"Host": "play.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 14400000,
"ThrottlePerMinute": 10000,
"CORSOrigin": "",
"StripPathPrefix": false,
"MaxBandwith": 0,
"AuthEnabled": false,
"AdminOnly": false,
"Target": "http://Jellyfin:8096",
"SmartShield": {
"Enabled": true,
"PolicyStrictness": 0,
"PerUserTimeBudget": 0,
"PerUserRequestLimit": 0,
"PerUserByteLimit": 0,
"PerUserSimultaneous": 0,
"MaxGlobalSimultaneous": 0,
"PrivilegedGroups": 0
},
"Mode": "SERVAPP",
"BlockCommonBots": false,
"BlockAPIAbuse": false
},
{
"Name": "Documents Folder",
"Description": "Share my Documents",
"UseHost": true,
"Host": "documents.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 14400000,
"ThrottlePerMinute": 10000,
"CORSOrigin": "",
"StripPathPrefix": false,
"MaxBandwith": 10000000,
"AuthEnabled": true,
"AdminOnly": false,
"Target": "/Doc",
"SmartShield": {
"Enabled": true,
"PolicyStrictness": 0,
"PerUserTimeBudget": 0,
"PerUserRequestLimit": 0,
"PerUserByteLimit": 0,
"PerUserSimultaneous": 0,
"MaxGlobalSimultaneous": 0,
"PrivilegedGroups": 0
},
"Mode": "STATIC",
"BlockCommonBots": false,
"BlockAPIAbuse": false
},
{
"Name": "Navidrome",
"Description": "Expose navidrome to the internet",
"UseHost": true,
"Host": "navidrome.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 0,
"ThrottlePerMinute": 9000,
"CORSOrigin": "",
"StripPathPrefix": false,
"MaxBandwith": 0,
"AuthEnabled": false,
"AdminOnly": false,
"Target": "http://navidrome:4533",
"SmartShield": {
"Enabled": true,
"PolicyStrictness": 0,
"PerUserTimeBudget": 0,
"PerUserRequestLimit": 0,
"PerUserByteLimit": 0,
"PerUserSimultaneous": 0,
"MaxGlobalSimultaneous": 0,
"PrivilegedGroups": 0
},
"Mode": "SERVAPP",
"BlockCommonBots": false,
"BlockAPIAbuse": false
},
{
"Name": "Plex",
"Description": "Expose Plex to the internet",
"UseHost": true,
"Host": "plex.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 0,
"ThrottlePerMinute": 0,
"CORSOrigin": "",
"StripPathPrefix": false,
"MaxBandwith": 0,
"AuthEnabled": false,
"AdminOnly": false,
"Target": "http://Plex:32400",
"SmartShield": {
"Enabled": true,
"PolicyStrictness": 2,
"PerUserTimeBudget": 0,
"PerUserRequestLimit": 0,
"PerUserByteLimit": 0,
"PerUserSimultaneous": 0,
"MaxGlobalSimultaneous": 0,
"PrivilegedGroups": 2
},
"Mode": "SERVAPP",
"BlockCommonBots": false,
"BlockAPIAbuse": false
},
{
"Name": "Radarr",
"Description": "Expose Radarr to the internet",
"UseHost": true,
"Host": "radarr.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 30000,
"ThrottlePerMinute": 2000,
"CORSOrigin": "",
"StripPathPrefix": false,
"MaxBandwith": 0,
"AuthEnabled": true,
"AdminOnly": true,
"Target": "http://Radarr:7878",
"SmartShield": {
"Enabled": true,
"PolicyStrictness": 0,
"PerUserTimeBudget": 0,
"PerUserRequestLimit": 0,
"PerUserByteLimit": 0,
"PerUserSimultaneous": 0,
"MaxGlobalSimultaneous": 0,
"PrivilegedGroups": 0
},
"Mode": "SERVAPP",
"BlockCommonBots": false,
"BlockAPIAbuse": false
},
{
"Name": "Sonarr",
"Description": "Expose Sonarr to the internet",
"UseHost": true,
"Host": "sonarr.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 30000,
"ThrottlePerMinute": 200,
"CORSOrigin": "",
"StripPathPrefix": false,
"MaxBandwith": 0,
"AuthEnabled": true,
"AdminOnly": true,
"Target": "http://Sonarr:8989",
"SmartShield": {
"Enabled": true,
"PolicyStrictness": 0,
"PerUserTimeBudget": 0,
"PerUserRequestLimit": 0,
"PerUserByteLimit": 0,
"PerUserSimultaneous": 0,
"MaxGlobalSimultaneous": 0,
"PrivilegedGroups": 0
},
"Mode": "SERVAPP",
"BlockCommonBots": false,
"BlockAPIAbuse": false
},
{
"Name": "nzbget",
"Description": "Expose nzbget to the internet",
"UseHost": true,
"Host": "nzbget.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 999999999,
"ThrottlePerMinute": 3000,
"CORSOrigin": "",
"StripPathPrefix": false,
"MaxBandwith": 0,
"AuthEnabled": true,
"AdminOnly": true,
"Target": "http://nzbget:6789",
"SmartShield": {
"Enabled": true,
"PolicyStrictness": 0,
"PerUserTimeBudget": 0,
"PerUserRequestLimit": 0,
"PerUserByteLimit": 0,
"PerUserSimultaneous": 0,
"MaxGlobalSimultaneous": 0,
"PrivilegedGroups": 0
},
"Mode": "SERVAPP",
"BlockCommonBots": false,
"BlockAPIAbuse": false
},
{
"Name": "photoprism",
"Description": "Expose photoprism to the internet",
"UseHost": true,
"Host": "photoprism.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 45000,
"ThrottlePerMinute": 5000,
"CORSOrigin": "",
"StripPathPrefix": false,
"MaxBandwith": 0,
"AuthEnabled": false,
"AdminOnly": false,
"Target": "http://photoprism:2342",
"SmartShield": {
"Enabled": true,
"PolicyStrictness": 0,
"PerUserTimeBudget": 0,
"PerUserRequestLimit": 0,
"PerUserByteLimit": 0,
"PerUserSimultaneous": 0,
"MaxGlobalSimultaneous": 0,
"PrivilegedGroups": 0
},
"Mode": "SERVAPP",
"BlockCommonBots": false,
"BlockAPIAbuse": false
},
{
"Name": "Owncloud",
"Description": "Expose Owncloud to the internet",
"UseHost": true,
"Host": "owncloud.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 0,
"ThrottlePerMinute": 2000,
"CORSOrigin": "",
"StripPathPrefix": false,
"MaxBandwith": 0,
"AuthEnabled": false,
"AdminOnly": false,
"Target": "http://Owncloud:8080",
"SmartShield": {
"Enabled": true,
"PolicyStrictness": 0,
"PerUserTimeBudget": 0,
"PerUserRequestLimit": 0,
"PerUserByteLimit": 0,
"PerUserSimultaneous": 0,
"MaxGlobalSimultaneous": 0,
"PrivilegedGroups": 0
},
"Mode": "SERVAPP",
"BlockCommonBots": false,
"BlockAPIAbuse": false
},
{
"Name": "Lidarr",
"Description": "Expose Lidarr to the internet",
"UseHost": true,
"Host": "lidarr.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 30000,
"ThrottlePerMinute": 2000,
"CORSOrigin": "",
"StripPathPrefix": false,
"MaxBandwith": 0,
"AuthEnabled": true,
"AdminOnly": true,
"Target": "http://Lidarr:8686",
"SmartShield": {
"Enabled": true,
"PolicyStrictness": 0,
"PerUserTimeBudget": 0,
"PerUserRequestLimit": 0,
"PerUserByteLimit": 0,
"PerUserSimultaneous": 0,
"MaxGlobalSimultaneous": 0,
"PrivilegedGroups": 0
},
"Mode": "SERVAPP",
"BlockCommonBots": false,
"BlockAPIAbuse": false
}
]
},
"Hostname": "xxxxxxxxxxxxx",
"SSLEmail": "myemail@gmail.com"
},
"MonitoringAlerts": {
"Anti Crypto-Miner": {
"Name": "Anti Crypto-Miner",
"Enabled": true,
"Period": "daily",
"TrackingMetric": "cosmos.system.docker.cpu.*",
"Condition": {
"Operator": "gt",
"Value": 80
},
"Actions": [
{
"Type": "notification",
"Target": ""
},
{
"Type": "email",
"Target": ""
},
{
"Type": "stop",
"Target": ""
}
],
"LastTriggered": "0001-01-01T00:00:00Z",
"Throttled": false,
"Severity": "warn"
},
"Anti Memory Leak": {
"Name": "Anti Memory Leak",
"Enabled": true,
"Period": "daily",
"TrackingMetric": "cosmos.system.docker.ram.*",
"Condition": {
"Percent": true,
"Operator": "gt",
"Value": 80
},
"Actions": [
{
"Type": "notification",
"Target": ""
},
{
"Type": "email",
"Target": ""
},
{
"Type": "stop",
"Target": ""
}
],
"LastTriggered": "0001-01-01T00:00:00Z",
"Throttled": false,
"Severity": "warn"
},
"Disk Full Notification": {
"Name": "Disk Full Notification",
"Enabled": true,
"Period": "latest",
"TrackingMetric": "cosmos.system.disk./",
"Condition": {
"Percent": true,
"Operator": "gt",
"Value": 95
},
"Actions": [
{
"Type": "notification",
"Target": ""
}
],
"LastTriggered": "0001-01-01T00:00:00Z",
"Throttled": true,
"Severity": "warn"
}
},
"EmailConfig": {
"Enabled": true,
"Host": "smtp.gmail.com",
"Port": "587",
"Username": "asdasdasd@gmail.com",
"Password": "ufahsd9f9asf",
"From": "Cosmos Server Admin",
"UseTLS": true
},
"DockerConfig": {
"SkipPruneNetwork": false
},
"OpenIDClients": [
{
"id": "gitea",
"secret": "$2a$10$406wbQbinog/zqpnc6amSu1UArA.zVrb/KuRkaBGJYA4oruGnxUga",
"redirect": "http://localhost:3000/user/oauth2/Cosmos/callback"
},
{
"id": "minio",
"secret": "$2a$10$cE30L/Ik3ThX0G8KX6663ujmDC5UsqAsbMGqE6zRKjI0WFD6zV.N6",
"redirect": "http://localhost:9090/oauth_callback"
},
{
"id": "nextcloud",
"secret": "$2a$10$IcpiICqki2cBnZc1.VOaYu0SPxKx6sXWyly44s0hsSNYMyfibsVAy",
"redirect": "https://localhost:12443/apps/oidc_login/oidc"
}
],
"BlockedCountries": [
"CN",
"RU",
"TR",
"BR",
"BD",
"IN",
"NP",
"PK",
"LK",
"VN",
"ID",
"IR",
"IQ",
"EG",
"AF",
"RO"
],
"ServerCountry": "",
"RequireMFA": false,
"AutoUpdate": false,
"ConstellationConfig": {
"Enabled": true,
"SlaveMode": false,
"PrivateNode": false,
"DNSDisabled": false,
"DNSPort": "",
"DNSFallback": "8.8.8.8:53",
"DNSBlockBlacklist": true,
"DNSAdditionalBlocklists": [
"https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
"https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt"
],
"CustomDNSEntries": [],
"NebulaConfig": {
"PKI": {
"CA": "",
"Cert": "",
"Key": "",
"Blocklist": null
},
"StaticHostMap": null,
"Lighthouse": {
"AMLighthouse": false,
"Interval": 0,
"Hosts": null
},
"Listen": {
"Host": "",
"Port": 0
},
"Punchy": {
"Punch": false,
"Respond": false
},
"Relay": {
"AMRelay": true,
"UseRelays": false,
"Relays": null
},
"TUN": {
"Disabled": false,
"Dev": "",
"DropLocalBroadcast": false,
"DropMulticast": false,
"TxQueue": 0,
"MTU": 0,
"Routes": null,
"UnsafeRoutes": null
},
"Logging": {
"Level": "",
"Format": ""
},
"Firewall": {
"OutboundAction": "",
"InboundAction": "",
"Conntrack": {
"TCPTimeout": "",
"UDPTimeout": "",
"DefaultTimeout": ""
},
"Outbound": null,
"Inbound": null
}
},
"ConstellationHostname": "vpn.domain.com"
}
},
"updates": {
"/Sonarr": true,
"/Jellyfin": true
},
"status": "OK"
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,230 @@
import configDemo from './docker.demo.json';
import configDemoCont from './container.demo.json';
import volumesDemo from './volumes.demo.json';
import networkDemo from './networks.demo.json';
import logDemo from './logs.demo.json';
function list() {
return new Promise((resolve, reject) => {
resolve(configDemo)
});
}
function get(containerName) {
return new Promise((resolve, reject) => {
resolve(configDemoCont)
});
}
function secure(id, res) {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
const newDB = () => {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
const manageContainer = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function updateContainer(containerId, values) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function listContainerNetworks(containerId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function createNetwork(values) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function attachNetwork(containerId, networkId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function detachNetwork(containerId, networkId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function createVolume(values) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function volumeList() {
return new Promise((resolve, reject) => {
resolve(volumesDemo)
});
}
function volumeDelete(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function networkList() {
return new Promise((resolve, reject) => {
resolve(networkDemo)
});
}
function networkDelete(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function getContainerLogs(containerId, searchQuery, limit, lastReceivedLogs, errorOnly) {
if(limit < 50) limit = 50;
return new Promise((resolve, reject) => {
resolve(logDemo)
});
}
function attachTerminal(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
100
);
});
}
function createTerminal(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
100
);
});
}
function autoUpdate(name) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
1000
);
});
}
function pullImage(imageName, onProgress, ifMissing) {
onProgress('Updating...')
new Promise((resolve, reject) => {
setTimeout(() => {
onProgress('Download in progress...')
},
1000
);
});
return new Promise((resolve, reject) => {
setTimeout(() => {
onProgress('[OPERATION SUCCEEDED]')
},
2500
);
});
}
const updateContainerImage = pullImage;
const createService = pullImage;
export {
list,
get,
newDB,
secure,
manageContainer,
volumeList,
volumeDelete,
networkList,
networkDelete,
getContainerLogs,
updateContainer,
listContainerNetworks,
createNetwork,
attachNetwork,
detachNetwork,
createVolume,
attachTerminal,
createTerminal,
createService,
pullImage,
autoUpdate,
updateContainerImage,
};

View file

@ -9,6 +9,70 @@ function list() {
}))
}
function get(containerName) {
return wrap(fetch('/cosmos/api/servapps/' + containerName, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function getContainerLogs(containerId, searchQuery, limit, lastReceivedLogs, errorOnly) {
if(limit < 50) limit = 50;
const queryParams = new URLSearchParams({
search: searchQuery || "",
limit: limit || "",
lastReceivedLogs: lastReceivedLogs || "",
errorOnly: errorOnly || "",
});
return wrap(fetch(`/cosmos/api/servapps/${containerId}/logs?${queryParams}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}));
}
function volumeList() {
return wrap(fetch('/cosmos/api/volumes', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function volumeDelete(name) {
return wrap(fetch(`/cosmos/api/volume/${name}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
}))
}
function networkList() {
return wrap(fetch('/cosmos/api/networks', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function networkDelete(name) {
return wrap(fetch(`/cosmos/api/network/${name}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
}))
}
function secure(id, res) {
return wrap(fetch('/cosmos/api/servapps/' + id + '/secure/'+res, {
method: 'GET',
@ -26,9 +90,262 @@ const newDB = () => {
}
}))
}
const manageContainer = (containerId, action) => {
return wrap(fetch('/cosmos/api/servapps/' + containerId + '/manage/' + action, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
function updateContainer(containerId, values) {
return wrap(fetch('/cosmos/api/servapps/' + containerId + '/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(values),
}))
}
function listContainerNetworks(containerId) {
return wrap(fetch('/cosmos/api/servapps/' + containerId + '/networks', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
function createNetwork(values) {
return wrap(fetch('/cosmos/api/networks', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(values),
}))
}
function attachNetwork(containerId, networkId) {
return wrap(fetch('/cosmos/api/servapps/' + containerId + '/network/' + networkId, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
}))
}
function detachNetwork(containerId, networkId) {
return wrap(fetch('/cosmos/api/servapps/' + containerId + '/network/' + networkId, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}))
}
function createVolume(values) {
return wrap(fetch('/cosmos/api/volumes', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(values),
}))
}
function attachTerminal(containerId) {
let protocol = 'ws://';
if (window.location.protocol === 'https:') {
protocol = 'wss://';
}
return new WebSocket(protocol + window.location.host + '/cosmos/api/servapps/' + containerId + '/terminal/attach');
}
function createTerminal(containerId) {
let protocol = 'ws://';
if (window.location.protocol === 'https:') {
protocol = 'wss://';
}
return new WebSocket(protocol + window.location.host + '/cosmos/api/servapps/' + containerId + '/terminal/new');
}
function createService(serviceData, onProgress) {
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(serviceData)
};
return fetch('/cosmos/api/docker-service', requestOptions)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
// The response body is a ReadableStream. This code reads the stream and passes chunks to the callback.
const reader = response.body.getReader();
// Read the stream and pass chunks to the callback as they arrive
return new ReadableStream({
start(controller) {
function read() {
return reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
// Decode the UTF-8 text
let text = new TextDecoder().decode(value);
// Split by lines in case there are multiple lines in one chunk
let lines = text.split('\n');
for (let line of lines) {
if (line) {
// Call the progress callback
onProgress(line);
}
}
controller.enqueue(value);
return read();
});
}
return read();
}
});
});
}
function pullImage(imageName, onProgress, ifMissing) {
const requestOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
};
const imageNameEncoded = encodeURIComponent(imageName);
return fetch(`/cosmos/api/images/${ifMissing ? 'pull-if-missing' : 'pull'}?imageName=${imageNameEncoded}`, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
// The response body is a ReadableStream. This code reads the stream and passes chunks to the callback.
const reader = response.body.getReader();
// Read the stream and pass chunks to the callback as they arrive
return new ReadableStream({
start(controller) {
function read() {
return reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
// Decode the UTF-8 text
let text = new TextDecoder().decode(value);
// Split by lines in case there are multiple lines in one chunk
let lines = text.split('\n');
for (let line of lines) {
if (line) {
// Call the progress callback
onProgress(line);
}
}
controller.enqueue(value);
return read();
});
}
return read();
}
});
});
}
function updateContainerImage(containerName, onProgress) {
const requestOptions = {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
};
const containerNameEncoded = encodeURIComponent(containerName);
return fetch(`/cosmos/api/servapps/${containerNameEncoded}/manage/update`, requestOptions)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
// The response body is a ReadableStream. This code reads the stream and passes chunks to the callback.
const reader = response.body.getReader();
// Read the stream and pass chunks to the callback as they arrive
return new ReadableStream({
start(controller) {
function read() {
return reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
// Decode the UTF-8 text
let text = new TextDecoder().decode(value);
// Split by lines in case there are multiple lines in one chunk
let lines = text.split('\n');
for (let line of lines) {
if (line) {
// Call the progress callback
onProgress(line);
}
}
controller.enqueue(value);
return read();
});
}
return read();
}
});
});
}
function autoUpdate(id, toggle) {
return wrap(fetch('/cosmos/api/servapps/' + id + '/auto-update/'+toggle, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
export {
list,
get,
newDB,
secure
secure,
manageContainer,
volumeList,
volumeDelete,
networkList,
networkDelete,
getContainerLogs,
updateContainer,
listContainerNetworks,
createNetwork,
attachNetwork,
detachNetwork,
createVolume,
attachTerminal,
createTerminal,
createService,
pullImage,
autoUpdate,
updateContainerImage,
};

View file

@ -0,0 +1,49 @@
import { ArrowDownOutlined } from "@ant-design/icons";
import { Button } from "@mui/material";
import ResponsiveButton from "../components/responseiveButton";
export const DownloadFile = ({ filename, content, contentGetter, label, style }) => {
const downloadFile = async () => {
// Get the content
if (contentGetter) {
try {
content = await contentGetter();
if(typeof content !== "string") {
content = JSON.stringify(content, null, 2);
}
} catch (e) {
console.error(e);
return;
}
}
// Create a blob with the content
const blob = new Blob([content], { type: "text/plain;charset=utf-8" });
// Create a link element
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
// Append the link to the document (needed for Firefox)
document.body.appendChild(link);
// Simulate a click to start the download
link.click();
// Cleanup the DOM by removing the link element
document.body.removeChild(link);
}
return (
<ResponsiveButton
color="primary"
onClick={downloadFile}
style={style}
variant={"outlined"}
startIcon={<ArrowDownOutlined />}
>
{label}
</ResponsiveButton>
);
}

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,89 @@
export const getStatus = () => {
return new Promise((resolve, reject) => {
resolve({
"data": {
"AVX": true,
"CPU": "amd64",
"HTTPSCertificateMode": "LETSENCRYPT",
"LetsEncryptErrors": [],
"MonitoringDisabled": false,
"backup_status": "",
"database": true,
"docker": true,
"domain": false,
"homepage": {
"Background": "/cosmos/api/background/avif",
"Widgets": null,
"Expanded": false
},
"hostname": "yann-server.com",
"letsencrypt": false,
"needsRestart": false,
"newVersionAvailable": false,
"resources": {},
"theme": {
"PrimaryColor": "rgba(191, 100, 64, 1)",
"SecondaryColor": ""
}
},
"status": "OK"
});
});
}
export const isOnline = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
export const newInstall = (req) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
export const getDNS = (host) => (req) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
"data": "199.199.199.199"
})},
100
);
});
}
export const checkHost = (host) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
"data": "199.199.199.199"
})},
100
);
});
}
export const uploadImage = (file) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
"data": ""
})}, 100 );
});
}

View file

@ -1,27 +1,237 @@
import * as auth from './authentication.jsx';
import * as users from './users.jsx';
import * as config from './config.jsx';
import * as docker from './docker.jsx';
import * as _auth from './authentication';
import * as _users from './users';
import * as _config from './config';
import * as _docker from './docker';
import * as _market from './market';
import * as _constellation from './constellation';
import * as _metrics from './metrics';
import * as authDemo from './authentication.demo';
import * as usersDemo from './users.demo';
import * as configDemo from './config.demo';
import * as dockerDemo from './docker.demo';
import * as indexDemo from './index.demo';
import * as marketDemo from './market.demo';
import * as constellationDemo from './constellation.demo';
import * as metricsDemo from './metrics.demo';
import wrap from './wrap';
import { redirectToLocal } from '../utils/indexs';
const getStatus = () => {
export let CPU_ARCH = 'amd64';
export let CPU_AVX = true;
export let HOME_BACKGROUND;
export let PRIMARY_COLOR;
export let SECONDARY_COLOR;
export let FIRST_LOAD = false;
let getStatus = (initial) => {
return wrap(fetch('/cosmos/api/status', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}), initial)
.then(async (response) => {
CPU_ARCH = response.data.CPU;
CPU_AVX = response.data.AVX;
HOME_BACKGROUND = response.data.homepage.Background;
PRIMARY_COLOR = response.data.theme.PrimaryColor;
SECONDARY_COLOR = response.data.theme.SecondaryColor;
FIRST_LOAD = true;
return response
}).catch((response) => {
const urlSearch = encodeURIComponent(window.location.search);
const redirectToURL = (window.location.pathname + urlSearch);
if(response.status != 'OK') {
if(
window.location.href.indexOf('/cosmos-ui/newInstall') == -1 &&
window.location.href.indexOf('/cosmos-ui/login') == -1 &&
window.location.href.indexOf('/cosmos-ui/loginmfa') == -1 &&
window.location.href.indexOf('/cosmos-ui/newmfa') == -1 &&
window.location.href.indexOf('/cosmos-ui/register') == -1 &&
window.location.href.indexOf('/cosmos-ui/forgot-password') == -1) {
if(response.status == 'NEW_INSTALL') {
redirectToLocal('/cosmos-ui/newInstall');
} else if (response.status == 'error' && response.code == "HTTP004") {
redirectToLocal('/cosmos-ui/login?redirect=' + redirectToURL);
} else if (response.status == 'error' && response.code == "HTTP006") {
redirectToLocal('/cosmos-ui/loginmfa?redirect=' + redirectToURL);
} else if (response.status == 'error' && response.code == "HTTP007") {
redirectToLocal('/cosmos-ui/newmfa?redirect=' + redirectToURL);
}
} else {
return "nothing";
}
}
return "NOT_AVAILABLE";
});
}
const newInstall = (req) => {
return wrap(fetch('/cosmos/api/newInstall', {
method: 'POST',
let isOnline = () => {
return fetch('/cosmos/api/status', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
}))
}
}).then(async (response) => {
let rep;
try {
rep = await response.json();
} catch {
throw new Error('Server error');
}
if (response.status == 200) {
return rep;
}
const e = new Error(rep.message);
e.status = response.status;
throw e;
});
}
let newInstall = (req, onProgress) => {
if(req.step == '2') {
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
};
return fetch('/cosmos/api/newInstall', requestOptions)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
// The response body is a ReadableStream. This code reads the stream and passes chunks to the callback.
const reader = response.body.getReader();
// Read the stream and pass chunks to the callback as they arrive
return new ReadableStream({
start(controller) {
function read() {
return reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
// Decode the UTF-8 text
let text = new TextDecoder().decode(value);
// Split by lines in case there are multiple lines in one chunk
let lines = text.split('\n');
for (let line of lines) {
if (line) {
// Call the progress callback
onProgress(line);
}
}
controller.enqueue(value);
return read();
});
}
return read();
}
});
}).catch((e) => {
console.error(e);
});
} else {
return wrap(fetch('/cosmos/api/newInstall', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
}))
}
}
let checkHost = (host) => {
return fetch('/cosmos/api/dns-check?url=' + host, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (response) => {
let rep;
try {
rep = await response.json();
} catch {
throw new Error('Server error');
}
if (response.status == 200) {
return rep;
}
const e = new Error(rep.message);
e.status = response.status;
e.message = rep.message;
throw e;
});
}
let getDNS = (host) => {
return fetch('/cosmos/api/dns?url=' + host, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (response) => {
let rep;
try {
rep = await response.json();
} catch {
throw new Error('Server error');
}
if (response.status == 200) {
return rep;
}
const e = new Error(rep.message);
e.status = response.status;
e.message = rep.message;
throw e;
});
}
let uploadImage = (file, name) => {
const formData = new FormData();
formData.append('image', file);
return wrap(fetch('/cosmos/api/upload/' + name, {
method: 'POST',
body: formData
}));
};
const isDemo = import.meta.env.MODE === 'demo';
let auth = _auth;
let users = _users;
let config = _config;
let docker = _docker;
let market = _market;
let constellation = _constellation;
let metrics = _metrics;
if(isDemo) {
auth = authDemo;
users = usersDemo;
config = configDemo;
docker = dockerDemo;
market = marketDemo;
getStatus = indexDemo.getStatus;
newInstall = indexDemo.newInstall;
isOnline = indexDemo.isOnline;
checkHost = indexDemo.checkHost;
getDNS = indexDemo.getDNS;
uploadImage = indexDemo.uploadImage;
constellation = constellationDemo;
metrics = metricsDemo;
}
export {
@ -29,6 +239,13 @@ export {
users,
config,
docker,
market,
constellation,
getStatus,
newInstall,
isOnline,
checkHost,
getDNS,
metrics,
uploadImage
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,11 @@
import marketDemo from './market.demo.json';
function list() {
return new Promise((resolve, reject) => {
resolve(marketDemo);
});
}
export {
list,
};

14
client/src/api/market.ts Normal file
View file

@ -0,0 +1,14 @@
import wrap from './wrap';
function list() {
return wrap(fetch('/cosmos/api/markets', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
export {
list,
};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,44 @@
import wrap from './wrap';
function get(metarr) {
return wrap(fetch('/cosmos/api/metrics?metrics=' + metarr.join(','), {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function reset() {
return wrap(fetch('/cosmos/api/reset-metrics', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function list() {
return wrap(fetch('/cosmos/api/list-metrics', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function events(from, to, search = '', query = '', page = '', logLevel) {
return wrap(fetch('/cosmos/api/events?from=' + from + '&to=' + to + '&search=' + search + '&query=' + query + '&page=' + page + '&logLevel=' + logLevel, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
export {
get,
reset,
list,
events
};

View file

@ -0,0 +1,170 @@
{
"data": [
{
"Name": "test2",
"Id": "6675d361c126d013dbc4cba07c140821879718af3d52f15a07aa94dabbffe124",
"Created": "2023-05-08T11:18:08.4273302Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.18.0.0/16",
"Gateway": "172.18.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
},
{
"Name": "bridge",
"Id": "d1d39f3e484527f0a22f72275309a6853ea5e84b64c726d75ab3735e5b316eeb",
"Created": "2023-05-08T11:06:04.195972Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.17.0.0/16",
"Gateway": "172.17.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {
"com.docker.network.bridge.default_bridge": "true",
"com.docker.network.bridge.enable_icc": "true",
"com.docker.network.bridge.enable_ip_masquerade": "true",
"com.docker.network.bridge.host_binding_ipv4": "0.0.0.0",
"com.docker.network.bridge.name": "docker0",
"com.docker.network.driver.mtu": "1500"
},
"Labels": {}
},
{
"Name": "Also Test",
"Id": "3128c9c7e83a03b297249a6c8df437d6a9656a405efd0c3f937aee7e7b1d993d",
"Created": "2023-05-07T17:28:12.2844952Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.26.0.0/16",
"Gateway": "172.26.0.1"
}
]
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
},
{
"Name": "host",
"Id": "40f43ad5c13390b477d496a57cd3f2eacef3b92262de2429e369e5655d1fe13a",
"Created": "2020-03-15T16:34:57.75505446Z",
"Scope": "local",
"Driver": "host",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": []
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
},
{
"Name": "none",
"Id": "d2fd80f8cd33c60a81487eec8b7e663acf539479411c8d38d234b7943403270f",
"Created": "2020-03-15T16:34:57.74630246Z",
"Scope": "local",
"Driver": "null",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": []
},
"Internal": false,
"Attachable": false,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
},
{
"Name": "cosmos-network-Y5UKzxkrk",
"Id": "37357fc736ac7510162d036033c4bc3b50955accc484a473bc78b21c9fe860e5",
"Created": "2023-05-08T13:28:43.0766901Z",
"Scope": "local",
"Driver": "bridge",
"EnableIPv6": false,
"IPAM": {
"Driver": "default",
"Options": null,
"Config": [
{
"Subnet": "172.20.0.0/16",
"Gateway": "172.20.0.1"
}
]
},
"Internal": false,
"Attachable": true,
"Ingress": false,
"ConfigFrom": {
"Network": ""
},
"ConfigOnly": false,
"Containers": {},
"Options": {},
"Labels": {}
}
],
"status": "OK"
}

View file

@ -0,0 +1,53 @@
{
"data": [
{
"nickname": "Cosmos",
"registerKey": "",
"registerKeyExp": "0001-01-01T00:00:00Z",
"role": 2,
"link": "/api/user/Cosmos",
"email": "",
"registeredAt": "2023-04-01T16:40:34.169Z",
"lastPasswordChangedAt": "0001-01-01T00:00:00Z",
"createdAt": "2023-04-01T16:40:34.169Z",
"lastLogin": "2023-04-30T22:17:26.463Z"
},
{
"nickname": "Gertrude",
"registerKey": "",
"registerKeyExp": "0001-01-01T00:00:00Z",
"role": 1,
"link": "/api/user/Gertrude",
"email": "",
"registeredAt": "2023-04-29T12:20:09.268Z",
"lastPasswordChangedAt": "2023-04-29T12:20:09.268Z",
"createdAt": "2023-04-29T11:46:36.521Z",
"lastLogin": "2023-04-30T16:37:38.815Z"
},
{
"nickname": "John",
"registerKey": "",
"registerKeyExp": "0001-01-01T00:00:00Z",
"role": 1,
"link": "/api/user/John",
"email": "",
"registeredAt": "2023-04-29T13:31:59.04Z",
"lastPasswordChangedAt": "2023-04-29T13:31:59.04Z",
"createdAt": "2023-04-29T13:10:23.515Z",
"lastLogin": "2023-04-29T13:33:31.982Z"
},
{
"nickname": "Wilfred",
"registerKey": "4S4ALTTl5Z9ROiY1tqFgT46xPiNk9FU9FI83WsOT15NXKhz6",
"registerKeyExp": "2023-05-08T15:14:21.856Z",
"role": 1,
"link": "/api/user/Wilfred",
"email": "wilfred@gmail.com",
"registeredAt": "0001-01-01T00:00:00Z",
"lastPasswordChangedAt": "0001-01-01T00:00:00Z",
"createdAt": "2023-05-01T15:14:21.812Z",
"lastLogin": "0001-01-01T00:00:00Z"
}
],
"status": "OK"
}

View file

@ -0,0 +1,150 @@
import configDemo from './users.demo.json';
function list() {
return new Promise((resolve, reject) => {
resolve(configDemo)
});
}
function create(values) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function register(values) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function invite(values) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function edit(nickname, values) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function get(nickname) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function deleteUser(nickname) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function new2FA(nickname) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function check2FA(values) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function reset2FA(values) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function resetPassword(values) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
function getNotifs() {
return new Promise((resolve, reject) => {
resolve({"data":[{"ID":"654b0fccce74bf6f8c8ccc61","Title":"Container Update","Message":"Container Prowlarr updated to the latest version!","Icon":"","Link":"/cosmos-ui/servapps/containers/Prowlarr","Date":"2023-11-08T04:33:39.041Z","Level":"info","Read":false,"Recipient":"admin","Actions":null},{"ID":"654b0fccce74bf6f8c8ccc60","Title":"Container Update","Message":"Container Lidarr updated to the latest version!","Icon":"","Link":"/cosmos-ui/servapps/containers/Lidarr","Date":"2023-11-08T04:33:29.779Z","Level":"info","Read":false,"Recipient":"admin","Actions":null},{"ID":"654a589ff54b04d499103b19","Title":"Container Update","Message":"Container transmission updated to the latest version!","Icon":"","Link":"/cosmos-ui/servapps/containers/transmission","Date":"2023-11-07T15:31:56.385Z","Level":"info","Read":true,"Recipient":"admin","Actions":null},{"ID":"6547ff777b87120576934c61","Title":"Container Update","Message":"Container Jellyfin updated to the latest version!","Icon":"","Link":"/cosmos-ui/servapps/containers/Jellyfin","Date":"2023-11-05T20:47:04.658Z","Level":"info","Read":true,"Recipient":"admin","Actions":null},{"ID":"6547ff777b87120576934c60","Title":"Container Update","Message":"Container logs updated to the latest version!","Icon":"","Link":"/cosmos-ui/servapps/containers/logs","Date":"2023-11-05T20:46:56.503Z","Level":"info","Read":true,"Recipient":"admin","Actions":null}],"status":"OK"})
});
}
function readNotifs(notifs) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
})},
2000
);
});
}
export {
list,
create,
register,
invite,
edit,
get,
deleteUser,
new2FA,
check2FA,
reset2FA,
resetPassword,
getNotifs,
readNotifs
};

View file

@ -67,6 +67,67 @@ function deleteUser(nickname) {
}))
}
function new2FA(nickname) {
return wrap(fetch('/cosmos/api/mfa', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function check2FA(values) {
return wrap(fetch('/cosmos/api/mfa', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
token: values
}),
}))
}
function reset2FA(values) {
return wrap(fetch('/cosmos/api/mfa', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
nickname: values
}),
}))
}
function resetPassword(values) {
return wrap(fetch('/cosmos/api/password-reset', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(values),
}))
}
function getNotifs() {
return wrap(fetch('/cosmos/api/notifications', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
function readNotifs(notifs) {
return wrap(fetch('/cosmos/api/notifications/read?ids=' + notifs.join(','), {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
}))
}
export {
list,
create,
@ -75,4 +136,10 @@ export {
edit,
get,
deleteUser,
new2FA,
check2FA,
reset2FA,
resetPassword,
getNotifs,
readNotifs,
};

View file

@ -0,0 +1,161 @@
{
"data": {
"Volumes": [
{
"CreatedAt": "2023-05-02T13:26:13Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/c4106d6074bc26e4f32858d750b7bac534d4f801396366afdd2df92f32229c21/_data",
"Name": "c4106d6074bc26e4f32858d750b7bac534d4f801396366afdd2df92f32229c21",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-04-10T19:59:09Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/fb7fd95481d9975953dcd967ea93addc95e6e93d16eb305fe9d4e43882bc4ad2/_data",
"Name": "fb7fd95481d9975953dcd967ea93addc95e6e93d16eb305fe9d4e43882bc4ad2",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-05-07T16:36:48Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/test3/_data",
"Name": "test3",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-03-25T17:00:47Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/1bc3e0f8ea49eb7482f8255a2525b0419d919611e62f0536517f79f3a258d278/_data",
"Name": "1bc3e0f8ea49eb7482f8255a2525b0419d919611e62f0536517f79f3a258d278",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-05-07T13:09:19Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/52573b0b23018e5412182e7ebdf60d684790616ffdfe7424b063acbfdd078bd8/_data",
"Name": "52573b0b23018e5412182e7ebdf60d684790616ffdfe7424b063acbfdd078bd8",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-04-10T19:33:39Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/740c05e5c53a7e9df5954ed82ab3a9987e666c2b6030cd66e3c5f5320a9d0053/_data",
"Name": "740c05e5c53a7e9df5954ed82ab3a9987e666c2b6030cd66e3c5f5320a9d0053",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-03-29T15:10:38Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/bb92c6cda0dcebe101489e0bd44e8cc2ddb3f2f6706b78ab08b496067e09a7ce/_data",
"Name": "bb92c6cda0dcebe101489e0bd44e8cc2ddb3f2f6706b78ab08b496067e09a7ce",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-05-01T13:28:52Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/cosmos-mongo-data-fxL/_data",
"Name": "cosmos-mongo-data-fxL",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-04-01T11:42:49Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/cosmos-mongo-config-NkM/_data",
"Name": "cosmos-mongo-config-NkM",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-04-01T12:26:59Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/cosmos-mongo-config-mPv/_data",
"Name": "cosmos-mongo-config-mPv",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-04-11T12:56:07Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/cosmos-mongo-data-XNq/_data",
"Name": "cosmos-mongo-data-XNq",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-03-31T14:34:56Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/469dd6e435f069173a7e88565812fbf8f354b966356361594b36fef0b0ae58fb/_data",
"Name": "469dd6e435f069173a7e88565812fbf8f354b966356361594b36fef0b0ae58fb",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-04-11T13:06:57Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/66689e6835b842df45a0062d7d69986e2876936a4e0c5b984b4b49a0a22247c6/_data",
"Name": "66689e6835b842df45a0062d7d69986e2876936a4e0c5b984b4b49a0a22247c6",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-05-07T16:46:53Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/8399775751b3ded6ba5a6532480b1ba43e85697b8261cf5d1d5f81d4a64fcbec/_data",
"Name": "8399775751b3ded6ba5a6532480b1ba43e85697b8261cf5d1d5f81d4a64fcbec",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-04-01T16:30:01Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/8715d74238cf3162649cf0bfa582efa6266e025cd45b8b197c897f445ed0c844/_data",
"Name": "8715d74238cf3162649cf0bfa582efa6266e025cd45b8b197c897f445ed0c844",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-04-11T12:36:37Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/a1fb959d8a54919926f703fcd950d4581bc51572a0ec2275ef08a11fde258956/_data",
"Name": "a1fb959d8a54919926f703fcd950d4581bc51572a0ec2275ef08a11fde258956",
"Options": null,
"Scope": "local"
},
{
"CreatedAt": "2023-05-07T16:08:35Z",
"Driver": "local",
"Labels": null,
"Mountpoint": "/var/lib/docker/volumes/MyVolume/_data",
"Name": "MyVolume",
"Options": null,
"Scope": "local"
}
],
"Warnings": null
},
"status": "OK"
}

View file

@ -1,24 +1,41 @@
let snackit;
export default function wrap(apicall) {
export default function wrap(apicall, noError = false) {
return apicall.then(async (response) => {
let rep;
try {
rep = await response.json();
} catch {
snackit('Server error');
throw new Error('Server error');
} catch (err) {
if (!noError) {
snackit('Server error');
throw new Error('Server error');
} else {
const e = new Error(rep.message);
e.status = rep.status;
e.code = rep.code;
throw e;
}
}
if (response.status == 200) {
return rep;
}
snackit(rep.message);
if (!noError) {
snackit(rep.message);
}
const e = new Error(rep.message);
e.status = response.status;
e.status = rep.status;
e.code = rep.code;
throw e;
});
}
export function setSnackit(snack) {
snackit = snack;
}
}
export {
snackit
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 354 KiB

After

Width:  |  Height:  |  Size: 348 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="2048px" height="2048px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 2048 2048"
xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<style type="text/css">
<![CDATA[
.fil2 {fill:none}
.fil3 {fill:none;fill-rule:nonzero}
.fil1 {fill:#78909C}
.fil0 {fill:#B0BEC5}
.fil4 {fill:#F57C00}
.fil6 {fill:#FFA726}
.fil5 {fill:white}
]]>
</style>
<clipPath id="id0">
<path d="M1024 0c565.541,0 1024,458.459 1024,1024 0,565.541 -458.459,1024 -1024,1024 -565.541,0 -1024,-458.459 -1024,-1024 0,-565.541 458.459,-1024 1024,-1024z"/>
</clipPath>
</defs>
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<path class="fil0" d="M1024 0c565.541,0 1024,458.459 1024,1024 0,565.541 -458.459,1024 -1024,1024 -565.541,0 -1024,-458.459 -1024,-1024 0,-565.541 458.459,-1024 1024,-1024z"/>
<g style="clip-path:url(#id0)">
<g id="_308718424">
<g>
<polygon id="_3030812801" class="fil1" points="1011.64,604.271 2147.52,1740.15 2149.55,1742.49 2151.47,1744.93 2153.27,1747.46 2154.95,1750.07 2156.5,1752.77 2157.93,1755.54 2159.22,1758.39 2160.38,1761.32 2161.4,1764.3 2162.27,1767.36 2162.99,1770.47 2163.56,1773.63 2163.97,1776.84 2164.22,1780.1 2164.31,1783.4 2164.31,1789.54 1028.42,653.653 1028.42,647.522 1028.34,644.221 1028.09,640.961 1027.68,637.746 1027.11,634.583 1026.39,631.473 1025.52,628.423 1024.5,625.434 1023.34,622.512 1022.05,619.661 1020.62,616.884 1019.07,614.188 1017.39,611.573 1015.58,609.047 1013.67,606.611 "/>
<polygon id="_303081616" class="fil1" points="1500.87,665.717 2636.75,1801.6 2638.66,1804.15 2640.32,1806.87 2641.72,1809.76 2642.83,1812.8 2643.65,1815.96 2644.16,1819.23 2644.33,1822.6 2644.33,1877.73 1508.45,741.843 1508.45,686.718 1508.27,683.35 1507.77,680.076 1506.95,676.914 1505.84,673.88 1504.44,670.991 1502.78,668.265 "/>
<polygon id="_303081808" class="fil1" points="1576.86,753.922 2712.75,1889.8 2714.68,1892.35 2716.35,1895.08 2717.72,1897.97 2718.78,1901 2719.49,1904.16 2719.85,1907.42 2719.82,1910.78 1583.94,774.897 1583.96,771.539 1583.61,768.273 1582.89,765.116 1581.84,762.086 1580.47,759.198 1578.8,756.471 "/>
<polygon id="_303081712" class="fil1" points="1583.94,774.897 2719.82,1910.78 2675.73,2567.71 1539.85,1431.82 "/>
<polygon id="_303081952" class="fil1" points="1539.85,1431.82 2675.73,2567.71 2675.36,2571.06 2674.7,2574.33 2673.78,2577.49 2672.6,2580.52 2671.18,2583.4 2669.52,2586.13 2667.64,2588.68 2665.55,2591.03 2663.26,2593.18 2660.79,2595.09 2658.14,2596.75 2655.32,2598.15 2652.35,2599.27 2649.25,2600.09 2646.01,2600.6 2642.66,2600.77 1677.12,2600.77 1673.77,2600.6 1670.54,2600.09 1667.44,2599.27 1664.49,2598.16 1661.68,2596.75 1659.04,2595.09 1656.58,2593.18 520.699,1457.3 523.163,1459.21 525.802,1460.87 528.605,1462.27 531.561,1463.39 534.659,1464.21 537.889,1464.72 541.238,1464.89 1506.78,1464.89 1510.13,1464.72 1513.37,1464.21 1516.47,1463.39 1519.44,1462.27 1522.25,1460.87 1524.9,1459.2 1527.38,1457.29 1529.67,1455.15 1531.76,1452.8 1533.64,1450.25 1535.3,1447.52 1536.72,1444.63 1537.9,1441.6 1538.82,1438.45 1539.48,1435.18 "/>
</g>
<path id="_308718448" class="fil1" d="M545.337 583.111l418.676 0c35.4272,0 64.4115,28.9843 64.4115,64.4115l0 6.13111 446.955 0c18.1878,0 33.0662,14.8784 33.0662,33.065l0 55.1245 42.424 -0.0106299c18.1866,-0.00472441 34.2803,14.9445 33.0662,33.065l-44.0882 656.926c-1.21536,18.1217 -14.9043,33.0662 -33.0662,33.0662l-965.544 0c-18.1665,0 -31.6607,-14.9339 -33.065,-33.0662l-44.0882 -568.75c-1.40551,-18.1311 14.8795,-33.065 33.065,-33.065l15.9815 0 0 -214.693c0,-17.713 14.4921,-32.2051 32.2051,-32.2051z"/>
</g>
</g>
<path class="fil2" d="M1024 0c565.541,0 1024,458.459 1024,1024 0,565.541 -458.459,1024 -1024,1024 -565.541,0 -1024,-458.459 -1024,-1024 0,-565.541 458.459,-1024 1024,-1024z"/>
<path class="fil3" d="M1488.93 870.905c0.103937,0.00708662 0.408662,0.108662 0.517323,0.168898"/>
<path class="fil4" d="M545.337 583.111l418.677 0c35.426,0 64.4103,28.9843 64.4103,64.4103l0 6.13229 446.955 0c18.1866,0 33.0662,14.8784 33.0662,33.065l0 496.002 -515.293 0 0 -70.5426 -480.022 0 0 -496.862c0,-17.713 14.4921,-32.2051 32.2051,-32.2051z"/>
<rect class="fil5" x="605.17" y="688.924" width="811.237" height="423.254"/>
<path class="fil6" d="M497.15 830.009l323.712 0 214.244 -88.0477 515.764 -0.12874c18.1866,-0.00354331 34.2803,14.9445 33.065,33.065l-44.087 656.926c-1.21536,18.1217 -14.9043,33.0662 -33.0662,33.0662l-965.544 0c-18.1665,0 -31.6607,-14.9339 -33.065,-33.0662l-44.0882 -568.75c-1.40433,-18.1323 14.8784,-33.065 33.065,-33.065z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 245 KiB

View file

@ -15,6 +15,15 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
const location = useLocation();
const [main, setMain] = useState();
const [item, setItem] = useState();
let subItem = '';
// extract /servapps/stack/:stack
const subPath = location.pathname.split('/')[3];
if(subPath && location.pathname.split('/')[4]) {
subItem = <Typography variant="subtitle1" color="textPrimary">
{location.pathname.split('/')[4]}
</Typography>;
}
// set active item state
const getCollapse = (menu) => {
@ -23,7 +32,7 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
if (collapse.type && collapse.type === 'collapse') {
getCollapse(collapse);
} else if (collapse.type && collapse.type === 'item') {
if (location.pathname === collapse.url) {
if (location.pathname.startsWith(collapse.url)) {
setMain(menu);
setItem(collapse);
}
@ -82,6 +91,8 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
</Typography>
{mainContent}
{itemContent}
{subPath && <Typography variant="subtitle1" color="textPrimary">{subPath}</Typography>}
{subItem}
</MuiBreadcrumbs>
</Grid>
{title && (

View file

@ -2,12 +2,14 @@
import { useTheme } from '@mui/material/styles';
import { fontWeight } from '@mui/system';
import logo from '../../assets/images/icons/cosmos.png';
import logo from '../../assets/images/icons/cosmos_simple_black.png';
import logoDark from '../../assets/images/icons/cosmos_simple_white.png';
// ==============================|| LOGO SVG ||============================== //
const Logo = () => {
const theme = useTheme();
const isLight = theme.palette.mode === 'light';
return (
/**
@ -17,7 +19,7 @@ const Logo = () => {
*
*/
<>
<img src={logo} alt="Cosmos" width="50" />
<img src={isLight ? logo : logoDark} alt="Cosmos" width="40" />
<span style={{fontWeight: 'bold', fontSize: '170%', paddingLeft:'10px'}}> Cosmos</span>
</>
);

View file

@ -0,0 +1,94 @@
// material-ui
import { LoadingButton } from '@mui/lab';
import { Button } from '@mui/material';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import * as React from 'react';
import { useEffect, useState } from 'react';
const preStyle = {
backgroundColor: '#000',
color: '#fff',
padding: '10px',
borderRadius: '5px',
overflow: 'auto',
maxHeight: '500px',
maxWidth: '100%',
width: '100%',
margin: '0',
position: 'relative',
fontSize: '12px',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
wordBreak: 'break-all',
lineHeight: '1.5',
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
border: '1px solid rgba(255,255,255,0.1)',
boxSizing: 'border-box',
marginBottom: '10px',
marginTop: '10px',
marginLeft: '0',
marginRight: '0',
display: 'block',
textAlign: 'left',
verticalAlign: 'baseline',
opacity: '1',
}
const ApiModal = ({ callback, label }) => {
const [openModal, setOpenModal] = useState(false);
const [content, setContent] = useState("");
const [loading, setLoading] = useState(true);
const getContent = async () => {
setLoading(true);
let content = await callback();
setContent(content.data);
setLoading(false);
};
useEffect(() => {
if (openModal)
getContent();
}, [openModal]);
return <>
<Dialog open={openModal} onClose={() => setOpenModal(false)} fullWidth maxWidth={'sm'}>
<DialogTitle>Refresh Page</DialogTitle>
<DialogContent>
<DialogContentText>
<pre style={preStyle}>
{content}
</pre>
</DialogContentText>
</DialogContent>
<DialogActions>
<LoadingButton
loading={loading}
onClick={() => {
getContent();
}}>Refresh</LoadingButton>
<Button onClick={() => {
setOpenModal(false);
}}>Close</Button>
</DialogActions>
</Dialog>
<Button
disableElevation
variant="outlined"
color="primary"
onClick={() => {
setOpenModal(true);
}}
>
{label}
</Button>
</>
};
export default ApiModal;

View file

@ -0,0 +1,16 @@
import { LeftOutlined } from "@ant-design/icons";
import { IconButton } from "@mui/material";
import { useNavigate } from "react-router";
function Back() {
const navigate = useNavigate();
const goBack = () => {
navigate(-1);
}
return <IconButton onClick={goBack}>
<LeftOutlined />
</IconButton>
;
}
export default Back;

View file

@ -0,0 +1,49 @@
// material-ui
import { LoadingButton } from '@mui/lab';
import { Button } from '@mui/material';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import * as React from 'react';
import { useEffect, useState } from 'react';
const ConfirmModal = ({ callback, label, content, startIcon }) => {
const [openModal, setOpenModal] = useState(false);
return <>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>Are you sure?</DialogTitle>
<DialogContent>
<DialogContentText>
{content}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => {
setOpenModal(false);
}}>Cancel</Button>
<LoadingButton
onClick={() => {
callback();
setOpenModal(false);
}}>Confirm</LoadingButton>
</DialogActions>
</Dialog>
<Button
disableElevation
variant="outlined"
color="warning"
startIcon={startIcon}
onClick={() => {
setOpenModal(true);
}}
>
{label}
</Button>
</>
};
export default ConfirmModal;

View file

@ -0,0 +1,9 @@
import { WarningFilled } from "@ant-design/icons";
import { Tooltip } from "@mui/material";
export const ContainerNetworkWarning = ({container}) => (
container.HostConfig.NetworkMode != "bridge" && container.HostConfig.NetworkMode != "default" &&
<Tooltip title={`This container is using an incompatible network mode (${container.HostConfig.NetworkMode.slice(0, 16)}). If you want Cosmos to proxy to this container, enabling this option will change the network mode to bridge for you. Otherwise, you dont need to do anything, as the container is already isolated. Note that changing to bridge might break connectivity to other containers. To fix it, please use a private network and static ips instead.`}>
<WarningFilled style={{color: 'red', fontSize: '18px', paddingLeft: '10px', paddingRight: '10px'}} />
</Tooltip>
);

View file

@ -0,0 +1,499 @@
import * as React from 'react';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import { Grid } from '@mui/material';
// From https://bitbucket.org/atlassian/atlaskit-mk-2/raw/4ad0e56649c3e6c973e226b7efaeb28cb240ccb0/packages/core/select/src/data/countries.js
const _countries = [
{ code: 'AD', label: 'Andorra', phone: '376' },
{
code: 'AE',
label: 'United Arab Emirates',
phone: '971',
},
{ code: 'AF', label: 'Afghanistan', phone: '93' },
{
code: 'AG',
label: 'Antigua and Barbuda',
phone: '1-268',
},
{ code: 'AI', label: 'Anguilla', phone: '1-264' },
{ code: 'AL', label: 'Albania', phone: '355' },
{ code: 'AM', label: 'Armenia', phone: '374' },
{ code: 'AO', label: 'Angola', phone: '244' },
{ code: 'AQ', label: 'Antarctica', phone: '672' },
{ code: 'AR', label: 'Argentina', phone: '54' },
{ code: 'AS', label: 'American Samoa', phone: '1-684' },
{ code: 'AT', label: 'Austria', phone: '43' },
{
code: 'AU',
label: 'Australia',
phone: '61',
suggested: true,
},
{ code: 'AW', label: 'Aruba', phone: '297' },
{ code: 'AX', label: 'Alland Islands', phone: '358' },
{ code: 'AZ', label: 'Azerbaijan', phone: '994' },
{
code: 'BA',
label: 'Bosnia and Herzegovina',
phone: '387',
},
{ code: 'BB', label: 'Barbados', phone: '1-246' },
{ code: 'BD', label: 'Bangladesh', phone: '880' },
{ code: 'BE', label: 'Belgium', phone: '32' },
{ code: 'BF', label: 'Burkina Faso', phone: '226' },
{ code: 'BG', label: 'Bulgaria', phone: '359' },
{ code: 'BH', label: 'Bahrain', phone: '973' },
{ code: 'BI', label: 'Burundi', phone: '257' },
{ code: 'BJ', label: 'Benin', phone: '229' },
{ code: 'BL', label: 'Saint Barthelemy', phone: '590' },
{ code: 'BM', label: 'Bermuda', phone: '1-441' },
{ code: 'BN', label: 'Brunei Darussalam', phone: '673' },
{ code: 'BO', label: 'Bolivia', phone: '591' },
{ code: 'BR', label: 'Brazil', phone: '55' },
{ code: 'BS', label: 'Bahamas', phone: '1-242' },
{ code: 'BT', label: 'Bhutan', phone: '975' },
{ code: 'BV', label: 'Bouvet Island', phone: '47' },
{ code: 'BW', label: 'Botswana', phone: '267' },
{ code: 'BY', label: 'Belarus', phone: '375' },
{ code: 'BZ', label: 'Belize', phone: '501' },
{
code: 'CA',
label: 'Canada',
phone: '1',
suggested: true,
},
{
code: 'CC',
label: 'Cocos (Keeling) Islands',
phone: '61',
},
{
code: 'CD',
label: 'Congo, Democratic Republic of the',
phone: '243',
},
{
code: 'CF',
label: 'Central African Republic',
phone: '236',
},
{
code: 'CG',
label: 'Congo, Republic of the',
phone: '242',
},
{ code: 'CH', label: 'Switzerland', phone: '41' },
{ code: 'CI', label: "Cote d'Ivoire", phone: '225' },
{ code: 'CK', label: 'Cook Islands', phone: '682' },
{ code: 'CL', label: 'Chile', phone: '56' },
{ code: 'CM', label: 'Cameroon', phone: '237' },
{ code: 'CN', label: 'China', phone: '86' },
{ code: 'CO', label: 'Colombia', phone: '57' },
{ code: 'CR', label: 'Costa Rica', phone: '506' },
{ code: 'CU', label: 'Cuba', phone: '53' },
{ code: 'CV', label: 'Cape Verde', phone: '238' },
{ code: 'CW', label: 'Curacao', phone: '599' },
{ code: 'CX', label: 'Christmas Island', phone: '61' },
{ code: 'CY', label: 'Cyprus', phone: '357' },
{ code: 'CZ', label: 'Czech Republic', phone: '420' },
{
code: 'DE',
label: 'Germany',
phone: '49',
suggested: true,
},
{ code: 'DJ', label: 'Djibouti', phone: '253' },
{ code: 'DK', label: 'Denmark', phone: '45' },
{ code: 'DM', label: 'Dominica', phone: '1-767' },
{
code: 'DO',
label: 'Dominican Republic',
phone: '1-809',
},
{ code: 'DZ', label: 'Algeria', phone: '213' },
{ code: 'EC', label: 'Ecuador', phone: '593' },
{ code: 'EE', label: 'Estonia', phone: '372' },
{ code: 'EG', label: 'Egypt', phone: '20' },
{ code: 'EH', label: 'Western Sahara', phone: '212' },
{ code: 'ER', label: 'Eritrea', phone: '291' },
{ code: 'ES', label: 'Spain', phone: '34' },
{ code: 'ET', label: 'Ethiopia', phone: '251' },
{ code: 'FI', label: 'Finland', phone: '358' },
{ code: 'FJ', label: 'Fiji', phone: '679' },
{
code: 'FK',
label: 'Falkland Islands (Malvinas)',
phone: '500',
},
{
code: 'FM',
label: 'Micronesia, Federated States of',
phone: '691',
},
{ code: 'FO', label: 'Faroe Islands', phone: '298' },
{
code: 'FR',
label: 'France',
phone: '33',
suggested: true,
},
{ code: 'GA', label: 'Gabon', phone: '241' },
{ code: 'GB', label: 'United Kingdom', phone: '44' },
{ code: 'GD', label: 'Grenada', phone: '1-473' },
{ code: 'GE', label: 'Georgia', phone: '995' },
{ code: 'GF', label: 'French Guiana', phone: '594' },
{ code: 'GG', label: 'Guernsey', phone: '44' },
{ code: 'GH', label: 'Ghana', phone: '233' },
{ code: 'GI', label: 'Gibraltar', phone: '350' },
{ code: 'GL', label: 'Greenland', phone: '299' },
{ code: 'GM', label: 'Gambia', phone: '220' },
{ code: 'GN', label: 'Guinea', phone: '224' },
{ code: 'GP', label: 'Guadeloupe', phone: '590' },
{ code: 'GQ', label: 'Equatorial Guinea', phone: '240' },
{ code: 'GR', label: 'Greece', phone: '30' },
{
code: 'GS',
label: 'South Georgia and the South Sandwich Islands',
phone: '500',
},
{ code: 'GT', label: 'Guatemala', phone: '502' },
{ code: 'GU', label: 'Guam', phone: '1-671' },
{ code: 'GW', label: 'Guinea-Bissau', phone: '245' },
{ code: 'GY', label: 'Guyana', phone: '592' },
{ code: 'HK', label: 'Hong Kong', phone: '852' },
{
code: 'HM',
label: 'Heard Island and McDonald Islands',
phone: '672',
},
{ code: 'HN', label: 'Honduras', phone: '504' },
{ code: 'HR', label: 'Croatia', phone: '385' },
{ code: 'HT', label: 'Haiti', phone: '509' },
{ code: 'HU', label: 'Hungary', phone: '36' },
{ code: 'ID', label: 'Indonesia', phone: '62' },
{ code: 'IE', label: 'Ireland', phone: '353' },
{ code: 'IL', label: 'Israel', phone: '972' },
{ code: 'IM', label: 'Isle of Man', phone: '44' },
{ code: 'IN', label: 'India', phone: '91' },
{
code: 'IO',
label: 'British Indian Ocean Territory',
phone: '246',
},
{ code: 'IQ', label: 'Iraq', phone: '964' },
{
code: 'IR',
label: 'Iran, Islamic Republic of',
phone: '98',
},
{ code: 'IS', label: 'Iceland', phone: '354' },
{ code: 'IT', label: 'Italy', phone: '39' },
{ code: 'JE', label: 'Jersey', phone: '44' },
{ code: 'JM', label: 'Jamaica', phone: '1-876' },
{ code: 'JO', label: 'Jordan', phone: '962' },
{
code: 'JP',
label: 'Japan',
phone: '81',
suggested: true,
},
{ code: 'KE', label: 'Kenya', phone: '254' },
{ code: 'KG', label: 'Kyrgyzstan', phone: '996' },
{ code: 'KH', label: 'Cambodia', phone: '855' },
{ code: 'KI', label: 'Kiribati', phone: '686' },
{ code: 'KM', label: 'Comoros', phone: '269' },
{
code: 'KN',
label: 'Saint Kitts and Nevis',
phone: '1-869',
},
{
code: 'KP',
label: "Korea, Democratic People's Republic of",
phone: '850',
},
{ code: 'KR', label: 'Korea, Republic of', phone: '82' },
{ code: 'KW', label: 'Kuwait', phone: '965' },
{ code: 'KY', label: 'Cayman Islands', phone: '1-345' },
{ code: 'KZ', label: 'Kazakhstan', phone: '7' },
{
code: 'LA',
label: "Lao People's Democratic Republic",
phone: '856',
},
{ code: 'LB', label: 'Lebanon', phone: '961' },
{ code: 'LC', label: 'Saint Lucia', phone: '1-758' },
{ code: 'LI', label: 'Liechtenstein', phone: '423' },
{ code: 'LK', label: 'Sri Lanka', phone: '94' },
{ code: 'LR', label: 'Liberia', phone: '231' },
{ code: 'LS', label: 'Lesotho', phone: '266' },
{ code: 'LT', label: 'Lithuania', phone: '370' },
{ code: 'LU', label: 'Luxembourg', phone: '352' },
{ code: 'LV', label: 'Latvia', phone: '371' },
{ code: 'LY', label: 'Libya', phone: '218' },
{ code: 'MA', label: 'Morocco', phone: '212' },
{ code: 'MC', label: 'Monaco', phone: '377' },
{
code: 'MD',
label: 'Moldova, Republic of',
phone: '373',
},
{ code: 'ME', label: 'Montenegro', phone: '382' },
{
code: 'MF',
label: 'Saint Martin (French part)',
phone: '590',
},
{ code: 'MG', label: 'Madagascar', phone: '261' },
{ code: 'MH', label: 'Marshall Islands', phone: '692' },
{
code: 'MK',
label: 'Macedonia, the Former Yugoslav Republic of',
phone: '389',
},
{ code: 'ML', label: 'Mali', phone: '223' },
{ code: 'MM', label: 'Myanmar', phone: '95' },
{ code: 'MN', label: 'Mongolia', phone: '976' },
{ code: 'MO', label: 'Macao', phone: '853' },
{
code: 'MP',
label: 'Northern Mariana Islands',
phone: '1-670',
},
{ code: 'MQ', label: 'Martinique', phone: '596' },
{ code: 'MR', label: 'Mauritania', phone: '222' },
{ code: 'MS', label: 'Montserrat', phone: '1-664' },
{ code: 'MT', label: 'Malta', phone: '356' },
{ code: 'MU', label: 'Mauritius', phone: '230' },
{ code: 'MV', label: 'Maldives', phone: '960' },
{ code: 'MW', label: 'Malawi', phone: '265' },
{ code: 'MX', label: 'Mexico', phone: '52' },
{ code: 'MY', label: 'Malaysia', phone: '60' },
{ code: 'MZ', label: 'Mozambique', phone: '258' },
{ code: 'NA', label: 'Namibia', phone: '264' },
{ code: 'NC', label: 'New Caledonia', phone: '687' },
{ code: 'NE', label: 'Niger', phone: '227' },
{ code: 'NF', label: 'Norfolk Island', phone: '672' },
{ code: 'NG', label: 'Nigeria', phone: '234' },
{ code: 'NI', label: 'Nicaragua', phone: '505' },
{ code: 'NL', label: 'Netherlands', phone: '31' },
{ code: 'NO', label: 'Norway', phone: '47' },
{ code: 'NP', label: 'Nepal', phone: '977' },
{ code: 'NR', label: 'Nauru', phone: '674' },
{ code: 'NU', label: 'Niue', phone: '683' },
{ code: 'NZ', label: 'New Zealand', phone: '64' },
{ code: 'OM', label: 'Oman', phone: '968' },
{ code: 'PA', label: 'Panama', phone: '507' },
{ code: 'PE', label: 'Peru', phone: '51' },
{ code: 'PF', label: 'French Polynesia', phone: '689' },
{ code: 'PG', label: 'Papua New Guinea', phone: '675' },
{ code: 'PH', label: 'Philippines', phone: '63' },
{ code: 'PK', label: 'Pakistan', phone: '92' },
{ code: 'PL', label: 'Poland', phone: '48' },
{
code: 'PM',
label: 'Saint Pierre and Miquelon',
phone: '508',
},
{ code: 'PN', label: 'Pitcairn', phone: '870' },
{ code: 'PR', label: 'Puerto Rico', phone: '1' },
{
code: 'PS',
label: 'Palestine, State of',
phone: '970',
},
{ code: 'PT', label: 'Portugal', phone: '351' },
{ code: 'PW', label: 'Palau', phone: '680' },
{ code: 'PY', label: 'Paraguay', phone: '595' },
{ code: 'QA', label: 'Qatar', phone: '974' },
{ code: 'RE', label: 'Reunion', phone: '262' },
{ code: 'RO', label: 'Romania', phone: '40' },
{ code: 'RS', label: 'Serbia', phone: '381' },
{ code: 'RU', label: 'Russian Federation', phone: '7' },
{ code: 'RW', label: 'Rwanda', phone: '250' },
{ code: 'SA', label: 'Saudi Arabia', phone: '966' },
{ code: 'SB', label: 'Solomon Islands', phone: '677' },
{ code: 'SC', label: 'Seychelles', phone: '248' },
{ code: 'SD', label: 'Sudan', phone: '249' },
{ code: 'SE', label: 'Sweden', phone: '46' },
{ code: 'SG', label: 'Singapore', phone: '65' },
{ code: 'SH', label: 'Saint Helena', phone: '290' },
{ code: 'SI', label: 'Slovenia', phone: '386' },
{
code: 'SJ',
label: 'Svalbard and Jan Mayen',
phone: '47',
},
{ code: 'SK', label: 'Slovakia', phone: '421' },
{ code: 'SL', label: 'Sierra Leone', phone: '232' },
{ code: 'SM', label: 'San Marino', phone: '378' },
{ code: 'SN', label: 'Senegal', phone: '221' },
{ code: 'SO', label: 'Somalia', phone: '252' },
{ code: 'SR', label: 'Suriname', phone: '597' },
{ code: 'SS', label: 'South Sudan', phone: '211' },
{
code: 'ST',
label: 'Sao Tome and Principe',
phone: '239',
},
{ code: 'SV', label: 'El Salvador', phone: '503' },
{
code: 'SX',
label: 'Sint Maarten (Dutch part)',
phone: '1-721',
},
{
code: 'SY',
label: 'Syrian Arab Republic',
phone: '963',
},
{ code: 'SZ', label: 'Swaziland', phone: '268' },
{
code: 'TC',
label: 'Turks and Caicos Islands',
phone: '1-649',
},
{ code: 'TD', label: 'Chad', phone: '235' },
{
code: 'TF',
label: 'French Southern Territories',
phone: '262',
},
{ code: 'TG', label: 'Togo', phone: '228' },
{ code: 'TH', label: 'Thailand', phone: '66' },
{ code: 'TJ', label: 'Tajikistan', phone: '992' },
{ code: 'TK', label: 'Tokelau', phone: '690' },
{ code: 'TL', label: 'Timor-Leste', phone: '670' },
{ code: 'TM', label: 'Turkmenistan', phone: '993' },
{ code: 'TN', label: 'Tunisia', phone: '216' },
{ code: 'TO', label: 'Tonga', phone: '676' },
{ code: 'TR', label: 'Turkey', phone: '90' },
{
code: 'TT',
label: 'Trinidad and Tobago',
phone: '1-868',
},
{ code: 'TV', label: 'Tuvalu', phone: '688' },
{
code: 'TW',
label: 'Taiwan, Republic of China',
phone: '886',
},
{
code: 'TZ',
label: 'United Republic of Tanzania',
phone: '255',
},
{ code: 'UA', label: 'Ukraine', phone: '380' },
{ code: 'UG', label: 'Uganda', phone: '256' },
{
code: 'US',
label: 'United States',
phone: '1',
suggested: true,
},
{ code: 'UY', label: 'Uruguay', phone: '598' },
{ code: 'UZ', label: 'Uzbekistan', phone: '998' },
{
code: 'VA',
label: 'Holy See (Vatican City State)',
phone: '379',
},
{
code: 'VC',
label: 'Saint Vincent and the Grenadines',
phone: '1-784',
},
{ code: 'VE', label: 'Venezuela', phone: '58' },
{
code: 'VG',
label: 'British Virgin Islands',
phone: '1-284',
},
{
code: 'VI',
label: 'US Virgin Islands',
phone: '1-340',
},
{ code: 'VN', label: 'Vietnam', phone: '84' },
{ code: 'VU', label: 'Vanuatu', phone: '678' },
{ code: 'WF', label: 'Wallis and Futuna', phone: '681' },
{ code: 'WS', label: 'Samoa', phone: '685' },
{ code: 'XK', label: 'Kosovo', phone: '383' },
{ code: 'YE', label: 'Yemen', phone: '967' },
{ code: 'YT', label: 'Mayotte', phone: '262' },
{ code: 'ZA', label: 'South Africa', phone: '27' },
{ code: 'ZM', label: 'Zambia', phone: '260' },
{ code: 'ZW', label: 'Zimbabwe', phone: '263' },
];
const countries = {};
const countriesOptions = [];
_countries.forEach((country) => {
countries[country.code] = country;
countriesOptions.push(country.code);
});
export default function CountrySelect({name, label, formik}) {
return (
<Grid item xs={12}>
<Autocomplete
id={name}
name={name}
multiple
options={countriesOptions}
autoHighlight
value={formik.values[name] || []}
onBlur={formik.handleBlur}
onChange={(event, value) => {
formik.setFieldValue(name, value)
}}
filterOptions={(options, state) => {
const inputValue = state.inputValue.toUpperCase();
return options.filter((option) => {
return countries[option].label.toUpperCase().includes(inputValue)
})
}}
error={Boolean(formik.touched[name] && formik.errors[name])}
getOptionLabel={(option) => <div style={{verticalAlign: 'middle'}}><img
loading="lazy"
width="15"
style={{verticalAlign: 'middle'}}
height="10"
src={`https://flagcdn.com/w20/${option.toLowerCase()}.png`}
srcSet={`https://flagcdn.com/w40/${option.toLowerCase()}.png 2x`}
alt=""
/> &nbsp;{countries[option].label}</div>}
renderOption={(props, option) => (
<Box component="li" sx={{ '& > img': { mr: 2, flexShrink: 0 } }} {...props}>
<img
loading="lazy"
width="20"
src={`https://flagcdn.com/w20/${option.toLowerCase()}.png`}
srcSet={`https://flagcdn.com/w40/${option.toLowerCase()}.png 2x`}
alt=""
/>
{countries[option].label} ({option.code}) +{countries[option].phone}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
label={label}
inputProps={{
...params.inputProps,
autoComplete: 'new-password', // disable autocomplete and autofill
}}
/>
)}
/>
</Grid>
);
}
export {
countries
};

View file

@ -0,0 +1,15 @@
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, SafetyOutlined, UpOutlined } from "@ant-design/icons";
import { Card, Chip, Stack, Tooltip } from "@mui/material";
import { useState } from "react";
import { useTheme } from '@mui/material/styles';
export const DeleteButton = ({onDelete, disabled, size}) => {
const [confirmDelete, setConfirmDelete] = useState(false);
return (<>
{!confirmDelete && (<Chip label={<DeleteOutlined size={size}/>}
onClick={() => !disabled && setConfirmDelete(true)}/>)}
{confirmDelete && (<Chip label={<CheckOutlined size={size}/>} color="error"
onClick={(event) => !disabled && onDelete(event)}/>)}
</>);
}

View file

@ -0,0 +1,24 @@
import React from 'react';
import { Button } from '@mui/material';
import { UploadOutlined } from '@ant-design/icons';
export default function UploadButtons({OnChange, accept, label, variant, fullWidth, size}) {
return (
<div>
<input
accept={accept}
style={{ display: 'none' }}
id="contained-button-file"
multiple
type="file"
onChange={OnChange}
/>
<label htmlFor="contained-button-file">
<Button variant={variant || "contained"} component="span"
fullWidth={fullWidth} startIcon={<UploadOutlined />}>
{label || 'Upload'}
</Button>
</label>
</div>
);
}

View file

@ -0,0 +1,45 @@
import { SettingOutlined } from "@ant-design/icons";
import { Chip } from "@mui/material";
import { useEffect, useState } from "react";
import { getOrigin, getFullOrigin } from "../utils/routes";
import { useTheme } from '@mui/material/styles';
const HostChip = ({route, settings, style}) => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const [isOnline, setIsOnline] = useState(null);
const url = getOrigin(route)
useEffect(() => {
fetch(getFullOrigin(route), {
method: 'HEAD',
mode: 'no-cors',
}).then((res) => {
setIsOnline(true);
}).catch((err) => {
setIsOnline(false);
});
}, [url]);
return <Chip
label={((isOnline == null) ? "⚪" : (isOnline ? "🟢 " : "🔴 ")) + url}
color="secondary"
style={{
paddingRight: '4px',
textDecoration: isOnline ? 'none' : 'underline wavy red',
...style
}}
onClick={() => {
if(route.UseHost)
window.open(window.location.origin.split("://")[0] + "://" + route.Host + route.PathPrefix, '_blank');
else
window.open(window.location.origin + route.PathPrefix, '_blank');
}}
onDelete={settings ? () => {
window.open('/cosmos-ui/config-url/'+route.Name, '_blank');
} : null}
deleteIcon={settings ? <SettingOutlined /> : null}
/>
}
export default HostChip;

View file

@ -0,0 +1,48 @@
import React, { useState } from 'react';
import LazyLoad from 'react-lazyload';
import cosmosGray from '../assets/images/icons/cosmos_gray.png';
function ImageWithPlaceholder({ src, alt, placeholder, ...props }) {
const [imageSrc, setImageSrc] = useState(placeholder || cosmosGray);
const [imageRef, setImageRef] = useState();
const onLoad = event => {
event.target.classList.add('loaded');
};
const onError = () => {
setImageSrc(cosmosGray);
};
// This function will be called when the actual image is loaded
const handleImageLoad = () => {
if (imageRef) {
imageRef.src = src;
}
};
return (
<>
<img
ref={setImageRef}
{...props}
src={imageSrc}
alt={alt}
onLoad={onLoad}
onError={onError}
// style={{ opacity: imageSrc === src ? 1 : 0, transition: 'opacity 0.5s ease-in-out' }}
/>
{/* This image will load the actual image and then handleImageLoad will be triggered */}
<img
{...props}
src={src}
alt={alt}
style={{ display: 'none' }} // Hide this image
onLoad={handleImageLoad}
onError={onError}
/>
</>
);
}
export default ImageWithPlaceholder;

View file

@ -0,0 +1,106 @@
import { Stack } from '@mui/material';
import React from 'react';
function decodeUnicode(str) {
return str.replace(/\\u([0-9a-zA-Z]{3-5})/g, (match, p1) => {
return String.fromCharCode(parseInt(p1, 16));
});
}
const LogLine = ({ message, docker, isMobile }) => {
let html = decodeUnicode(message)
.replace('\u0001\u0000\u0000\u0000\u0000\u0000\u0000', '')
.replace(/(?:\r\n|\r|\n)/g, '<br>')
.replace(/ /g, '&nbsp;')
.replace(/<2F>/g, '')
.replace(/\x1b\[([0-9]{1,2}(?:;[0-9]{1,2})*)?m/g, (match, p1) => {
if (!p1) {
return '</span>';
}
const codes = p1.split(';');
const styles = [];
for (const code of codes) {
switch (code) {
case '1':
styles.push('font-weight:bold');
break;
case '3':
styles.push('font-style:italic');
break;
case '4':
styles.push('text-decoration:underline');
break;
case '30':
case '31':
case '32':
case '33':
case '34':
case '35':
case '36':
case '37':
case '90':
case '91':
case '92':
case '93':
case '94':
case '95':
case '96':
case '97':
styles.push(`color:${getColor(code)}`);
break;
}
}
return `<span style="${styles.join(';')}">`;
});
if(docker) {
let parts = html.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/)
if(!parts) {
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
let restString = html.replace(parts[0], '')
return <Stack direction={isMobile ? 'column' : 'row'} spacing={1}>
<div style={{color:'grey', fontStyle:'italic', whiteSpace: 'pre'}}>
{parts[0].replace('T', ' ').split('.')[0]}
</div>
<div dangerouslySetInnerHTML={{ __html: restString }} />
</Stack>;
}
return <div dangerouslySetInnerHTML={{ __html: html }} />;
};
const getColor = (code) => {
switch (code) {
case '30':
case '90':
return 'black';
case '31':
case '91':
return 'red';
case '32':
case '92':
return 'green';
case '33':
case '93':
return 'yellow';
case '34':
case '94':
return 'blue';
case '35':
case '95':
return 'magenta';
case '36':
case '96':
return 'cyan';
case '37':
case '97':
return 'white';
default:
return 'inherit';
}
};
export default LogLine;

View file

@ -0,0 +1,88 @@
// material-ui
import * as React from 'react';
import { Alert, Button, Stack, Typography } from '@mui/material';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined, ArrowUpOutlined, FileZipOutlined } from '@ant-design/icons';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import { useEffect, useState } from 'react';
import { smartDockerLogConcat, tryParseProgressLog } from '../utils/docker';
const preStyle = {
backgroundColor: '#000',
color: '#fff',
padding: '10px',
borderRadius: '5px',
overflow: 'auto',
maxHeight: '500px',
maxWidth: '100%',
width: '100%',
margin: '0',
position: 'relative',
fontSize: '12px',
fontFamily: 'monospace',
whiteSpace: 'pre-wrap',
wordWrap: 'break-word',
wordBreak: 'break-all',
lineHeight: '1.5',
boxShadow: '0 0 10px rgba(0,0,0,0.5)',
border: '1px solid rgba(255,255,255,0.1)',
boxSizing: 'border-box',
marginBottom: '10px',
marginTop: '10px',
marginLeft: '0',
marginRight: '0',
}
const LogsInModal = ({title, request, OnSuccess, OnError, closeAnytime, initialLogs = [], }) => {
const [openModal, setOpenModal] = useState(false);
const [logs, setLogs] = useState(initialLogs);
const [done, setDone] = useState(closeAnytime);
const preRef = React.useRef(null);
useEffect(() => {
setLogs(initialLogs);
setDone(closeAnytime);
if(request === null) return;
request((newlog) => {
setLogs((old) => smartDockerLogConcat(old, newlog));
if(preRef.current)
preRef.current.scrollTop = preRef.current.scrollHeight;
if (newlog.includes('[OPERATION SUCCEEDED]')) {
setDone(true);
setOpenModal(false);
OnSuccess && OnSuccess();
} else if (newlog.includes('[OPERATION FAILED]')) {
setDone(true);
OnError && OnError(newlog);
} else {
setOpenModal(true);
}
});
}, [request]);
return <>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>{title}</DialogTitle>
<DialogContent>
<DialogContentText>
<pre style={preStyle} ref={preRef}>
{logs.map((l) => {
return <div>{tryParseProgressLog(l)}</div>
})}
</pre>
</DialogContentText>
</DialogContent>
<DialogActions>
</DialogActions>
</Dialog>
</>;
};
export default LogsInModal;

View file

@ -0,0 +1,29 @@
import { Button, useMediaQuery, IconButton } from "@mui/material";
const ResponsiveButton = ({ children, startIcon, endIcon, size, style, ...props }) => {
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
let newStyle = style || {};
if (isMobile) {
newStyle.minHeight = '40px';
newStyle.fontSize = '145%'
}
return (
<Button
className="responsive-button"
size={isMobile ? 'large' : size}
startIcon={isMobile ? null : startIcon}
endIcon={isMobile ? null : endIcon}
{...props} style={newStyle}>
{(isMobile) ? startIcon : (
startIcon ? children : null
)}
{(isMobile) ? endIcon : (
endIcon ? children : null
)}
</Button>
);
}
export default ResponsiveButton;

View file

@ -0,0 +1,141 @@
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, SafetyOutlined, UpOutlined } from "@ant-design/icons";
import { Card, Chip, Stack, Tooltip } from "@mui/material";
import { useState } from "react";
import { useTheme } from '@mui/material/styles';
let routeImages = {
"SERVAPP": {
label: "ServApp",
icon: "🐳",
backgroundColor: "#0db7ed",
color: "white",
colorDark: "black",
},
"STATIC": {
label: "Static",
icon: "📁",
backgroundColor: "#f9d71c",
color: "black",
colorDark: "black",
},
"REDIRECT": {
label: "Redir",
icon: "🔀",
backgroundColor: "#2c3e50",
color: "white",
colorDark: "white",
},
"PROXY": {
label: "Proxy",
icon: "🔗",
backgroundColor: "#2ecc71",
color: "white",
colorDark: "black",
},
"SPA": {
label: "SPA",
icon: "🌐",
backgroundColor: "#e74c3c",
color: "white",
colorDark: "black",
},
}
export const RouteMode = ({route}) => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
let c = routeImages[route.Mode.toUpperCase()];
return c ? <>
<Chip
icon={<span>{c.icon}</span>}
label={c.label}
sx={{
backgroundColor: c.backgroundColor,
color: isDark ? c.colorDark : c.color,
paddingLeft: "5px",
alignItems: "right",
}}
></Chip>
</> : <></>;
}
export const RouteSecurity = ({route}) => {
return <div style={{fontWeight: 'bold', fontSize: '110%'}}>
<Tooltip title={route.SmartShield && route.SmartShield.Enabled ? "Smart Shield is enabled" : "Smart Shield is disabled"}>
<div style={{display: 'inline-block'}}>
{route.SmartShield && route.SmartShield.Enabled ?
<SafetyOutlined style={{color: 'green'}} /> :
<SafetyOutlined style={{color: 'red'}} />
}
</div>
</Tooltip>
&nbsp;
<Tooltip title={route.AuthEnabled ? "Authentication is enabled" : "Authentication is disabled"}>
<div style={{display: 'inline-block'}}>
{route.AuthEnabled ?
<LockOutlined style={{color: 'green'}} /> :
<LockOutlined style={{color: 'red'}} />
}
</div>
</Tooltip>
&nbsp;
<Tooltip title={route.ThrottlePerMinute ? "Throttling is enabled" : "Throttling is disabled"}>
<div style={{display: 'inline-block'}}>
{route.ThrottlePerMinute ?
<DashboardOutlined style={{color: 'green'}} /> :
<DashboardOutlined style={{color: 'red'}} />
}
</div>
</Tooltip>
&nbsp;
<Tooltip title={route.Timeout ? "Timeout is enabled" : "Timeout is disabled"}>
<div style={{display: 'inline-block'}}>
{route.Timeout ?
<ClockCircleOutlined style={{color: 'green'}} /> :
<ClockCircleOutlined style={{color: 'red'}} />
}
</div>
</Tooltip>
</div>
}
export const RouteActions = ({route, routeKey, up, down, deleteRoute}) => {
const [confirmDelete, setConfirmDelete] = useState(false);
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const miniChip = {
width: '30px',
height: '20px',
display: 'inline-block',
textAlign: 'center',
cursor: 'pointer',
color: theme.palette.text.secondary,
fontSize: '12px',
lineHeight: '20px',
padding: '0px',
borderRadius: '0px',
background: isDark ? 'rgba(255, 255, 255, 0.03)' : '',
fontWeight: 'bold',
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
}
}
return <>
<Stack direction={'row'} spacing={2} alignItems={'center'} justifyContent={'right'}>
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
{confirmDelete && (<Chip label={<CheckOutlined />} color="error" onClick={(event) => deleteRoute(event)}/>)}
<Tooltip title='Routes with the lowest priority are matched first'>
<Stack direction={'column'} spacing={0}>
<Card sx={{...miniChip, borderBottom: 'none'}} onClick={(event) => up(event)}><UpOutlined /></Card>
<Card sx={{...miniChip, cursor: 'auto'}}>{routeKey}</Card>
<Card sx={{...miniChip, borderTop: 'none'}} onClick={(event) => down(event)}><DownOutlined /></Card>
</Stack>
</Tooltip>
</Stack>
</>;
}

View file

@ -0,0 +1,106 @@
import React, { useState } from 'react';
import { Box, Tab, Tabs, Typography, MenuItem, Select, useMediaQuery, CircularProgress } from '@mui/material';
import { styled } from '@mui/system';
const StyledTabs = styled(Tabs)`
border-right: 1px solid ${({ theme }) => theme.palette.divider};
`;
const TabPanel = (props) => {
const { children, value, index, ...other } = props;
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md'));
return (
<div
role="tabpanel"
style={{
width: '100%',
}}
hidden={value !== index}
id={`vertical-tabpanel-${index}`}
aria-labelledby={`vertical-tab-${index}`}
{...other}
>
{value === index && (
<Box p={isMobile ? 1 : 3}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
};
const a11yProps = (index) => {
return {
id: `vertical-tab-${index}`,
'aria-controls': `vertical-tabpanel-${index}`,
};
};
const PrettyTabbedView = ({ tabs, isLoading, currentTab, setCurrentTab }) => {
const [value, setValue] = useState(0);
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('md'));
if((currentTab != null && typeof currentTab === 'number') && value !== currentTab)
setValue(currentTab);
const handleChange = (event, newValue) => {
setValue(newValue);
setCurrentTab && setCurrentTab(newValue);
};
const handleSelectChange = (event) => {
setValue(event.target.value);
setCurrentTab && setCurrentTab(event.target.value);
};
return (
<Box display="flex" height="100%" flexDirection={isMobile ? 'column' : 'row'}>
{(isMobile) ? (
<Select value={value} onChange={handleSelectChange} sx={{ minWidth: 120, marginBottom: '15px' }}>
{tabs.map((tab, index) => (
<MenuItem key={index} value={index}>
{tab.title}
</MenuItem>
))}
</Select>
) : (
<StyledTabs
orientation="vertical"
variant="scrollable"
value={value}
onChange={handleChange}
aria-label="Vertical tabs"
>
{tabs.map((tab, index) => (
<Tab
style={{fontWeight: !tab.children ? '1000' : '', }}
disabled={tab.disabled || !tab.children} key={index}
label={tab.title} {...a11yProps(index)}
/>
))}
</StyledTabs>
)}
{!isLoading && tabs.map((tab, index) => (
<TabPanel key={index} value={value} index={index}>
{tab.children}
</TabPanel>
))}
{isLoading && (
<Box
display="flex"
alignItems="center"
justifyContent="center"
width="100%"
height="100%"
color="text.primary"
p={2}
>
<CircularProgress />
</Box>
)}
</Box>
);
};
export default PrettyTabbedView;

View file

@ -0,0 +1,120 @@
import * as React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import { CircularProgress, Input, InputAdornment, Stack, TextField, useMediaQuery } from '@mui/material';
import { SearchOutlined } from '@ant-design/icons';
import { useTheme } from '@mui/material/styles';
import { Link } from 'react-router-dom';
const PrettyTableView = ({ isLoading, getKey, data, columns, sort, onRowClick, linkTo, buttons, fullWidth }) => {
const [search, setSearch] = React.useState('');
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const screenMin = {
xs: useMediaQuery((theme) => theme.breakpoints.up('xs')),
sm: useMediaQuery((theme) => theme.breakpoints.up('sm')),
md: useMediaQuery((theme) => theme.breakpoints.up('md')),
lg: useMediaQuery((theme) => theme.breakpoints.up('lg')),
xl: useMediaQuery((theme) => theme.breakpoints.up('xl')),
xxl: useMediaQuery((theme) => theme.breakpoints.up('xxl')),
}
return (
<Stack direction="column" spacing={2} style={{width: fullWidth ? '100%': ''}}>
<Stack direction="row" spacing={2}>
<Input placeholder="Search"
value={search}
style={{
width: '250px',
}}
startAdornment={
<InputAdornment position="start">
<SearchOutlined />
</InputAdornment>
}
onChange={(e) => {
setSearch(e.target.value);
}}
/>
{buttons}
</Stack>
{isLoading && (<div style={{height: '550px'}}>
<center>
<br />
<CircularProgress />
</center>
</div>
)}
{!isLoading && <TableContainer style={{width: fullWidth ? '100%': '', background: isDark ? '#252b32' : '', borderTop: '3px solid ' + theme.palette.primary.main}} component={Paper}>
<Table aria-label="simple table">
<TableHead>
<TableRow>
{columns.map((column) => (
(!column.screenMin || screenMin[column.screenMin]) && <TableCell>{column.title}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{data
.filter((row) => {
if (!search || search.length <= 2) return true;
let found = false;
columns.forEach((column) => {
if (column.search && column.search(row).toLowerCase().includes(search.toLowerCase())) {
found = true;
}
})
return found;
})
.sort((a, b) => {
if (!sort) return 0;
return sort(a, b);
})
.map((row, key) => (
<TableRow
key={getKey(row)}
sx={{
cursor: 'pointer',
borderLeft: 'transparent solid 2px',
'&:last-child td, &:last-child th': { border: 0 },
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.06)',
borderColor: 'gray',
'&:hover .emphasis': {
textDecoration: 'underline'
}
},
}}
>
{columns.map((column) => (
(!column.screenMin || screenMin[column.screenMin]) && <TableCell
component={(linkTo && !column.clickable) ? Link : 'td'}
onClick={() => !column.clickable && onRowClick && onRowClick(row, key)}
to={linkTo && linkTo(row, key)}
className={column.underline ? 'emphasis' : ''}
sx={{
textDecoration: 'none',
...column.style,
}}>
{column.field(row, key)}
</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>}
</Stack>
)
}
export default PrettyTableView;

View file

@ -0,0 +1,62 @@
import React, { useState, useEffect, useRef } from 'react';
import { Box, Button, Checkbox, CircularProgress, Input, Stack, TextField, Typography, useMediaQuery } from '@mui/material';
import * as API from '../api';
import LogLine from '../components/logLine';
import { useTheme } from '@emotion/react';
const Terminal = ({ logs, setLogs, fetchLogs, docker }) => {
const [hasMore, setHasMore] = useState(true);
const [hasScrolled, setHasScrolled] = useState(false);
const [fetching, setFetching] = useState(false);
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const screenMin = useMediaQuery((theme) => theme.breakpoints.up('sm'))
const bottomRef = useRef(null);
const terminalRef = useRef(null);
const scrollToBottom = () => {
bottomRef.current.scrollIntoView({});
};
useEffect(() => {
if (!hasScrolled) {
scrollToBottom();
}
}, [logs]);
const handleScroll = (event) => {
if (event.target.scrollHeight - event.target.scrollTop === event.target.clientHeight) {
setHasScrolled(false);
}else {
setHasScrolled(true);
}
};
return (
<Box
ref={terminalRef}
sx={{
minHeight: '50px',
maxHeight: 'calc(1vh * 80 - 200px)',
overflow: 'auto',
padding: '10px',
wordBreak: 'break-all',
background: '#272d36',
color: '#fff',
borderTop: '3px solid ' + theme.palette.primary.main
}}
onScroll={handleScroll}
>
{logs && logs.map((log, index) => (
<div key={index} style={{paddingTop: (!screenMin) ? '10px' : '2px'}}>
<LogLine message={log.output} docker isMobile={!screenMin} />
</div>
))}
{fetching && <CircularProgress sx={{ mt: 1, mb: 2 }} />}
<div ref={bottomRef} />
</Box>
);
};
export default Terminal;

View file

@ -1,7 +1,7 @@
// ==============================|| THEME CONFIG ||============================== //
const config = {
defaultPath: '/ui',
defaultPath: '/cosmos-ui',
fontFamily: `'Public Sans', sans-serif`,
i18n: 'en',
miniDrawer: false,

View file

@ -21,10 +21,72 @@
}
@keyframes pulsing {
0% { -webkit-transform: scale(1); }
50% { -webkit-transform: scale(1.1); }
100% { -webkit-transform: scale(1); }
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1.0);
}
}
.dot {
background-color: #222;
border-radius: 100%;
width: 1.5rem;
height: 1.5rem;
margin: 0 0.25rem;
/* Animation */
animation: bounce 1.4s infinite;
animation-fill-mode: both;
}
@media (prefers-color-scheme: dark) {
.dot {
background-color: #eee;
}
}
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
.dot:nth-child(3) {
animation-delay: 0s;
}
.loader {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.shinyButton {
overflow: hidden;
}
.stickyButton {
position: fixed;
bottom: 20px;
width: 100%;
left: 0;
right: 0;
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.50);
z-index: 10;
}
.shinyButton:before {
position: absolute;
content: '';
@ -44,4 +106,46 @@
.code {
background-color: rgba(0.2,0.2,0.2,0.2);
}
@media (prefers-color-scheme: dark) {
.MuiPopper-root > * {
color:white;
background-color: rgba(0,0,0,0.8);
}
}
.darken {
filter: brightness(0.5);
}
.MuiAlert-icon {
align-items: center;
}
.loading-image:empty {
/* background: url('assets/images/icons/cosmos_gray.png') no-repeat center center;
background-size: contain; */
}
.raw-table table {
width: 100%;
border-collapse: collapse;
}
.raw-table table th {
text-align: left;
}
.raw-table table th, .raw-table table td {
padding: 5px;
border: 1px solid #ccc;
}
.pulsing {
animation: pulsing 2s ease-in-out infinite;
}
.force-light > * {
color: black !important;
}

View file

@ -1,6 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import customParseFormat from 'dayjs/plugin/customParseFormat'; // import this if you need to parse custom formats
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import localizedFormat from 'dayjs/plugin/localizedFormat'; // import this for localized formatting
import 'dayjs/locale/en-gb';
// scroll bar
import 'simplebar/src/simplebar.css';
@ -17,6 +21,13 @@ import './index.css';
import App from './App';
import { store } from './store';
import reportWebVitals from './reportWebVitals';
import { LocalizationProvider } from '@mui/x-date-pickers';
import dayjs from 'dayjs';
import 'dayjs/locale/en-gb';
dayjs.extend(customParseFormat); // if needed
dayjs.extend(localizedFormat); // if needed
dayjs.locale('en-gb');
// ==============================|| MAIN - REACT DOM RENDER ||============================== //
@ -25,8 +36,10 @@ const root = createRoot(container); // createRoot(container!) if you use TypeScr
root.render(
<StrictMode>
<ReduxProvider store={store}>
<BrowserRouter basename="/">
<App />
<BrowserRouter basename="/">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<App />
</LocalizationProvider>
</BrowserRouter>
</ReduxProvider>
</StrictMode>

View file

@ -1,17 +1,25 @@
import * as API from './api';
import { useEffect } from 'react';
import { redirectToLocal } from './utils/indexs';
const IsLoggedIn = () => useEffect(() => {
const urlSearch = encodeURIComponent(window.location.search);
const redirectToURL = (window.location.pathname + urlSearch);
const isLoggedIn = () => useEffect(() => {
console.log("CHECK LOGIN")
API.auth.me().then((data) => {
if(data.status != 'OK') {
if(data.status == 'NEW_INSTALL') {
window.location.href = '/ui/newInstall';
} else
window.location.href = '/ui/login';
redirectToLocal('/cosmos-ui/newInstall');
} else if (data.status == 'error' && data.code == "HTTP004") {
redirectToLocal('/cosmos-ui/login?redirect=' + redirectToURL);
} else if (data.status == 'error' && data.code == "HTTP006") {
redirectToLocal('/cosmos-ui/loginmfa?redirect=' + redirectToURL);
} else if (data.status == 'error' && data.code == "HTTP007") {
redirectToLocal('/cosmos-ui/newmfa?redirect=' + redirectToURL);
}
}
});
})
}, []);
export default isLoggedIn;
export default IsLoggedIn;

View file

@ -47,7 +47,7 @@ const NavGroup = ({ item }) => {
}
sx={{ mb: drawerOpen ? 1.5 : 0, py: 0, zIndex: 0 }}
>
{navCollapse}
{navCollapse}
</List>
);
};

View file

@ -9,6 +9,7 @@ import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography }
// project import
import { activeItem } from '../../../../../store/reducers/menu';
import { useClientInfos } from '../../../../../utils/hooks';
// ==============================|| NAVIGATION - LIST ITEM ||============================== //
@ -17,15 +18,23 @@ const NavItem = ({ item, level }) => {
const dispatch = useDispatch();
const menu = useSelector((state) => state.menu);
const { drawerOpen, openItem } = menu;
const {role} = useClientInfos();
const isAdmin = role === "2";
if (item.adminOnly && !isAdmin) {
return null;
}
let itemTarget = '_self';
if (item.target) {
itemTarget = '_blank';
}
let listItemProps = { component: forwardRef((props, ref) => <Link ref={ref} {...props} to={item.url} target={itemTarget} />) };
let listItemProps = { component: forwardRef((props, ref) => <Link ref={ref} {...props} to={item.url} target={itemTarget}
rel={itemTarget === '_blank' ? 'noopener noreferrer' : ''}
/>) };
if (item?.external) {
listItemProps = { component: 'a', href: item.url, target: itemTarget };
listItemProps = { component: 'a', href: item.url, target: itemTarget, rel: itemTarget === '_blank' ? 'noopener noreferrer' : '' };
}
const itemHandler = (id) => {
@ -52,6 +61,25 @@ const NavItem = ({ item, level }) => {
const textColor = 'text.primary';
const iconSelectedColor = 'primary.main';
// SET BETA (TODO REMOVE)
if(item.title === "Constellation")
item.title = <>{item.title} <span style={{
color: 'gray',
fontSize: '11px',
textDecoration: 'italic',
transform: 'translateY(-5px)',
display: 'inline-block',
}}>Beta</span></>;
if(item.title === "Monitoring")
item.title = <>{item.title} <span style={{
color: 'gray',
fontSize: '11px',
textDecoration: 'italic',
transform: 'translateY(-5px)',
display: 'inline-block',
}}>New!</span></>;
return (
<ListItemButton
{...listItemProps}

View file

@ -19,9 +19,9 @@ const DrawerHeader = ({ open }) => {
<Stack direction="row" spacing={1} alignItems="center">
<Logo />
<Chip
label={version}
label={version.replace('unstable', '')}
size="small"
sx={{ height: 16, '& .MuiChip-label': { fontSize: '0.625rem', py: 0.25 } }}
sx={{ height: 16, '& .MuiChip-label': { fontSize: '0.55rem', py: 0.25 } }}
component="a"
href="/"
clickable

View file

@ -1,4 +1,4 @@
import { useRef, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
@ -19,12 +19,16 @@ import {
Typography,
useMediaQuery
} from '@mui/material';
import * as timeago from 'timeago.js';
// project import
import MainCard from '../../../../components/MainCard';
import Transitions from '../../../../components/@extended/Transitions';
// assets
import { BellOutlined, CloseOutlined, GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons';
import { BellOutlined, CloseOutlined, ExclamationCircleOutlined, GiftOutlined, InfoCircleOutlined, MessageOutlined, SettingOutlined, WarningOutlined } from '@ant-design/icons';
import * as API from '../../../../api';
import { redirectToLocal } from '../../../../utils/indexs';
// sx styles
const avatarSX = {
@ -48,10 +52,55 @@ const actionSX = {
const Notification = () => {
const theme = useTheme();
const matchesXs = useMediaQuery(theme.breakpoints.down('md'));
const [notifications, setNotifications] = useState([]);
const [from, setFrom] = useState('');
const refreshNotifications = () => {
API.users.getNotifs(from).then((res) => {
setNotifications(() => res.data);
});
};
const setAsRead = () => {
let unread = [];
notifications.forEach((notif) => {
if (!notif.Read) {
unread.push(notif.ID);
}
})
if (unread.length > 0) {
API.users.readNotifs(unread);
}
}
const setLocalAsRead = (id) => {
let newN = notifications.map((notif) => {
notif.Read = true;
return notif;
})
setNotifications(newN);
}
useEffect(() => {
refreshNotifications();
const interval = setInterval(() => {
refreshNotifications();
}, 10000);
return () => clearInterval(interval);
}, []);
const anchorRef = useRef(null);
const [open, setOpen] = useState(false);
const handleToggle = () => {
if (!open) {
setAsRead();
}
setOpen((prevOpen) => !prevOpen);
};
@ -59,12 +108,48 @@ const Notification = () => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setLocalAsRead();
setOpen(false);
};
const getNotifIcon = (notification) => {
switch (notification.Level) {
case 'warn':
return <Avatar
sx={{
color: 'warning.main',
bgcolor: 'warning.lighter'
}}
>
<WarningOutlined />
</Avatar>
case 'error':
return <Avatar
sx={{
color: 'error.main',
bgcolor: 'error.lighter'
}}
>
<ExclamationCircleOutlined />
</Avatar>
default:
return <Avatar
sx={{
color: 'info.main',
bgcolor: 'info.lighter'
}}
>
<InfoCircleOutlined />
</Avatar>
}
};
const iconBackColor = theme.palette.mode === 'dark' ? 'grey.700' : 'grey.100';
const iconBackColorOpen = theme.palette.mode === 'dark' ? 'grey.800' : 'grey.200';
const nbUnread = notifications.filter((notif) => !notif.Read).length;
return (
<Box sx={{ flexShrink: 0, ml: 0.75 }}>
<IconButton
@ -77,7 +162,7 @@ const Notification = () => {
aria-haspopup="true"
onClick={handleToggle}
>
<Badge badgeContent={4} color="primary">
<Badge badgeContent={nbUnread} color="error">
<BellOutlined />
</Badge>
</IconButton>
@ -127,6 +212,8 @@ const Notification = () => {
<List
component="nav"
sx={{
maxHeight: 350,
overflow: 'auto',
p: 0,
'& .MuiListItemButton-root': {
py: 0.5,
@ -135,127 +222,43 @@ const Notification = () => {
}
}}
>
<ListItemButton>
<ListItemAvatar>
<Avatar
sx={{
color: 'success.main',
bgcolor: 'success.lighter'
}}
>
<GiftOutlined />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="h6">
It&apos;s{' '}
<Typography component="span" variant="subtitle1">
Cristina danny&apos;s
</Typography>{' '}
birthday today.
</Typography>
}
secondary="2 min ago"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
3:00 AM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton>
<ListItemAvatar>
<Avatar
sx={{
color: 'primary.main',
bgcolor: 'primary.lighter'
}}
>
<MessageOutlined />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="h6">
<Typography component="span" variant="subtitle1">
Aida Burg
</Typography>{' '}
commented your post.
</Typography>
}
secondary="5 August"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
6:00 PM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton>
<ListItemAvatar>
<Avatar
sx={{
color: 'error.main',
bgcolor: 'error.lighter'
}}
>
<SettingOutlined />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="h6">
Your Profile is Complete &nbsp;
<Typography component="span" variant="subtitle1">
60%
</Typography>{' '}
</Typography>
}
secondary="7 hours ago"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
2:45 PM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton>
<ListItemAvatar>
<Avatar
sx={{
color: 'primary.main',
bgcolor: 'primary.lighter'
}}
>
C
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<Typography variant="h6">
<Typography component="span" variant="subtitle1">
Cristina Danny
</Typography>{' '}
invited to join{' '}
<Typography component="span" variant="subtitle1">
Meeting.
{notifications && notifications.map(notification => (<>
<ListItemButton onClick={() => {
notification.Link && redirectToLocal(notification.Link);
}}
style={{
borderLeft: notification.Read ? 'none' : `4px solid ${notification.Level === 'warn' ? theme.palette.warning.main : notification.Level === 'error' ? theme.palette.error.main : theme.palette.info.main}`,
paddingLeft: notification.Read ? '14px' : '10px',
}}>
<ListItemAvatar>
{getNotifIcon(notification)}
</ListItemAvatar>
<ListItemText
primary={<>
<Typography variant={notification.Read ? 'body' : 'h6'} noWrap>
{notification.Title}
</Typography>
<div style={{
overflow: 'hidden',
maxHeight: '48px',
borderLeft: '1px solid grey',
paddingLeft: '8px',
margin: '2px'
}}>
{notification.Message}
</div></>
}
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
{timeago.format(notification.Date)}
</Typography>
}
secondary="Daily scrum meeting time"
/>
<ListItemSecondaryAction>
<Typography variant="caption" noWrap>
9:10 PM
</Typography>
</ListItemSecondaryAction>
</ListItemButton>
<Divider />
<ListItemButton sx={{ textAlign: 'center', py: `${12}px !important` }}>
</ListItemSecondaryAction>
</ListItemButton>
<Divider /></>))}
{/* <ListItemButton sx={{ textAlign: 'center', py: `${12}px !important` }}>
<ListItemText
primary={
<Typography variant="h6" color="primary">
@ -263,7 +266,7 @@ const Notification = () => {
</Typography>
}
/>
</ListItemButton>
</ListItemButton> */}
</List>
</MainCard>
</ClickAwayListener>

View file

@ -1,5 +1,5 @@
// material-ui
import { Box, Chip, IconButton, Link, useMediaQuery } from '@mui/material';
import { Box, Chip, IconButton, Link, Stack, useMediaQuery } from '@mui/material';
import { GithubOutlined } from '@ant-design/icons';
// project import
@ -18,10 +18,13 @@ const HeaderContent = () => {
{!matchesXs && <Search />}
{matchesXs && <Box sx={{ width: '100%', ml: 1 }} />}
<Link href="/ui/logout" underline="none">
<Chip label="Logout" />
</Link>
{/* <Notification /> */}
<Stack direction="row" spacing={2}>
<Notification />
<Link href="/cosmos-ui/logout" underline="none">
<Chip label="Logout" />
</Link>
</Stack>
{/* {!matchesXs && <Profile />}
{matchesXs && <MobileSection />} */}
</>

View file

@ -50,7 +50,7 @@ const MainLayout = () => {
<Drawer open={open} handleDrawerToggle={handleDrawerToggle} />
<Box component="main" sx={{ width: '100%', flexGrow: 1, p: { xs: 2, sm: 3 } }}>
<Toolbar />
<Breadcrumbs navigation={navigation} title divider={false} />
<Breadcrumbs navigation={navigation} divider={false} />
<Outlet />
</Box>
</Box>

View file

@ -1,5 +1,5 @@
// assets
import { HomeOutlined, AppstoreOutlined } from '@ant-design/icons';
import { HomeOutlined, AppstoreOutlined, DashboardOutlined, AppstoreAddOutlined } from '@ant-design/icons';
// icons
const icons = {
@ -17,16 +17,26 @@ const dashboard = {
id: 'home',
title: 'Home',
type: 'item',
url: '/ui',
url: '/cosmos-ui/',
icon: icons.HomeOutlined,
breadcrumbs: false
},
{
id: 'servapps',
title: 'ServApps',
id: 'dashboard',
title: 'Monitoring',
type: 'item',
url: '/ui/servapps',
icon: AppstoreOutlined
url: '/cosmos-ui/monitoring',
icon: DashboardOutlined,
breadcrumbs: false,
adminOnly: true
},
{
id: 'market',
title: 'Market',
type: 'item',
url: '/cosmos-ui/market-listing',
icon: AppstoreAddOutlined,
breadcrumbs: false
},
]
};

View file

@ -1,5 +1,6 @@
// assets
import { ProfileOutlined, SettingOutlined, NodeExpandOutlined} from '@ant-design/icons';
import { ProfileOutlined, PicLeftOutlined, SettingOutlined, NodeExpandOutlined, AppstoreOutlined} from '@ant-design/icons';
import ConstellationIcon from '../assets/images/icons/constellation.png'
// icons
const icons = {
@ -7,7 +8,6 @@ const icons = {
ProfileOutlined,
SettingOutlined
};
// ==============================|| MENU ITEMS - EXTRA PAGES ||============================== //
const pages = {
@ -15,25 +15,50 @@ const pages = {
title: 'Management',
type: 'group',
children: [
{
id: 'servapps',
title: 'ServApps',
type: 'item',
url: '/cosmos-ui/servapps',
icon: AppstoreOutlined,
adminOnly: true
},
{
id: 'url',
title: 'URLs',
type: 'item',
url: '/ui/config-url',
url: '/cosmos-ui/config-url',
icon: icons.NodeExpandOutlined,
},
{
id: 'users',
title: 'Manage Users',
id: 'constellation',
title: 'Constellation',
type: 'item',
url: '/ui/config-users',
url: '/cosmos-ui/constellation',
icon: () => <img height="28px" width="28px" style={{marginLeft: "-6px"}} src={ConstellationIcon} />,
},
{
id: 'users',
title: 'Users',
type: 'item',
url: '/cosmos-ui/config-users',
icon: icons.ProfileOutlined,
adminOnly: true
},
{
id: 'openid',
title: 'OpenID',
type: 'item',
url: '/cosmos-ui/openid-manage',
icon: PicLeftOutlined,
adminOnly: true
},
{
id: 'config',
title: 'Configuration',
type: 'item',
url: '/ui/config-general',
url: '/cosmos-ui/config-general',
icon: icons.SettingOutlined,
}
]

View file

@ -41,7 +41,7 @@ const support = {
id: 'documentation',
title: 'Documentation',
type: 'item',
url: 'https://github.com/azukaar/Cosmos-Server/wiki',
url: 'https://cosmos-cloud.io/doc',
icon: QuestionOutlined,
external: true,
target: true

View file

@ -7,10 +7,15 @@ import { Grid, Stack, Typography } from '@mui/material';
import AuthLogin from './auth-forms/AuthLogin';
import AuthWrapper from './AuthWrapper';
const selfport = new URL(window.location.href).port
const selfprotocol = new URL(window.location.href).protocol + "//"
const selfHostname = selfprotocol + (new URL(window.location.href).hostname) + (selfport ? ":" + selfport : "")
// ================================|| LOGIN ||================================ //
const Login = () => (
<AuthWrapper>
<link rel="openid2.provider openid.server" href={selfHostname + "/oauth2/auth"} />
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>

View file

@ -9,6 +9,7 @@ import AuthWrapper from './AuthWrapper';
import { useEffect } from 'react';
import * as API from '../../api';
import { redirectTo, redirectToLocal } from '../../utils/indexs';
// ================================|| REGISTER ||================================ //
@ -17,7 +18,7 @@ const Logout = () => {
API.auth.logout()
.then(() => {
setTimeout(() => {
window.location.href = '/ui/login';
redirectToLocal('/cosmos-ui/login');
}, 2000);
});
},[]);

View file

@ -15,7 +15,7 @@ const Signup = () => (
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<Typography variant="h3">Sign up</Typography>
<Typography component={Link} to="/ui/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
<Typography component={Link} to="/cosmos-ui/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
Already have an account?
</Typography>
</Stack>

View file

@ -1,5 +1,5 @@
import React, { useEffect } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { Link, Link as RouterLink } from 'react-router-dom';
// material-ui
import {
@ -9,7 +9,6 @@ import {
FormControlLabel,
FormHelperText,
Grid,
Link,
IconButton,
InputAdornment,
InputLabel,
@ -31,11 +30,14 @@ import AnimateButton from '../../../components/@extended/AnimateButton';
// assets
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import { LoadingButton } from '@mui/lab';
import { redirectToLocal } from '../../../utils/indexs';
// ============================|| FIREBASE - LOGIN ||============================ //
const AuthLogin = () => {
const [checked, setChecked] = React.useState(false);
const [showResetPassword, setShowResetPassword] = React.useState(false);
const [showPassword, setShowPassword] = React.useState(false);
const handleClickShowPassword = () => {
@ -50,18 +52,26 @@ const AuthLogin = () => {
// TODO: Extract ?redirect=<URL> to redirect to a specific page after login
const urlSearchParams = new URLSearchParams(window.location.search);
const notLogged = urlSearchParams.get('notlogged') == 1;
const notLoggedAdmin = urlSearchParams.get('notlogged') == 2;
const invalid = urlSearchParams.get('invalid') == 1;
const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/ui';
const redirectToURL = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/cosmos-ui';
useEffect(() => {
API.auth.me().then((data) => {
if(data.status == 'OK') {
window.location.href = redirectTo;
redirectToLocal(redirectToURL);
} else if(data.status == 'NEW_INSTALL') {
window.location.href = '/ui/newInstall';
redirectToLocal('/cosmos-ui/newInstall');
}
});
});
API.config.canSendEmail().then((resp) => {
if(resp.status == 'OK' && resp.data.canSendEmail) {
setShowResetPassword(true);
}
});
}, []);
return (
<>
@ -70,6 +80,11 @@ const AuthLogin = () => {
<br />
</Grid>}
{ notLoggedAdmin &&<Grid container spacing={2} justifyContent="center">
<Alert severity="error">You need to be Admin</Alert>
<br />
</Grid>}
{ invalid &&<Grid container spacing={2} justifyContent="center">
<Alert severity="error">You have been disconnected. Please login to continue</Alert>
<br />
@ -85,30 +100,22 @@ const AuthLogin = () => {
password: Yup.string().max(255).required('Password is required')
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
API.auth.login(values).then((data) => {
if(data.status == 'error') {
setStatus({ success: false });
if(data.code == 'UL001') {
setErrors({ submit: 'Wrong nickname or password. Try again or try resetting your password' });
} else if (data.code == 'UL002') {
setErrors({ submit: 'You have not yet registered your account. You should have an invite link in your emails. If you need a new one, contact your administrator.' });
} else if(data.status == 'error') {
setErrors({ submit: 'Unexpected error. Try again later.' });
}
setSubmitting(false);
return;
} else {
setStatus({ success: true });
setSubmitting(false);
window.location.href = redirectTo;
}
})
} catch (err) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(true);
return API.auth.login(values).then((data) => {
setStatus({ success: true });
setSubmitting(false);
}
redirectToLocal(redirectToURL);
}).catch((err) => {
setStatus({ success: false });
if(err.code == 'UL001') {
setErrors({ submit: 'Wrong nickname or password. Try again or try resetting your password' });
} else if (err.code == 'UL002') {
setErrors({ submit: 'You have not yet registered your account. You should have an invite link in your emails. If you need a new one, contact your administrator.' });
} else {
setErrors({ submit: 'Unexpected error. Try again later.' });
}
setSubmitting(false);
});
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
@ -183,10 +190,13 @@ const AuthLogin = () => {
/>
}
label={<Typography variant="h6">Keep me sign in</Typography>}
/>
<Link variant="h6" component={RouterLink} to="" color="text.primary">
Forgot Password?
</Link> */}
/>*/}
{showResetPassword && <Link variant="h6" component={RouterLink} to="/cosmos-ui/forgot-password" color="primary">
Forgot Your Password?
</Link>}
{!showResetPassword && <Typography variant="h6">
This server does not allow password reset.
</Typography>}
</Stack>
</Grid>
{errors.submit && (
@ -195,10 +205,9 @@ const AuthLogin = () => {
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button
<LoadingButton
disableElevation
disabled={isSubmitting}
loading={isSubmitting}
fullWidth
size="large"
type="submit"
@ -206,8 +215,7 @@ const AuthLogin = () => {
color="primary"
>
Login
</Button>
</AnimateButton>
</LoadingButton>
</Grid>
{/* <Grid item xs={12}>
<Divider>

View file

@ -32,6 +32,8 @@ import { strengthColor, strengthIndicator } from '../../../utils/password-streng
// assets
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import { LoadingButton } from '@mui/lab';
import { redirectToLocal } from '../../../utils/indexs';
// ============================|| FIREBASE - REGISTER ||============================ //
@ -72,8 +74,8 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
.max(255)
.required('Password is required')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/,
'Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and one special case Character'
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[~!@#$%\^&\*\(\)_\+=\-\{\[\}\]:;"'<,>\/])(?=.{9,})/,
'Must Contain 9 Characters, One Uppercase, One Lowercase, One Number and one special case Character (~!@#$%^&*()_+=-{[}]:;"\'<>.?/)'
),
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
@ -84,7 +86,7 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
}).then((res) => {
setStatus({ success: true });
setSubmitting(false);
window.location.href = '/ui/login';
redirectToLocal('/cosmos-ui/login');
}).catch((err) => {
setStatus({ success: false });
setErrors({ submit: err.message });
@ -178,10 +180,9 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button
<LoadingButton
disableElevation
disabled={isSubmitting}
loading={isSubmitting}
fullWidth
size="large"
type="submit"
@ -191,8 +192,7 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
{
isRegister ? 'Register' : 'Reset Password'
}
</Button>
</AnimateButton>
</LoadingButton>
</Grid>
</Grid>
</form>

View file

@ -0,0 +1,122 @@
import { Link } from 'react-router-dom';
// material-ui
import { Button, FormHelperText, Grid, InputLabel, OutlinedInput, Stack, Typography } from '@mui/material';
// project import
import AuthWrapper from './AuthWrapper';
import { Formik } from 'formik';
// third-party
import * as Yup from 'yup';
import * as API from '../../api';
import { CosmosInputText } from '../config/users/formShortcuts';
import { useState } from 'react';
// ================================|| LOGIN ||================================ //
const ForgotPassword = () => {
const [isSuccess, setIsSuccess] = useState(false);
return (<AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<Typography variant="h3">Password Reset</Typography>
{/* <Typography component={Link} to="/register" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
Don&apos;t have an account?
</Typography> */}
</Stack>
</Grid>
<Grid item xs={12}>
{!isSuccess && <Formik
initialValues={{
nickname: '',
email: '',
}}
validationSchema={Yup.object().shape({
nickname: Yup.string().max(255).required('Nickname is required'),
email: Yup.string().email('Must be a valid email').max(255).required('Email is required'),
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
API.users.resetPassword(values).then((data) => {
if (data.status == 'error') {
setStatus({ success: false });
setErrors({ submit: 'Unexpected error. Check your infos or try again later.' });
setSubmitting(false);
return;
} else {
setStatus({ success: true });
setSubmitting(false);
setIsSuccess(true);
}
})
} catch (err) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<Grid container spacing={3}>
<CosmosInputText
name="nickname"
label="Nickname"
formik={formik}
/>
<CosmosInputText
name="email"
label="Email"
type="email"
formik={formik}
/>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<Button
disableElevation
disabled={formik.isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
Reset Password
</Button>
</Grid>
</Grid>
</form>
)}
</Formik>}
{isSuccess && <div>
<Typography variant="h6">Check your email for a link to reset your password. If it doesnt appear within a few minutes, check your spam folder.</Typography>
<br/><br/>
<Button
disableElevation
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
component={Link}
to="/cosmos-ui/login"
>
Back to login
</Button>
</div>}
</Grid>
</Grid>
</AuthWrapper>
)};
export default ForgotPassword;

View file

@ -0,0 +1,199 @@
import { Link } from 'react-router-dom';
// material-ui
import {
Button,
Checkbox,
Divider,
FormControlLabel,
FormHelperText,
Grid,
IconButton,
InputAdornment,
InputLabel,
OutlinedInput,
Stack,
Typography,
Alert,
TextField,
Tooltip
} from '@mui/material';
// project import
import AuthWrapper from './AuthWrapper';
import { useEffect, useState, useRef } from 'react';
import * as Yup from 'yup';
import * as API from '../../api';
import QRCode from 'qrcode';
import { useTheme } from '@mui/material/styles';
import { Formik } from 'formik';
import { LoadingButton } from '@mui/lab';
import { CosmosCollapse } from '../config/users/formShortcuts';
import { redirectToLocal } from '../../utils/indexs';
const MFALoginForm = () => {
const urlSearchParams = new URLSearchParams(window.location.search);
const redirectToURL = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/cosmos-ui';
useEffect(() => {
API.auth.me().then((data) => {
if(data.status == 'OK') {
redirectToLocal(redirectToURL);
} else if(data.status == 'NEW_INSTALL') {
redirectToLocal('/cosmos-ui/newInstall');
}
});
});
return <Formik
initialValues={{
token: '',
}}
validationSchema={Yup.object().shape({
token: Yup.string().required('Token is required').min(6, 'Token must be at least 6 characters').max(6, 'Token must be at most 6 characters'),
})}
onSubmit={(values, { setSubmitting, setStatus, setErrors }) => {
API.users.check2FA(values.token).then((data) => {
redirectToLocal(redirectToURL);
}).catch((error) => {
console.log(error)
setStatus({ success: false });
setErrors({ submit: "Wrong OTP. Try again" });
setSubmitting(false);
});
}}
>
{(formik) => (
<form autoComplete="off" noValidate onSubmit={formik.handleSubmit}>
<Stack spacing={3}>
<TextField
fullWidth
autoComplete="off"
type="text"
label="Token"
{...formik.getFieldProps('token')}
error={formik.touched.token && formik.errors.token && true}
helperText={formik.touched.token && formik.errors.token && formik.errors.token}
autoFocus
/>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<LoadingButton
fullWidth
size="large"
type="submit"
variant="contained"
loading={formik.isSubmitting}
>
Login
</LoadingButton>
</Stack>
</form>
)}
</Formik>;
}
const MFASetup = () => {
const [mfaCode, setMfaCode] = useState('');
const canvasRef = useRef(null);
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const getCode = () => {
return API.users.new2FA().then(({data}) => {
if (data) {
setMfaCode(data.key);
QRCode.toCanvas(canvasRef.current, data.key, {
width: 300,
color: {
dark: theme.palette.secondary.main,
light: '#ffffff'
}
}, function (error) {
if (error) console.error(error)
})
}
});
};
useEffect(() => {
getCode();
}, []);
return (
<Grid container spacing={3}>
<Grid item xs={12}>
<Typography variant="h5">This server requires 2FA. Scan this QR code with your <Tooltip title="For example FreeOTP(+) or Google/Microsoft authenticator"><span style={{cursor: 'pointer', textDecoration:"underline dotted"}}>authenticator app</span></Tooltip> to proceed</Typography>
</Grid>
<Grid item xs={12} textAlign={'center'}>
<canvas style={{borderRadius: '15px'}} ref={canvasRef} />
</Grid>
<Grid item xs={12}>
<Typography variant="h5">...Or enter this code manually in it</Typography>
</Grid>
<Grid item xs={12}>
<CosmosCollapse title="Show manual code" defaultExpanded={false}>
<div style={{padding: '20px', fontSize: '90%', borderRadius: '15px', background: 'rgba(0,0,0,0.2)'}}>
{mfaCode && <span>{mfaCode.split('?')[1].split('&').map(a => <div>{decodeURI(a).replace('=', ': ')}</div>)}</span>}
</div>
</CosmosCollapse>
</Grid>
<Grid item xs={12}>
<Typography variant="h5">Once you have scanned the QR code or entered the code manually, enter the token from your authenticator app below</Typography>
</Grid>
<Grid item xs={12}>
<MFALoginForm />
</Grid>
<Grid item xs={12}>
<Link to="/cosmos-ui/logout">
<Typography variant="h5">Logout</Typography>
</Link>
</Grid>
</Grid>
);
}
const NewMFA = () => (
<AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<Typography variant="h3">New MFA Setup</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<MFASetup />
</Grid>
</Grid>
</AuthWrapper>
);
const MFALogin = () => (
<AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<Typography variant="h3">Enter your OTP</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<MFALoginForm />
</Grid>
</Grid>
</AuthWrapper>
);
export default NewMFA;
export {
MFASetup,
NewMFA,
MFALogin,
MFALoginForm
};

View file

@ -0,0 +1,113 @@
import { Link, useSearchParams } from 'react-router-dom';
// material-ui
import { Checkbox, Grid, Stack, Typography } from '@mui/material';
// project import
import AuthLogin from './auth-forms/AuthLogin';
import AuthWrapper from './AuthWrapper';
import { getFaviconURL } from '../../utils/routes';
import IsLoggedIn from '../../isLoggedIn';
import { LoadingButton } from '@mui/lab';
import { Field, useFormik } from 'formik';
import { useState } from 'react';
// ================================|| LOGIN ||================================ //
const OpenID = () => {
const [searchParams, setSearchParams] = useSearchParams();
const client_id = searchParams.get("client_id")
const redirect_uri = searchParams.get("redirect_uri")
const scope = searchParams.get("scope")
const entireSearch = searchParams.toString()
const [checkedScopes, setCheckedScopes] = useState(["openid"])
let icon;
// get hostname from redirect_uri with port
let port, protocol, appHostname;
try {
port = new URL(redirect_uri).port
protocol = new URL(redirect_uri).protocol + "//"
appHostname = protocol + (new URL(redirect_uri).hostname) + (port ? ":" + port : "")
icon = getFaviconURL({
Mode: 'PROXY',
Target: appHostname
});
} catch (e) {
icon = getFaviconURL();
}
const selfport = new URL(window.location.href).port
const selfprotocol = new URL(window.location.href).protocol + "//"
const selfHostname = selfprotocol + (new URL(window.location.href).hostname) + (selfport ? ":" + selfport : "")
const onchange = (e, scope) => {
if (e.target.checked) {
setCheckedScopes([...checkedScopes, scope])
} else {
setCheckedScopes(checkedScopes.filter((scope) => scope != scope))
}
}
return (<AuthWrapper>
<IsLoggedIn />
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={2}>
<Typography variant="h3">Login with OpenID - {client_id}</Typography>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" spacing={2} style={{
alignItems: 'center',
}}>
<img src={icon} alt={'icon'} width="64px" />
<div>
You are about to login into <b>{client_id}</b>. <br />
Check which permissions you are giving to this application. <br />
</div>
</Stack>
</Stack>
</Grid>
<Grid item xs={12}>
<link rel="openid2.provider openid.server" href={selfHostname + "/oauth2/auth"} />
<form action={"/oauth2/auth?" + entireSearch} method="post">
<input type="hidden" name="client_id" value={client_id} />
{scope.split(' ').map((scope) => {
return scope == "openid" ? <div>
<input type="checkbox" name="scopes" value={scope} checked hidden />
<Checkbox checked disabled />
account
</div>
: <div>
<input type="checkbox" name="scopes" hidden value={scope} checked={checkedScopes.includes(scope)} />
<Checkbox onChange={(e) => onchange(e, scope)} />
{scope}
</div>
})}
<div style={{
fontSize: '0.8rem',
marginTop: '15px',
marginBottom: '20px',
opacity: '0.8',
fontStyle: 'italic',
}}>
You will be redirected to <b>{redirect_uri}</b> after login. <br />
</div>
<LoadingButton
disableElevation
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
OpenID Login
</LoadingButton>
</form>
</Grid>
</Grid>
</AuthWrapper>)
};
export default OpenID;

View file

@ -0,0 +1,86 @@
import { useParams } from "react-router";
import Back from "../../components/back";
import { Alert, CircularProgress, Stack } from "@mui/material";
import PrettyTabbedView from "../../components/tabbedView/tabbedView";
import RouteManagement from "./routes/routeman";
import { useEffect, useState } from "react";
import * as API from "../../api";
import RouteSecurity from "./routes/routeSecurity";
import RouteOverview from "./routes/routeoverview";
import IsLoggedIn from "../../isLoggedIn";
import RouteMetrics from "../dashboard/routeMonitoring";
import EventExplorerStandalone from "../dashboard/eventsExplorerStandalone";
const RouteConfigPage = () => {
const { routeName } = useParams();
const [config, setConfig] = useState(null);
let currentRoute = null;
if (config) {
currentRoute = config.HTTPConfig.ProxyConfig.Routes.find((r) => r.Name === routeName);
}
const refreshConfig = () => {
API.config.get().then((res) => {
setConfig(res.data);
});
};
useEffect(() => {
refreshConfig();
}, []);
return <div>
<IsLoggedIn />
<h2>
<Stack spacing={1}>
<Stack direction="row" spacing={1} alignItems="center">
<Back />
<div>{routeName}</div>
</Stack>
{config && !currentRoute && <div>
<Alert severity="error">Route not found</Alert>
</div>}
{config && currentRoute && <PrettyTabbedView tabs={[
{
title: 'Overview',
children: <RouteOverview routeConfig={currentRoute} />
},
{
title: 'Setup',
children: <RouteManagement
title="Setup"
submitButton
routeConfig={currentRoute}
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
config={config}
/>
},
{
title: 'Security',
children: <RouteSecurity
routeConfig={currentRoute}
config={config}
/>
},
{
title: 'Monitoring',
children: <RouteMetrics routeName={routeName} />
},
{
title: 'Events',
children: <EventExplorerStandalone initLevel='info' initSearch={`{"object":"route@${routeName}"}`}/>
},
]}/>}
{!config && <div style={{textAlign: 'center'}}>
<CircularProgress />
</div>}
</Stack>
</h2>
</div>
}
export default RouteConfigPage;

View file

@ -0,0 +1,98 @@
// material-ui
import { AppstoreAddOutlined, PlusCircleOutlined, ReloadOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons';
import { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Input, InputAdornment, TextField, Tooltip, Typography } from '@mui/material';
import Grid2 from '@mui/material/Unstable_Grid2/Grid2';
import { Stack } from '@mui/system';
import { useEffect, useState } from 'react';
import Paper from '@mui/material/Paper';
import { styled } from '@mui/material/styles';
import * as API from '../../../api';
import IsLoggedIn from '../../../isLoggedIn';
import RestartModal from '../../config/users/restart';
import RouteManagement from '../../config/routes/routeman';
import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../../utils/routes';
import HostChip from '../../../components/hostChip';
const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => {
const [openRestartModal, setOpenRestartModal] = useState(false);
const [submitErrors, setSubmitErrors] = useState([]);
const [newRoute, setNewRoute] = useState(null);
function addRoute() {
return API.config.addRoute(newRoute).then((res) => {
setOpenNewModal(false);
setOpenRestartModal(true);
});
}
const routes = config.HTTPConfig.ProxyConfig.Routes || [];
return <>
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} config={config} newRoute />
<Dialog open={openNewModal} onClose={() => setOpenNewModal(false)}>
<DialogTitle>New URL</DialogTitle>
{openNewModal && <>
<DialogContent>
<DialogContentText>
<Stack spacing={2}>
<div>
<RouteManagement
routeConfig={{
Target: "",
Mode: "SERVAPP",
Name: "New Route",
Description: "New Route",
UseHost: false,
Host: "",
UsePathPrefix: false,
PathPrefix: '',
CORSOrigin: '',
StripPathPrefix: false,
AuthEnabled: false,
Timeout: 14400000,
ThrottlePerMinute: 10000,
BlockCommonBots: true,
SmartShield: {
Enabled: true,
}
}}
routeNames={routes.map((r) => r.Name)}
setRouteConfig={(_newRoute) => {
setNewRoute(sanitizeRoute(_newRoute));
}}
up={() => {}}
down={() => {}}
deleteRoute={() => {}}
noControls
config={config}
/>
</div>
</Stack>
</DialogContentText>
</DialogContent>
<DialogActions>
{submitErrors && submitErrors.length > 0 && <Stack spacing={2} direction={"column"}>
<Alert severity="error">{submitErrors.map((err) => {
return <div>{err}</div>
})}</Alert>
</Stack>}
<Button onClick={() => setOpenNewModal(false)}>Cancel</Button>
<Button onClick={() => {
let errors = ValidateRoute(newRoute, config);
if (errors && errors.length > 0) {
setSubmitErrors(errors);
} else {
setSubmitErrors([]);
addRoute();
}
}}>Confirm</Button>
</DialogActions>
</>}
</Dialog>
</>;
}
export default NewRouteCreate;

View file

@ -0,0 +1,255 @@
import * as React from 'react';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik } from 'formik';
import {
Alert,
Button,
Divider,
Grid,
Stack,
} from '@mui/material';
import RestartModal from '../users/restart';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
import { snackit } from '../../../api/wrap';
const RouteSecurity = ({ routeConfig, config }) => {
const [openModal, setOpenModal] = React.useState(false);
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} config={config} />
{routeConfig && <>
<Formik
initialValues={{
AuthEnabled: routeConfig.AuthEnabled,
Timeout: routeConfig.Timeout,
ThrottlePerMinute: routeConfig.ThrottlePerMinute,
CORSOrigin: routeConfig.CORSOrigin,
MaxBandwith: routeConfig.MaxBandwith,
BlockAPIAbuse: routeConfig.BlockAPIAbuse,
BlockCommonBots: routeConfig.BlockCommonBots,
AdminOnly: routeConfig.AdminOnly,
VerboseForwardHeader: routeConfig.VerboseForwardHeader,
DisableHeaderHardening: routeConfig.DisableHeaderHardening,
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
_SmartShield_PolicyStrictness: (routeConfig.SmartShield ? routeConfig.SmartShield.PolicyStrictness : 0),
_SmartShield_PerUserTimeBudget: (routeConfig.SmartShield ? routeConfig.SmartShield.PerUserTimeBudget : 0),
_SmartShield_PerUserRequestLimit: (routeConfig.SmartShield ? routeConfig.SmartShield.PerUserRequestLimit : 0),
_SmartShield_PerUserByteLimit: (routeConfig.SmartShield ? routeConfig.SmartShield.PerUserByteLimit : 0),
_SmartShield_PerUserSimultaneous: (routeConfig.SmartShield ? routeConfig.SmartShield.PerUserSimultaneous : 0),
_SmartShield_MaxGlobalSimultaneous: (routeConfig.SmartShield ? routeConfig.SmartShield.MaxGlobalSimultaneous : 0),
_SmartShield_PrivilegedGroups: (routeConfig.SmartShield ? routeConfig.SmartShield.PrivilegedGroups : []),
}}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
const fullValues = {
...routeConfig,
...values,
}
if(!fullValues.SmartShield) {
fullValues.SmartShield = {};
}
fullValues.SmartShield.Enabled = values._SmartShield_Enabled;
delete fullValues._SmartShield_Enabled;
fullValues.SmartShield.PolicyStrictness = values._SmartShield_PolicyStrictness;
delete fullValues._SmartShield_PolicyStrictness;
fullValues.SmartShield.PerUserTimeBudget = values._SmartShield_PerUserTimeBudget;
delete fullValues._SmartShield_PerUserTimeBudget;
fullValues.SmartShield.PerUserRequestLimit = values._SmartShield_PerUserRequestLimit;
delete fullValues._SmartShield_PerUserRequestLimit;
fullValues.SmartShield.PerUserByteLimit = values._SmartShield_PerUserByteLimit;
delete fullValues._SmartShield_PerUserByteLimit;
fullValues.SmartShield.PerUserSimultaneous = values._SmartShield_PerUserSimultaneous;
delete fullValues._SmartShield_PerUserSimultaneous;
fullValues.SmartShield.MaxGlobalSimultaneous = values._SmartShield_MaxGlobalSimultaneous;
delete fullValues._SmartShield_MaxGlobalSimultaneous;
fullValues.SmartShield.PrivilegedGroups = values._SmartShield_PrivilegedGroups;
delete fullValues._SmartShield_PrivilegedGroups;
API.config.replaceRoute(routeConfig.Name, fullValues).then((res) => {
if (res.status == "OK") {
setStatus({ success: true });
snackit('Route updated successfully', 'success');
setSubmitting(false);
setOpenModal(true);
} else {
setStatus({ success: false });
setErrors({ submit: res.status });
setSubmitting(false);
}
});
}}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<Stack spacing={2}>
<MainCard name={routeConfig.Name} title={'Security'}>
<Grid container spacing={2}>
<CosmosFormDivider title={'Authentication'} />
<CosmosCheckbox
name="AuthEnabled"
label="Authentication Required"
formik={formik}
/>
<CosmosCheckbox
name="AdminOnly"
label="Admin only"
formik={formik}
/>
<CosmosFormDivider title={'Headers'} />
<CosmosCheckbox
name="VerboseForwardHeader"
label="Forward IP and Host Headers to target"
formik={formik}
/>
<CosmosCheckbox
name="DisableHeaderHardening"
label="Disable Header Hardening"
formik={formik}
/>
<CosmosFormDivider title={'Smart Shield'} />
<CosmosCheckbox
name="_SmartShield_Enabled"
label="Smart Shield Protection"
formik={formik}
/>
<CosmosSelect
name="_SmartShield_PolicyStrictness"
label="Policy Strictness"
placeholder="Policy Strictness"
options={[
[0, 'Default'],
[1, 'Strict'],
[2, 'Normal'],
[3, 'Lenient'],
]}
formik={formik}
/>
<CosmosInputText
name="_SmartShield_PerUserTimeBudget"
label="Per User Time Budget in milliseconds (0 for default)"
placeholder="Per User Time Budget"
type="number"
formik={formik}
/>
<CosmosInputText
name="_SmartShield_PerUserRequestLimit"
label="Per User Request Limit (0 for default)"
placeholder="Per User Request Limit"
type="number"
formik={formik}
/>
<CosmosInputText
name="_SmartShield_PerUserByteLimit"
label="Per User Byte Limit (0 for default)"
placeholder="Per User Byte Limit"
type="number"
formik={formik}
/>
<CosmosInputText
name="_SmartShield_PerUserSimultaneous"
label="Per User Simultaneous Connections Limit (0 for default)"
placeholder="Per User Simultaneous Connections Limit"
type="number"
formik={formik}
/>
<CosmosInputText
name="_SmartShield_MaxGlobalSimultaneous"
label="Max Global Simultaneous Connections Limit (0 for default)"
placeholder="Max Global Simultaneous Connections Limit"
type="number"
formik={formik}
/>
<CosmosSelect
name="_SmartShield_PrivilegedGroups"
label="Privileged Groups "
placeholder="Privileged Group"
options={[
[0, 'Default'],
[1, 'Users & Admins'],
[2, 'Admin Only'],
]}
formik={formik}
/>
<CosmosFormDivider title={'Limits'} />
<CosmosInputText
name="Timeout"
label="Timeout in milliseconds (0 for no timeout, at least 60000 or less recommended)"
placeholder="Timeout"
type="number"
formik={formik}
/>
<CosmosInputText
name="MaxBandwith"
label="Maximum Bandwith limit per user in bytes per seconds (0 for no limit)"
placeholder="Maximum Bandwith"
type="number"
formik={formik}
/>
<CosmosInputText
name="ThrottlePerMinute"
label="Maximum number of requests Per Minute (0 for no limit, at least 2000 or less recommended)"
placeholder="Throttle Per Minute"
type="number"
formik={formik}
/>
<CosmosInputText
name="CORSOrigin"
label="Custom CORS Origin (Recommended to leave blank)"
placeholder="CORS Origin"
formik={formik}
/>
<CosmosCheckbox
name="BlockCommonBots"
label="Block Common Bots (Recommended)"
formik={formik}
/>
<CosmosCheckbox
name="BlockAPIAbuse"
label="Block requests without Referer header"
formik={formik}
/>
</Grid>
</MainCard>
<MainCard ><Button
fullWidth
disableElevation
size="large"
type="submit"
variant="contained"
color="primary"
>
Save
</Button></MainCard>
</Stack>
</form>
)}
</Formik>
</>}
</div>;
}
export default RouteSecurity;

View file

@ -0,0 +1,326 @@
import * as React from 'react';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik } from 'formik';
import * as Yup from 'yup';
import {
Alert,
Button,
Grid,
Stack,
FormHelperText,
} from '@mui/material';
import RestartModal from '../users/restart';
import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
import { CosmosContainerPicker } from '../users/containerPicker';
import { snackit } from '../../../api/wrap';
import { ValidateRouteSchema, sanitizeRoute } from '../../../utils/routes';
import { isDomain } from '../../../utils/indexs';
const Hide = ({ children, h }) => {
return h ? <div style={{ display: 'none' }}>
{children}
</div> : <>{children}</>
}
const debounce = (func, wait) => {
let timeout;
return function (...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
};
const checkHost = debounce((host, setHostError) => {
if(isDomain(host)) {
API.checkHost(host).then((data) => {
setHostError(null)
}).catch((err) => {
setHostError(err.message)
});
} else {
setHostError(null);
}
}, 500)
const RouteManagement = ({ routeConfig, routeNames, config, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false, newRoute }) => {
const [openModal, setOpenModal] = React.useState(false);
const [hostError, setHostError] = React.useState(null);
React.useEffect(() => {
if(routeConfig && routeConfig.Host) {
checkHost(routeConfig.Host, setHostError);
}
}, [])
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} config={config} />
{routeConfig && <>
<Formik
initialValues={{
Name: routeConfig.Name,
Description: routeConfig.Description,
Mode: routeConfig.Mode || "SERVAPP",
Target: routeConfig.Target,
UseHost: routeConfig.UseHost,
Host: routeConfig.Host,
AcceptInsecureHTTPSTarget: routeConfig.AcceptInsecureHTTPSTarget === true,
UsePathPrefix: routeConfig.UsePathPrefix,
PathPrefix: routeConfig.PathPrefix,
StripPathPrefix: routeConfig.StripPathPrefix,
AuthEnabled: routeConfig.AuthEnabled,
HideFromDashboard: routeConfig.HideFromDashboard,
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
RestrictToConstellation: routeConfig.RestrictToConstellation,
OverwriteHostHeader: routeConfig.OverwriteHostHeader,
WhitelistInboundIPs: routeConfig.WhitelistInboundIPs && routeConfig.WhitelistInboundIPs.join(', '),
}}
validationSchema={ValidateRouteSchema}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
if(!submitButton) {
return false;
} else {
let commaSepIps = values.WhitelistInboundIPs;
if(commaSepIps) {
values.WhitelistInboundIPs = commaSepIps.split(',').map((ip) => ip.trim());
}
let fullValues = {
...routeConfig,
...values,
}
fullValues = sanitizeRoute(fullValues);
let op;
if(newRoute) {
op = API.config.newRoute(routeConfig.Name, fullValues)
} else {
op = API.config.replaceRoute(routeConfig.Name, fullValues)
}
op.then((res) => {
if (res.status == "OK") {
setStatus({ success: true });
snackit('Route updated successfully', 'success')
setSubmitting(false);
setOpenModal(true);
} else {
setStatus({ success: false });
setErrors({ submit: res.status });
setSubmitting(false);
}
});
}
}}
validate={(values) => {
let fullValues = {
...routeConfig,
...values,
}
// check name is unique
if (newRoute && routeNames.includes(fullValues.Name)) {
return { Name: 'Name must be unique' }
}
setRouteConfig && debounce(() => setRouteConfig(fullValues), 500)();
}}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<Stack spacing={2}>
<MainCard name={routeConfig.Name} title={
noControls ? 'New URL' :
<div>{title || routeConfig.Name}</div>
}>
<Grid container spacing={2}>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<CosmosInputText
name="Name"
label="Name"
placeholder="Name"
formik={formik}
/>
<CosmosInputText
name="Description"
label="Description"
placeholder="Description"
formik={formik}
/>
<Hide h={lockTarget}>
<CosmosFormDivider title={'Target Type'} />
<Grid item xs={12}>
<Alert color='info'>What are you trying to access with this route?</Alert>
</Grid>
<CosmosSelect
name="Mode"
label="Mode"
formik={formik}
disabled={lockTarget}
options={[
["SERVAPP", "ServApp - Docker Container"],
["PROXY", "Proxy"],
["STATIC", "Static Folder"],
["SPA", "Single Page Application"],
["REDIRECT", "Redirection"]
]}
/>
</Hide>
<CosmosFormDivider title={'Target Settings'} />
{
(formik.values.Mode === "SERVAPP") ?
<CosmosContainerPicker
formik={formik}
lockTarget={lockTarget}
TargetContainer={TargetContainer}
onTargetChange={() => {
setRouteConfig && setRouteConfig(formik.values);
}}
/>
: <CosmosInputText
name="Target"
label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
placeholder={formik.values.Mode == "PROXY" ? "http://localhost:8080" : "/path/to/my/app"}
formik={formik}
/>
}
{formik.values.Target.startsWith('https://') && <CosmosCheckbox
name="AcceptInsecureHTTPSTarget"
label="Accept Insecure HTTPS Target (not recommended)"
formik={formik}
/>}
<CosmosFormDivider title={'Source'} />
<Grid item xs={12}>
<Alert color='info'>What URL do you want to access your target from?</Alert>
</Grid>
<CosmosCheckbox
name="UseHost"
label="Use Host"
formik={formik}
/>
{formik.values.UseHost && (<><CosmosInputText
name="Host"
label="Host"
placeholder="Host"
formik={formik}
style={{ paddingLeft: '20px' }}
onChange={(e) => {
checkHost(e.target.value, setHostError)
}}
/>
{hostError && <Grid item xs={12}>
<Alert color='error'>{hostError}</Alert>
</Grid>}
</>
)}
<CosmosCheckbox
name="UsePathPrefix"
label="Use Path Prefix"
formik={formik}
/>
{formik.values.UsePathPrefix && <CosmosInputText
name="PathPrefix"
label="Path Prefix"
placeholder="Path Prefix"
formik={formik}
style={{ paddingLeft: '20px' }}
/>}
{formik.values.UsePathPrefix && <CosmosCheckbox
name="StripPathPrefix"
label="Strip Path Prefix"
formik={formik}
style={{ paddingLeft: '20px' }}
/>}
<CosmosFormDivider title={'Basic Security'} />
<CosmosCheckbox
name="AuthEnabled"
label="Authentication Required"
formik={formik}
/>
<CosmosCheckbox
name="_SmartShield_Enabled"
label="Smart Shield Protection"
formik={formik}
/>
<CosmosCheckbox
name="RestrictToConstellation"
label="Restrict access to Constellation VPN"
formik={formik}
/>
<CosmosCollapse title={'Advanced Settings'}>
<Stack spacing={2}>
<CosmosCheckbox
name="HideFromDashboard"
label="Hide from Dashboard"
formik={formik}
/>
<CosmosFormDivider />
<Alert severity='info'>These settings are for advanced users only. Please do not change these unless you know what you are doing.</Alert>
<CosmosInputText
name="OverwriteHostHeader"
label="Overwrite Host Header (use this to chain resolve request from another server/ip)"
placeholder="Overwrite Host Header"
formik={formik}
/>
<Alert severity='warning'>
This setting will filter out all requests that do not come from the specified IPs.
This requires your setup to report the true IP of the client. By default it will, but some exotic setup (like installing docker/cosmos on Windows, or behind Cloudlfare)
will prevent Cosmos from knowing what is the client's real IP. If you used "Restrict to Constellation" above, Constellation IPs will always be allowed regardless of this setting.
</Alert>
<CosmosInputText
name="WhitelistInboundIPs"
label="Whitelist Inbound IPs and/or IP ranges (comma separated)"
placeholder="Whitelist Inbound IPs"
formik={formik}
/>
</Stack>
</CosmosCollapse>
</Grid>
</MainCard>
{submitButton && <MainCard ><Button
fullWidth
disableElevation
size="large"
type="submit"
variant="contained"
color="primary"
>
Save
</Button></MainCard>}
</Stack>
</form>
)}
</Formik>
</>}
</div>;
}
export default RouteManagement;

View file

@ -0,0 +1,79 @@
import * as React from 'react';
import MainCard from '../../../components/MainCard';
import RestartModal from '../users/restart';
import { Checkbox, Chip, Divider, FormControlLabel, Stack, useMediaQuery } from '@mui/material';
import HostChip from '../../../components/hostChip';
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
import { getFaviconURL } from '../../../utils/routes';
import * as API from '../../../api';
import { CheckOutlined, ClockCircleOutlined, ContainerOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, InfoCircleFilled, InfoCircleOutlined, LockOutlined, NodeExpandOutlined, SafetyCertificateOutlined, UpOutlined } from "@ant-design/icons";
import IsLoggedIn from '../../../isLoggedIn';
import { redirectToLocal } from '../../../utils/indexs';
import { CosmosCheckbox } from '../users/formShortcuts';
import { Field } from 'formik';
import MiniPlotComponent from '../../dashboard/components/mini-plot';
import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
import UploadButtons from '../../../components/fileUpload';
const info = {
backgroundColor: 'rgba(0, 0, 0, 0.1)',
padding: '10px',
borderRadius: '5px',
}
const RouteOverview = ({ routeConfig }) => {
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
const [confirmDelete, setConfirmDelete] = React.useState(false);
function deleteRoute(event) {
event.stopPropagation();
API.config.deleteRoute(routeConfig.Name).then(() => {
redirectToLocal('/cosmos-ui/config-url');
});
}
return <div style={{ maxWidth: '1000px', width: '100%'}}>
{routeConfig && <>
<MainCard name={routeConfig.Name} title={<div>
{routeConfig.Name} &nbsp;&nbsp;
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
{confirmDelete && (<Chip label={<CheckOutlined />} color="error" onClick={(event) => deleteRoute(event)}/>)}
</div>}>
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
<div>
<ImageWithPlaceholder className="loading-image" alt="" src={getFaviconURL(routeConfig)} width="128px" />
</div>
<Stack spacing={2} style={{ width: '100%' }}>
<strong><ContainerOutlined />Description</strong>
<div style={info}>{routeConfig.Description}</div>
<strong><NodeExpandOutlined /> URL</strong>
<div><HostChip route={routeConfig} /></div>
<strong><InfoCircleOutlined /> Target</strong>
<div><RouteMode route={routeConfig} /> <Chip label={routeConfig.Target} /></div>
<strong><SafetyCertificateOutlined/> Security</strong>
<div><RouteSecurity route={routeConfig} /></div>
<strong><DashboardOutlined/> Monitoring</strong>
<div>
<MiniPlotComponent agglo metrics={[
"cosmos.proxy.route.success." + routeConfig.Name,
"cosmos.proxy.route.error." + routeConfig.Name,
]} labels={{
["cosmos.proxy.route.error." + routeConfig.Name]: "Error",
["cosmos.proxy.route.success." + routeConfig.Name]: "Succ."
}}/>
<MiniPlotComponent agglo metrics={[
"cosmos.proxy.route.bytes." + routeConfig.Name,
"cosmos.proxy.route.time." + routeConfig.Name,
]} labels={{
["cosmos.proxy.route.bytes." + routeConfig.Name]: "Bytes",
["cosmos.proxy.route.time." + routeConfig.Name]: "Time"
}}/>
</div>
</Stack>
</Stack>
</MainCard>
</>}
</div>;
}
export default RouteOverview;

View file

@ -1,5 +1,5 @@
import * as React from 'react';
import isLoggedIn from '../../../isLoggedIn';
import IsLoggedIn from '../../../isLoggedIn';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik, Field } from 'formik';
@ -8,33 +8,41 @@ import {
Alert,
Button,
Checkbox,
Divider,
FormControlLabel,
Grid,
IconButton,
InputAdornment,
InputLabel,
Link,
OutlinedInput,
Stack,
Typography,
FormHelperText,
Collapse,
TextField,
MenuItem,
Skeleton,
} from '@mui/material';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import AnimateButton from '../../../components/@extended/AnimateButton';
import RestartModal from './restart';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
import { CosmosInputText, CosmosSelect } from './formShortcuts';
import { DeleteOutlined, SyncOutlined } from '@ant-design/icons';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputPassword, CosmosInputText, CosmosSelect } from './formShortcuts';
import CountrySelect from '../../../components/countrySelect';
import { DnsChallengeComp } from '../../../utils/dns-challenge-comp';
import UploadButtons from '../../../components/fileUpload';
import { SliderPicker
} from 'react-color';
import { LoadingButton } from '@mui/lab';
// TODO: Remove circular deps
import {SetPrimaryColor, SetSecondaryColor} from '../../../App';
import { useClientInfos } from '../../../utils/hooks';
import ConfirmModal from '../../../components/confirmModal';
import { DownloadFile } from '../../../api/downloadButton';
const ConfigManagement = () => {
isLoggedIn();
const [config, setConfig] = React.useState(null);
const [openModal, setOpenModal] = React.useState(false);
const [openResartModal, setOpenRestartModal] = React.useState(false);
const [uploadingBackground, setUploadingBackground] = React.useState(false);
const [saveLabel, setSaveLabel] = React.useState("Save");
const {role} = useClientInfos();
const isAdmin = role === "2";
function refresh() {
API.config.get().then((res) => {
@ -42,20 +50,49 @@ const ConfigManagement = () => {
});
}
function getRouteDomain(domain) {
let parts = domain.split('.');
return parts[parts.length - 2] + '.' + parts[parts.length - 1];
}
React.useEffect(() => {
refresh();
}, []);
return <div style={{maxWidth: '1000px', margin: ''}}>
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>Refresh</Button><br /><br />
<IsLoggedIn />
<Stack direction="row" spacing={2} style={{marginBottom: '15px'}}>
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>Refresh</Button>
{isAdmin && <Button variant="outlined" color="primary" startIcon={<SyncOutlined />} onClick={() => {
setOpenRestartModal(true);
}}>Restart Server</Button>}
<ConfirmModal variant="outlined" color="warning" startIcon={<DeleteOutlined />} callback={() => {
API.metrics.reset().then((res) => {
refresh();
});
}}
label={'Purge Metrics Dashboard'}
content={'Are you sure you want to purge all the metrics data from the dashboards?'} />
</Stack>
{config && <>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
<RestartModal openModal={openModal} setOpenModal={setOpenModal} config={config} />
<RestartModal openModal={openResartModal} setOpenModal={setOpenRestartModal} />
<Formik
initialValues={{
MongoDB: config.MongoDB,
LoggingLevel: config.LoggingLevel,
RequireMFA: config.RequireMFA,
GeoBlocking: config.BlockedCountries,
CountryBlacklistIsWhitelist: config.CountryBlacklistIsWhitelist,
AutoUpdate: config.AutoUpdate,
Hostname: config.HTTPConfig.Hostname,
GenerateMissingTLSCert: config.HTTPConfig.GenerateMissingTLSCert,
@ -63,276 +100,622 @@ const ConfigManagement = () => {
HTTPPort: config.HTTPConfig.HTTPPort,
HTTPSPort: config.HTTPConfig.HTTPSPort,
SSLEmail: config.HTTPConfig.SSLEmail,
UseWildcardCertificate: config.HTTPConfig.UseWildcardCertificate,
HTTPSCertificateMode: config.HTTPConfig.HTTPSCertificateMode,
DNSChallengeProvider: config.HTTPConfig.DNSChallengeProvider,
DNSChallengeConfig: config.HTTPConfig.DNSChallengeConfig,
ForceHTTPSCertificateRenewal: config.HTTPConfig.ForceHTTPSCertificateRenewal,
OverrideWildcardDomains: config.HTTPConfig.OverrideWildcardDomains,
UseForwardedFor: config.HTTPConfig.UseForwardedFor,
Email_Enabled: config.EmailConfig.Enabled,
Email_Host: config.EmailConfig.Host,
Email_Port: config.EmailConfig.Port,
Email_Username: config.EmailConfig.Username,
Email_Password: config.EmailConfig.Password,
Email_From: config.EmailConfig.From,
Email_UseTLS : config.EmailConfig.UseTLS,
Email_AllowInsecureTLS : config.EmailConfig.AllowInsecureTLS,
SkipPruneNetwork: config.DockerConfig.SkipPruneNetwork,
DefaultDataPath: config.DockerConfig.DefaultDataPath || "/usr",
Background: config && config.HomepageConfig && config.HomepageConfig.Background,
Expanded: config && config.HomepageConfig && config.HomepageConfig.Expanded,
PrimaryColor: config && config.ThemeConfig && config.ThemeConfig.PrimaryColor,
SecondaryColor: config && config.ThemeConfig && config.ThemeConfig.SecondaryColor,
MonitoringEnabled: !config.MonitoringDisabled,
}}
validationSchema={Yup.object().shape({
Hostname: Yup.string().max(255).required('Hostname is required'),
MongoDB: Yup.string().max(512),
LoggingLevel: Yup.string().max(255).required('Logging Level is required'),
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
let toSave = {
...config,
MongoDB: values.MongoDB,
LoggingLevel: values.LoggingLevel,
HTTPConfig: {
...config.HTTPConfig,
Hostname: values.Hostname,
GenerateMissingAuthCert: values.GenerateMissingAuthCert,
HTTPPort: values.HTTPPort,
HTTPSPort: values.HTTPSPort,
SSLEmail: values.SSLEmail,
HTTPSCertificateMode: values.HTTPSCertificateMode,
}
}
API.config.set(toSave).then((data) => {
if (data.status == 'error') {
setStatus({ success: false });
if (data.code == 'UL001') {
setErrors({ submit: 'Wrong nickname or password. Try again or try resetting your password' });
} else if (data.status == 'error') {
setErrors({ submit: 'Unexpected error. Try again later.' });
}
setSubmitting(false);
return;
} else {
setStatus({ success: true });
setSubmitting(false);
setOpenModal(true);
}
})
} catch (err) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
setSubmitting(true);
let toSave = {
...config,
MongoDB: values.MongoDB,
LoggingLevel: values.LoggingLevel,
RequireMFA: values.RequireMFA,
// AutoUpdate: values.AutoUpdate,
BlockedCountries: values.GeoBlocking,
CountryBlacklistIsWhitelist: values.CountryBlacklistIsWhitelist,
MonitoringDisabled: !values.MonitoringEnabled,
HTTPConfig: {
...config.HTTPConfig,
Hostname: values.Hostname,
GenerateMissingAuthCert: values.GenerateMissingAuthCert,
HTTPPort: values.HTTPPort,
HTTPSPort: values.HTTPSPort,
SSLEmail: values.SSLEmail,
UseWildcardCertificate: values.UseWildcardCertificate,
HTTPSCertificateMode: values.HTTPSCertificateMode,
DNSChallengeProvider: values.DNSChallengeProvider,
DNSChallengeConfig: values.DNSChallengeConfig,
ForceHTTPSCertificateRenewal: values.ForceHTTPSCertificateRenewal,
OverrideWildcardDomains: values.OverrideWildcardDomains.replace(/\s/g, ''),
UseForwardedFor: values.UseForwardedFor,
},
EmailConfig: {
...config.EmailConfig,
Enabled: values.Email_Enabled,
Host: values.Email_Host,
Port: values.Email_Port,
Username: values.Email_Username,
Password: values.Email_Password,
From: values.Email_From,
UseTLS: values.Email_UseTLS,
AllowInsecureTLS: values.Email_AllowInsecureTLS,
},
DockerConfig: {
...config.DockerConfig,
SkipPruneNetwork: values.SkipPruneNetwork,
DefaultDataPath: values.DefaultDataPath
},
HomepageConfig: {
...config.HomepageConfig,
Background: values.Background,
Expanded: values.Expanded
},
ThemeConfig: {
...config.ThemeConfig,
PrimaryColor: values.PrimaryColor,
SecondaryColor: values.SecondaryColor
},
}
return API.config.set(toSave).then((data) => {
setOpenModal(true);
setSaveLabel("Saved!");
setTimeout(() => {
setSaveLabel("Save");
}, 3000);
}).catch((err) => {
setOpenModal(true);
setSaveLabel("Error while saving, try again.");
setTimeout(() => {
setSaveLabel("Save");
}, 3000);
});
}}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<MainCard title="General">
<Grid container spacing={3}>
<Stack spacing={3}>
{isAdmin && <MainCard>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<Alert severity="info">This page allow you to edit the configuration file. Any Environment Variable overwritting configuration won't appear here.</Alert>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="MongoDB-login">MongoDB connection string. It is advised to use Environment variable to store this securely instead. (Optional)</InputLabel>
<OutlinedInput
id="MongoDB-login"
type="password"
value={formik.values.MongoDB}
name="MongoDB"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="MongoDB"
<LoadingButton
disableElevation
loading={formik.isSubmitting}
fullWidth
error={Boolean(formik.touched.MongoDB && formik.errors.MongoDB)}
/>
{formik.touched.MongoDB && formik.errors.MongoDB && (
<FormHelperText error id="standard-weight-helper-text-MongoDB-login">
{formik.errors.MongoDB}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="LoggingLevel-login">Level of logging (Default: INFO)</InputLabel>
<TextField
className="px-2 my-2"
variant="outlined"
name="LoggingLevel"
id="LoggingLevel"
select
value={formik.values.LoggingLevel}
onChange={formik.handleChange}
error={
formik.touched.LoggingLevel &&
Boolean(formik.errors.LoggingLevel)
}
helperText={
formik.touched.LoggingLevel && formik.errors.LoggingLevel
}
size="large"
type="submit"
variant="contained"
color="primary"
>
<MenuItem key={"DEBUG"} value={"DEBUG"}>
DEBUG
</MenuItem>
<MenuItem key={"INFO"} value={"INFO"}>
INFO
</MenuItem>
<MenuItem key={"WARNING"} value={"WARNING"}>
WARNING
</MenuItem>
<MenuItem key={"ERROR"} value={"ERROR"}>
ERROR
</MenuItem>
</TextField>
</Stack>
{saveLabel}
</LoadingButton>
</Grid>
</Grid>
</MainCard>
</MainCard>}
<br /><br />
{!isAdmin && <div>
<Alert severity="warning">As you are not an admin, you can't edit the configuration.
This page is only here for visibility.
</Alert>
</div>}
<MainCard title="HTTP">
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="Hostname-login">Hostname: This will be used to restrict access to your Cosmos Server (Default: 0.0.0.0)</InputLabel>
<OutlinedInput
id="Hostname-login"
type="text"
value={formik.values.Hostname}
name="Hostname"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="Hostname"
fullWidth
error={Boolean(formik.touched.Hostname && formik.errors.Hostname)}
/>
{formik.touched.Hostname && formik.errors.Hostname && (
<FormHelperText error id="standard-weight-helper-text-Hostname-login">
{formik.errors.Hostname}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="HTTPPort-login">HTTP Port (Default: 80)</InputLabel>
<OutlinedInput
id="HTTPPort-login"
type="text"
value={formik.values.HTTPPort}
name="HTTPPort"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="HTTPPort"
fullWidth
error={Boolean(formik.touched.HTTPPort && formik.errors.HTTPPort)}
/>
{formik.touched.HTTPPort && formik.errors.HTTPPort && (
<FormHelperText error id="standard-weight-helper-text-HTTPPort-login">
{formik.errors.HTTPPort}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="HTTPSPort-login">HTTPS Port (Default: 443)</InputLabel>
<OutlinedInput
id="HTTPSPort-login"
type="text"
value={formik.values.HTTPSPort}
name="HTTPSPort"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="HTTPSPort"
fullWidth
error={Boolean(formik.touched.HTTPSPort && formik.errors.HTTPSPort)}
/>
{formik.touched.HTTPSPort && formik.errors.HTTPSPort && (
<FormHelperText error id="standard-weight-helper-text-HTTPSPort-login">
{formik.errors.HTTPSPort}
</FormHelperText>
)}
</Stack>
</Grid>
</Grid>
</MainCard>
<br /><br />
<MainCard title="Security Certificates">
<MainCard title="General">
<Grid container spacing={3}>
<Grid item xs={12}>
<Alert severity="info">For security reasons, It is not possible to remotely change the Private keys of any certificates on your instance. It is advised to manually edit the config file, or better, use Environment Variables to store them.</Alert>
<Grid item xs={12}>
<Alert severity="info">This page allow you to edit the configuration file. Any Environment Variable overwritting configuration won't appear here.</Alert>
</Grid>
<CosmosCheckbox
label="Force Multi-Factor Authentication"
name="RequireMFA"
formik={formik}
helperText="Require MFA for all users"
/>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="MongoDB-login">MongoDB connection string. It is advised to use Environment variable to store this securely instead. (Optional)</InputLabel>
<OutlinedInput
id="MongoDB-login"
type="password"
autoComplete='new-password'
value={formik.values.MongoDB}
name="MongoDB"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="MongoDB"
fullWidth
error={Boolean(formik.touched.MongoDB && formik.errors.MongoDB)}
/>
{formik.touched.MongoDB && formik.errors.MongoDB && (
<FormHelperText error id="standard-weight-helper-text-MongoDB-login">
{formik.errors.MongoDB}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="LoggingLevel-login">Level of logging (Default: INFO)</InputLabel>
<TextField
className="px-2 my-2"
variant="outlined"
name="LoggingLevel"
id="LoggingLevel"
select
value={formik.values.LoggingLevel}
onChange={formik.handleChange}
error={
formik.touched.LoggingLevel &&
Boolean(formik.errors.LoggingLevel)
}
helperText={
formik.touched.LoggingLevel && formik.errors.LoggingLevel
}
>
<MenuItem key={"DEBUG"} value={"DEBUG"}>
DEBUG
</MenuItem>
<MenuItem key={"INFO"} value={"INFO"}>
INFO
</MenuItem>
<MenuItem key={"WARNING"} value={"WARNING"}>
WARNING
</MenuItem>
<MenuItem key={"ERROR"} value={"ERROR"}>
ERROR
</MenuItem>
</TextField>
</Stack>
</Grid>
<CosmosCheckbox
label="Monitoring Enabled"
name="MonitoringEnabled"
formik={formik}
/>
</Grid>
</MainCard>
<MainCard title="Appearance">
<Grid container spacing={3}>
<Grid item xs={12}>
{!uploadingBackground && formik.values.Background && <img src=
{formik.values.Background} alt="preview seems broken. Please re-upload."
width={285} />}
{uploadingBackground && <Skeleton variant="rectangular" width={285} height={140} />}
<Stack spacing={1} direction="row">
<UploadButtons
accept='.jpg, .png, .gif, .jpeg, .webp, .bmp, .avif, .tiff, .svg'
label="Upload Wallpaper"
OnChange={(e) => {
setUploadingBackground(true);
const file = e.target.files[0];
API.uploadImage(file, "background").then((data) => {
formik.setFieldValue('Background', data.data.path);
setUploadingBackground(false);
});
}}
/>
<CosmosSelect
name="HTTPSCertificateMode"
label="HTTPS Certificates"
formik={formik}
options={[
["LETSENCRYPT", "Automatically generate certificates using Let's Encrypt (Recommended)"],
["SELFSIGNED", "Locally self-sign certificates (unsecure)"],
["PROVIDED", "I have my own certificates"],
["DISABLED", "Do not use HTTPS (very unsecure)"],
]}
/>
{
formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
<CosmosInputText
name="SSLEmail"
label="Email address for Let's Encrypt"
<Button
variant="outlined"
onClick={() => {
formik.setFieldValue('Background', "");
}}
>
Reset Wallpaper
</Button>
<Button
variant="outlined"
onClick={() => {
formik.setFieldValue('PrimaryColor', "");
SetPrimaryColor("");
formik.setFieldValue('SecondaryColor', "");
SetSecondaryColor("");
}}
>
Reset Colors
</Button>
</Stack>
</Grid>
<Grid item xs={12}>
<CosmosCheckbox
label="Show Application Details on Homepage"
name="Expanded"
formik={formik}
/>
)
}
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel style={{marginBottom: '10px'}} htmlFor="PrimaryColor">Primary Color</InputLabel>
<SliderPicker
id="PrimaryColor"
color={formik.values.PrimaryColor}
onChange={color => {
let colorRGB = `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})`
formik.setFieldValue('PrimaryColor', colorRGB);
SetPrimaryColor(colorRGB);
}}
/>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel style={{marginBottom: '10px'}} htmlFor="SecondaryColor">Secondary Color</InputLabel>
<SliderPicker
id="SecondaryColor"
color={formik.values.SecondaryColor}
onChange={color => {
let colorRGB = `rgba(${color.rgb.r}, ${color.rgb.g}, ${color.rgb.b}, ${color.rgb.a})`
formik.setFieldValue('SecondaryColor', colorRGB);
SetSecondaryColor(colorRGB);
}}
/>
</Stack>
</Grid>
</Grid>
</MainCard>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Field
type="checkbox"
name="GenerateMissingAuthCert"
as={FormControlLabel}
control={<Checkbox size="large" />}
label="Generate missing Authentication Certificates automatically (Default: true)"
<MainCard title="HTTP">
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="Hostname-login">Hostname: This will be used to restrict access to your Cosmos Server (Your IP, or your domain name)</InputLabel>
<OutlinedInput
id="Hostname-login"
type="text"
value={formik.values.Hostname}
name="Hostname"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="Hostname"
fullWidth
error={Boolean(formik.touched.Hostname && formik.errors.Hostname)}
/>
{formik.touched.Hostname && formik.errors.Hostname && (
<FormHelperText error id="standard-weight-helper-text-Hostname-login">
{formik.errors.Hostname}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="HTTPPort-login">HTTP Port (Default: 80)</InputLabel>
<OutlinedInput
id="HTTPPort-login"
type="text"
value={formik.values.HTTPPort}
name="HTTPPort"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="HTTPPort"
fullWidth
error={Boolean(formik.touched.HTTPPort && formik.errors.HTTPPort)}
/>
{formik.touched.HTTPPort && formik.errors.HTTPPort && (
<FormHelperText error id="standard-weight-helper-text-HTTPPort-login">
{formik.errors.HTTPPort}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="HTTPSPort-login">HTTPS Port (Default: 443)</InputLabel>
<OutlinedInput
id="HTTPSPort-login"
type="text"
value={formik.values.HTTPSPort}
name="HTTPSPort"
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder="HTTPSPort"
fullWidth
error={Boolean(formik.touched.HTTPSPort && formik.errors.HTTPSPort)}
/>
{formik.touched.HTTPSPort && formik.errors.HTTPSPort && (
<FormHelperText error id="standard-weight-helper-text-HTTPSPort-login">
{formik.errors.HTTPSPort}
</FormHelperText>
)}
</Stack>
</Grid>
</Grid>
</MainCard>
<MainCard title="Emails - SMTP">
<Stack spacing={2}>
<Alert severity="info">This allow you to setup an SMTP server for Cosmos to send emails such as password reset emails and invites.</Alert>
<CosmosCheckbox
label="Enable SMTP"
name="Email_Enabled"
formik={formik}
helperText="Enable SMTP"
/>
{formik.values.Email_Enabled && (<>
<CosmosInputText
label="SMTP Host"
name="Email_Host"
formik={formik}
helperText="SMTP Host"
/>
</Stack>
</Grid>
<CosmosInputText
label="SMTP Port"
name="Email_Port"
formik={formik}
helperText="SMTP Port"
/>
<CosmosInputText
label="SMTP Username"
name="Email_Username"
formik={formik}
helperText="SMTP Username"
/>
<CosmosInputPassword
label="SMTP Password"
name="Email_Password"
autoComplete='new-password'
formik={formik}
helperText="SMTP Password"
noStrength
/>
<CosmosInputText
label="SMTP From"
name="Email_From"
formik={formik}
helperText="SMTP From"
/>
<CosmosCheckbox
label="SMTP Uses TLS"
name="Email_UseTLS"
formik={formik}
helperText="SMTP Uses TLS"
/>
{formik.values.Email_UseTLS && (
<CosmosCheckbox
label="Allow Insecure TLS"
name="Email_AllowInsecureTLS"
formik={formik}
helperText="Allow self-signed certificate"
/>
)}
</>)}
</Stack>
</MainCard>
<MainCard title="Docker">
<Stack spacing={2}>
<CosmosCheckbox
label="Skip Prune Network"
name="SkipPruneNetwork"
formik={formik}
/>
<CosmosInputText
label="Default data path for installs"
name="DefaultDataPath"
formik={formik}
placeholder={'/usr'}
/>
</Stack>
</MainCard>
<MainCard title="Security">
<Grid container spacing={3}>
{/* <CosmosCheckbox
label={"Read Client IP from X-Forwarded-For header (not recommended)"}
name="UseForwardedFor"
formik={formik}
/> */}
<CosmosFormDivider title='Geo-Blocking' />
<CosmosCheckbox
label={"Use list as whitelist instead of blacklist"}
name="CountryBlacklistIsWhitelist"
formik={formik}
/>
<Grid item xs={12}>
<InputLabel htmlFor="GeoBlocking">Geo-Blocking: (Those countries will be
{formik.values.CountryBlacklistIsWhitelist ? " allowed to access " : " blocked from accessing "}
your server)</InputLabel>
</Grid>
<CountrySelect name="GeoBlocking" label="Choose which countries you want to block or allow" formik={formik} />
<Grid item xs={12}>
<Button onClick={() => {
formik.setFieldValue("GeoBlocking", ["CN","RU","TR","BR","BD","IN","NP","PK","LK","VN","ID","IR","IQ","EG","AF","RO",])
formik.setFieldValue("CountryBlacklistIsWhitelist", false)
}} variant="outlined">Reset to default (most dangerous countries)</Button>
</Grid>
<CosmosFormDivider title='Encryption' />
<Grid item xs={12}>
<Alert severity="info">For security reasons, It is not possible to remotely change the Private keys of any certificates on your instance. It is advised to manually edit the config file, or better, use Environment Variables to store them.</Alert>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Field
type="checkbox"
name="GenerateMissingAuthCert"
as={FormControlLabel}
control={<Checkbox size="large" />}
label="Generate missing Authentication Certificates automatically (Default: true)"
/>
</Stack>
</Grid>
<CosmosSelect
name="HTTPSCertificateMode"
label="HTTPS Certificates"
formik={formik}
onChange={(e) => {
formik.setFieldValue("ForceHTTPSCertificateRenewal", true);
}}
options={[
["LETSENCRYPT", "Automatically generate certificates using Let's Encrypt (Recommended)"],
["SELFSIGNED", "Locally self-sign certificates (unsecure)"],
["PROVIDED", "I have my own certificates"],
["DISABLED", "Do not use HTTPS (very unsecure)"],
]}
/>
<CosmosCheckbox
label={"Use Wildcard Certificate for the root domain of " + formik.values.Hostname}
onChange={(e) => {
formik.setFieldValue("ForceHTTPSCertificateRenewal", true);
}}
name="UseWildcardCertificate"
formik={formik}
/>
{formik.values.UseWildcardCertificate && (
<CosmosInputText
name="OverrideWildcardDomains"
onChange={(e) => {
formik.setFieldValue("ForceHTTPSCertificateRenewal", true);
}}
label="(optional, only if you know what you are doing) Override Wildcard Domains (comma separated, need to add both wildcard AND root domain like in the placeholder)"
formik={formik}
placeholder={"example.com,*.example.com"}
/>
)}
{formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
<CosmosInputText
name="SSLEmail"
onChange={(e) => {
formik.setFieldValue("ForceHTTPSCertificateRenewal", true);
}}
label="Email address for Let's Encrypt"
formik={formik}
/>
)
}
{
formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
<DnsChallengeComp
onChange={(e) => {
formik.setFieldValue("ForceHTTPSCertificateRenewal", true);
}}
label="Pick a DNS provider (if you are using a DNS Challenge, otherwise leave empty)"
name="DNSChallengeProvider"
configName="DNSChallengeConfig"
formik={formik}
/>
)
}
<Grid item xs={12}>
<h4>Authentication Public Key</h4>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<pre className='code'>
{config.HTTPConfig.AuthPublicKey}
</pre>
</Stack>
</Grid>
<Grid item xs={12}>
<h4>Root HTTPS Public Key</h4>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<pre className='code'>
{config.HTTPConfig.TLSCert}
</pre>
</Stack>
</Grid>
<Grid item xs={12}>
<CosmosCheckbox
label={"Force HTTPS Certificate Renewal On Next Save"}
name="ForceHTTPSCertificateRenewal"
formik={formik}
/>
</Grid>
</Grid>
</MainCard>
{isAdmin && <MainCard>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<h4>Authentication Public Key</h4>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<pre className='code'>
{config.HTTPConfig.AuthPublicKey}
</pre>
</Stack>
<LoadingButton
disableElevation
loading={formik.isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
{saveLabel}
</LoadingButton>
</Grid>
<Grid item xs={12}>
<h4>Root HTTPS Public Key</h4>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<pre className='code'>
{config.HTTPConfig.TLSCert}
</pre>
</Stack>
</Grid>
</Grid>
</MainCard>
<br /><br />
<MainCard>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button
disableElevation
disabled={formik.isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
Save
</Button>
</AnimateButton>
</Grid>
</MainCard>
</MainCard>}
</Stack>
</form>
)}
</Formik>

View file

@ -33,17 +33,15 @@ import defaultport from '../../servapps/defaultport.json';
import * as API from '../../../api';
export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTargetChange}) {
export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetContainer, onTargetChange, label = "Container Name", name = "Target"}) {
const [open, setOpen] = React.useState(false);
const [containers, setContainers] = React.useState([]);
const [hasPublicPorts, setHasPublicPorts] = React.useState(false);
const [isOnBridge, setIsOnBridge] = React.useState(false);
const [options, setOptions] = React.useState(null);
const [portsOptions, setPortsOptions] = React.useState([]);
const [portsOptions, setPortsOptions] = React.useState(null);
const loading = options === null;
const name = "Target"
const label = "Container Name"
let targetResult = {
container: 'null',
port: "",
@ -78,35 +76,37 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
})
setPortsOptions(portsTemp)
targetResult.port = '80'
if(portsTemp.length > 0) {
// pick best default port
// set default to first port
targetResult.port = portsTemp[0]
// first, check if a manual override exists
let override = Object.keys(defaultport.overrides).find((key) => {
let keyMatch = new RegExp(key, "i");
return newContainer.Image.match(keyMatch) && portsTemp.includes(defaultport.overrides[key])
});
if(override) {
targetResult.port = defaultport.overrides[override]
} else {
// if not, check the default list of common ports
let priorityList = defaultport.priority;
priorityList.find((_portReg) => {
return portsTemp.find((portb) => {
let portReg = new RegExp(_portReg, "i");
if(portb.toString().match(portReg)) {
targetResult.port = portb
return true;
}
if(targetResult.port == '') {
targetResult.port = '80'
if(portsTemp.length > 0) {
// pick best default port
// set default to first port
targetResult.port = portsTemp[0]
// first, check if a manual override exists
let override = Object.keys(defaultport.overrides).find((key) => {
let keyMatch = new RegExp(key, "i");
return newContainer.Image.match(keyMatch) && portsTemp.includes(defaultport.overrides[key])
});
if(override) {
targetResult.port = defaultport.overrides[override]
} else {
// if not, check the default list of common ports
let priorityList = defaultport.priority;
priorityList.find((_portReg) => {
return portsTemp.find((portb) => {
let portReg = new RegExp(_portReg, "i");
if(portb.toString().match(portReg)) {
targetResult.port = portb
return true;
}
})
})
})
}
}
}
formik.setFieldValue(name, getTarget());
if(newContainer.NetworkSettings.Networks["bridge"]) {
@ -151,8 +151,9 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
if (active) {
setOptions([...names]);
}
if (targetResult.container !== 'null') {
postContainerChange(res.data.find((container) => container.Names[0] === targetResult.container))
postContainerChange(res.data.find((container) => container.Names[0] === targetResult.container) || targetResult.containerObject)
}
})();
@ -170,7 +171,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
const newTarget = formik.values[name];
React.useEffect(() => {
if(onTargetChange) {
onTargetChange(newTarget)
onTargetChange(newTarget, targetResult.container.replace("/", ""), targetResult)
}
}, [newTarget])
@ -216,62 +217,63 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
/>
)}
/>}
{!nameOnly && <>
<InputLabel htmlFor={name + "-port"}>Container Port</InputLabel>
<Autocomplete
className="px-2 my-2"
variant="outlined"
name={name + "-port"}
id={name + "-port"}
value={targetResult.port}
options={((portsOptions && portsOptions.length) ? portsOptions : [])}
placeholder='Select a port'
onBlur={(event) => {
targetResult.port = event.target.value || '';
formik.setFieldValue(name, getTarget())
}}
freeSolo
filterOptions={(x) => x} // disable filtering
getOptionLabel={(option) => '' + option}
isOptionEqualToValue={(option, value) => {
return ('' + option) === value
}}
onChange={(event, newValue) => {
targetResult.port = newValue || '';
formik.setFieldValue(name, getTarget())
}}
renderInput={(params) => <TextField {...params} />}
/>
{targetResult.port == '' && targetResult.port == 0 && <FormHelperText error id="standard-weight-helper-text-name-login">
Please select a port
</FormHelperText>}
{(portsOptions.length > 0) ? (<>
<InputLabel htmlFor={name + "-port"}>Container Port</InputLabel>
<Autocomplete
className="px-2 my-2"
variant="outlined"
name={name + "-port"}
id={name + "-port"}
value={targetResult.port}
options={portsOptions.map((option) => (option))}
placeholder='Select a port'
freeSolo
filterOptions={(x) => x} // disable filtering
getOptionLabel={(option) => '' + option}
isOptionEqualToValue={(option, value) => {
return ('' + option) === value
}}
onChange={(event, newValue) => {
targetResult.port = newValue
formik.setFieldValue(name, getTarget())
}}
renderInput={(params) => <TextField {...params} />}
/>
{targetResult.port == '' && targetResult.port == 0 && <FormHelperText error id="standard-weight-helper-text-name-login">
Please select a port
</FormHelperText>}
</>) : ''}
{(portsOptions.length > 0) ? (<>
<InputLabel htmlFor={name + "-protocol"}>Container Protocol (use HTTP if unsure)</InputLabel>
<TextField
type="text"
name={name + "-protocol"}
defaultValue={targetResult.protocol}
onChange={(event) => {
targetResult.protocol = event.target.value && event.target.value.toLowerCase()
formik.setFieldValue(name, getTarget())
}}
/>
</>) : ''}
<InputLabel htmlFor={name + "-protocol"}>Container Protocol (use HTTP if unsure)</InputLabel>
<TextField
type="text"
name={name + "-protocol"}
defaultValue={targetResult.protocol}
onChange={(event) => {
targetResult.protocol = event.target.value && event.target.value.toLowerCase()
formik.setFieldValue(name, getTarget())
}}
/>
<InputLabel htmlFor={name}>Result Target Preview</InputLabel>
<TextField
name={name}
placeholder={"This will be generated automatically"}
id={name}
value={formik.values[name]}
disabled={true}
/>
{formik.errors[name] && (
<FormHelperText error id="standard-weight-helper-text-name-login">
{formik.errors[name]}
</FormHelperText>
)}
<InputLabel htmlFor={name}>Result Target Preview</InputLabel>
<TextField
name={name}
placeholder={"This will be generated automatically"}
id={name}
value={formik.values[name]}
disabled={true}
/>
</>}
{formik.errors[name] && (
<FormHelperText error id="standard-weight-helper-text-name-login">
{formik.errors[name]}
</FormHelperText>
)}
</Stack>
</Grid>
);

View file

@ -27,23 +27,46 @@ import { strengthColor, strengthIndicator } from '../../../utils/password-streng
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
export const CosmosInputText = ({ name, style, multiline, type, placeholder, onChange, label, formik }) => {
export const getNestedValue = (values, path) => {
return path.split('.').reduce((current, key) => {
if (current && current[key] !== undefined) {
return current[key];
}
if (Array.isArray(current)) {
const index = parseInt(key, 10);
return current[index];
}
return undefined;
}, values);
};
export const CosmosInputText = ({ name, style, value, errors, multiline, type, placeholder, onChange, label, formik }) => {
return <Grid item xs={12}>
<Stack spacing={1} style={style}>
<InputLabel htmlFor={name}>{label}</InputLabel>
{label && <InputLabel htmlFor={name}>{label}</InputLabel>}
<OutlinedInput
id={name}
type={type ? type : 'text'}
value={formik.values[name]}
value={value || (formik && getNestedValue(formik.values, name))}
name={name}
multiline={multiline}
onBlur={formik.handleBlur}
onChange={formik.handleChange}
onBlur={(...ar) => {
return formik && formik.handleBlur(...ar);
}}
onChange={(...ar) => {
onChange && onChange(...ar);
return formik && formik.handleChange(...ar);
}}
placeholder={placeholder}
fullWidth
error={Boolean(formik.touched[name] && formik.errors[name])}
error={Boolean(formik && formik.touched[name] && formik.errors[name])}
/>
{formik.touched[name] && formik.errors[name] && (
{formik && formik.touched[name] && formik.errors[name] && (
<FormHelperText error id="standard-weight-helper-text-name-login">
{formik.errors[name]}
</FormHelperText>
)}
{errors && (
<FormHelperText error id="standard-weight-helper-text-name-login">
{formik.errors[name]}
</FormHelperText>
@ -52,7 +75,7 @@ export const CosmosInputText = ({ name, style, multiline, type, placeholder, onC
</Grid>
}
export const CosmosInputPassword = ({ name, type, placeholder, onChange, label, formik }) => {
export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoComplete, onChange, label, formik }) => {
const [level, setLevel] = React.useState();
const [showPassword, setShowPassword] = React.useState(false);
const handleClickShowPassword = () => {
@ -78,8 +101,9 @@ export const CosmosInputPassword = ({ name, type, placeholder, onChange, label,
<OutlinedInput
id={name}
type={showPassword ? 'text' : 'password'}
value={formik.values[name]}
value={getNestedValue(formik.values, name)}
name={name}
autoComplete={autoComplete}
onBlur={formik.handleBlur}
onChange={(e) => {
changePassword(e.target.value);
@ -108,7 +132,7 @@ export const CosmosInputPassword = ({ name, type, placeholder, onChange, label,
</FormHelperText>
)}
<FormControl fullWidth sx={{ mt: 2 }}>
{!noStrength && <FormControl fullWidth sx={{ mt: 2 }}>
<Grid container spacing={2} alignItems="center">
<Grid item>
<Box sx={{ bgcolor: level?.color, width: 85, height: 8, borderRadius: '7px' }} />
@ -119,12 +143,12 @@ export const CosmosInputPassword = ({ name, type, placeholder, onChange, label,
</Typography>
</Grid>
</Grid>
</FormControl>
</FormControl>}
</Stack>
</Grid>
}
export const CosmosSelect = ({ name, label, formik, disabled, options }) => {
export const CosmosSelect = ({ name, onChange, label, formik, disabled, options, style }) => {
return <Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor={name}>{label}</InputLabel>
@ -135,8 +159,11 @@ export const CosmosSelect = ({ name, label, formik, disabled, options }) => {
id={name}
disabled={disabled}
select
value={formik.values[name]}
onChange={formik.handleChange}
value={getNestedValue(formik.values, name)}
onChange={(...ar) => {
onChange && onChange(...ar);
formik.handleChange(...ar);
}}
error={
formik.touched[name] &&
Boolean(formik.errors[name])
@ -144,6 +171,7 @@ export const CosmosSelect = ({ name, label, formik, disabled, options }) => {
helperText={
formik.touched[name] && formik.errors[name]
}
style={style}
>
{options.map((option) => (
<MenuItem key={option[0]} value={option[0]}>
@ -185,8 +213,9 @@ export const CosmosCollapse = ({ children, title }) => {
aria-controls="panel1a-content"
id="panel1a-header"
>
<Typography variant="h6">
{title}</Typography>
<Typography variant="h6" style={{width: '100%', marginRight: '20px'}}>
{title}
</Typography>
</AccordionSummary>
<AccordionDetails>
{children}
@ -199,7 +228,7 @@ export const CosmosCollapse = ({ children, title }) => {
export function CosmosFormDivider({title}) {
return <Grid item xs={12}>
<Divider>
<Chip label={title} />
{title && <Chip label={title} />}
</Divider>
</Grid>
}

View file

@ -1,9 +1,10 @@
import * as React from 'react';
import isLoggedIn from '../../../isLoggedIn';
import IsLoggedIn from '../../../isLoggedIn';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik, Field } from 'formik';
import * as Yup from 'yup';
import { useTheme } from '@mui/material/styles';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
import {
Alert,
@ -23,32 +24,58 @@ import {
Collapse,
TextField,
MenuItem,
Chip,
CircularProgress,
} from '@mui/material';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import AnimateButton from '../../../components/@extended/AnimateButton';
import RestartModal from './restart';
import RouteManagement, {ValidateRoute} from './routeman';
import RouteManagement from '../routes/routeman';
import { map } from 'lodash';
import { getFaviconURL, sanitizeRoute, ValidateRoute } from '../../../utils/routes';
import PrettyTableView from '../../../components/tableView/prettyTableView';
import HostChip from '../../../components/hostChip';
import {RouteActions, RouteMode, RouteSecurity} from '../../../components/routeComponents';
import { useNavigate } from 'react-router';
import NewRouteCreate from '../routes/newRoute';
import LazyLoad from 'react-lazyload';
import MiniPlotComponent from '../../dashboard/components/mini-plot';
import ImageWithPlaceholder from '../../../components/imageWithPlaceholder';
const stickyButton = {
position: 'fixed',
bottom: '20px',
width: '100%',
maxWidth: '1000px',
// left: '20px',
// right: '20px',
width: '300px',
boxShadow: '0px 0px 10px 0px rgba(0,0,0,0.50)',
right: '20px',
}
function shorten(test) {
if (test.length > 75) {
return test.substring(0, 75) + '...';
}
return test;
}
const ProxyManagement = () => {
isLoggedIn();
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const [config, setConfig] = React.useState(null);
const [openModal, setOpenModal] = React.useState(false);
const [error, setError] = React.useState(null);
const [submitErrors, setSubmitErrors] = React.useState([]);
const [needSave, setNeedSave] = React.useState(false);
const [openNewModal, setOpenNewModal] = React.useState(false);
const [isLoading, setIsLoading] = React.useState(false);
function setRouteEnabled(key) {
return (event) => {
routes[key].Disabled = !event.target.checked;
updateRoutes(routes);
setNeedSave(true);
}
}
function updateRoutes(routes) {
let con = {
@ -79,7 +106,8 @@ const ProxyManagement = () => {
});
}
function up(key) {
function up(event, key) {
event.stopPropagation();
if (key > 0) {
let tmp = routes[key];
routes[key] = routes[key-1];
@ -87,15 +115,19 @@ const ProxyManagement = () => {
updateRoutes(routes);
setNeedSave(true);
}
return false;
}
function deleteRoute(key) {
function deleteRoute(event, key) {
event.stopPropagation();
routes.splice(key, 1);
updateRoutes(routes);
setNeedSave(true);
return false;
}
function down(key) {
function down(event, key) {
event.stopPropagation();
if (key < routes.length - 1) {
let tmp = routes[key];
routes[key] = routes[key+1];
@ -103,61 +135,94 @@ const ProxyManagement = () => {
updateRoutes(routes);
setNeedSave(true);
}
return false;
}
React.useEffect(() => {
refresh();
}, []);
const testRoute = (route) => {
try {
ValidateRoute.validateSync(route);
} catch (e) {
return e.errors;
}
}
let routes = config && (config.HTTPConfig.ProxyConfig.Routes || []);
return <div style={{ maxWidth: '1000px', margin: '' }}>
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>Refresh</Button>&nbsp;&nbsp;
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
routes.unshift({
Name: 'New URL',
Description: 'New URL',
Mode: "SERVAPP",
UseHost: false,
Host: '',
UsePathPrefix: false,
PathPrefix: '',
Timeout: 30000,
ThrottlePerMinute: 0,
CORSOrigin: '',
StripPathPrefix: false,
AuthEnabled: false,
});
updateRoutes(routes);
setNeedSave(true);
}}>Create</Button>
<br /><br />
{config && <>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
{routes && routes.map((route,key) => (<>
<RouteManagement key={route.Name} routeConfig={route}
setRouteConfig={(newRoute) => {
routes[key] = newRoute;
setNeedSave(true);
}}
up={() => up(key)}
down={() => down(key)}
deleteRoute={() => deleteRoute(key)}
/>
<br /><br />
</>))}
return <div style={{ }}>
<IsLoggedIn />
<Stack direction="row" spacing={1} style={{ marginBottom: '20px' }}>
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>Refresh</Button>&nbsp;&nbsp;
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
setOpenNewModal(true);
}}>Create</Button>
</Stack>
{config && <>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} config={config} />
<NewRouteCreate openNewModal={openNewModal} setOpenNewModal={setOpenNewModal} config={config}/>
{routes && <PrettyTableView
data={routes}
getKey={(r) => r.Name + r.Target + r.Mode}
linkTo={(r) => '/cosmos-ui/config-url/' + r.Name}
columns={[
{
title: '',
field: (r) => <LazyLoad width={"64px"} height={"64px"}>
<ImageWithPlaceholder className="loading-image" alt="" src={getFaviconURL(r)} width="64px" height="64px"/>
</LazyLoad>,
style: {
textAlign: 'center',
},
},
{
title: 'Enabled',
clickable:true,
field: (r, k) => <Checkbox disabled={isLoading} size='large' color={!r.Disabled ? 'success' : 'default'}
onChange={setRouteEnabled(k)}
checked={!r.Disabled}
/>,
},
{ title: 'URL',
search: (r) => r.Name + ' ' + r.Description,
style: {
textDecoration: 'inherit',
},
underline: true,
field: (r) => <>
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize:'125%', color: isDark ? theme.palette.primary.light : theme.palette.primary.dark}}>{r.Name}</div><br/>
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize: '90%', opacity: '90%'}}>{r.Description}</div>
</>
},
{ title: 'Network', screenMin: 'lg', clickable:false, field: (r) =>
<div style={{width: '400px', marginLeft: '-200px', marginBottom: '10px'}}>
<MiniPlotComponent metrics={[
"cosmos.proxy.route.bytes." + r.Name,
"cosmos.proxy.route.time." + r.Name,
]} noLabels noBackground/>
</div>
},
{ title: 'Origin', screenMin: 'md', clickable:true, search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => <HostChip route={r} /> },
{ title: 'Target', screenMin: 'md', search: (r) => r.Target, field: (r) => <><RouteMode route={r} /> <Chip label={r.Target} /></> },
{ title: 'Security', screenMin: 'lg', field: (r) => <RouteSecurity route={r} />,
style: {minWidth: '70px'} },
{ title: '', clickable:true, field: (r, k) => <RouteActions
route={r}
routeKey={routes.indexOf(r)}
up={(event) => up(event, routes.indexOf(r))}
down={(event) => down(event, routes.indexOf(r))}
deleteRoute={(event) => deleteRoute(event, routes.indexOf(r))}
/>,
style: {
textAlign: 'right',
}
},
]}
/>}
{
!routes && <div style={{textAlign: 'center'}}>
<CircularProgress />
</div>
}
{routes && needSave && <>
<div>
<br /><br /><br /><br />
@ -180,20 +245,6 @@ const ProxyManagement = () => {
disableElevation
fullWidth
onClick={() => {
if(routes.some((route, key) => {
let errors = testRoute(route);
if (errors && errors.length > 0) {
errors = errors.map((err) => {
return `${route.Name}: ${err}`;
});
setSubmitErrors(errors);
return true;
}
})) {
return;
} else {
setSubmitErrors([]);
}
API.config.set(cleanRoutes(updateRoutes(routes))).then(() => {
setNeedSave(false);
setOpenModal(true);
@ -207,7 +258,7 @@ const ProxyManagement = () => {
variant="contained"
color="primary"
>
Save
Save Changes
</Button>
</AnimateButton>
</Stack>

View file

@ -1,6 +1,6 @@
// material-ui
import * as React from 'react';
import { Button, Typography } from '@mui/material';
import { Alert, Button, Stack, Typography } from '@mui/material';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
@ -20,33 +20,93 @@ import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import isLoggedIn from '../../../isLoggedIn';
import IsLoggedIn from '../../../isLoggedIn';
import { useEffect, useState } from 'react';
import { isDomain } from '../../../utils/indexs';
const RestartModal = ({openModal, setOpenModal}) => {
return <>
function checkIsOnline() {
API.isOnline().then((res) => {
window.location.reload();
}).catch((err) => {
setTimeout(() => {
checkIsOnline();
}, 1000);
});
}
const RestartModal = ({openModal, setOpenModal, config, newRoute }) => {
const [isRestarting, setIsRestarting] = useState(false);
const [warn, setWarn] = useState(false);
const needsRefresh = config && (config.HTTPConfig.HTTPSCertificateMode == "SELFSIGNED" ||
!isDomain(config.HTTPConfig.Hostname))
const isNotDomain = config && !isDomain(config.HTTPConfig.Hostname);
let newRouteWarning = config && (config.HTTPConfig.HTTPSCertificateMode == "LETSENCRYPT" && newRoute &&
(!config.HTTPConfig.DNSChallengeProvider || !config.HTTPConfig.UseWildcardCertificate))
return config ? (<>
{needsRefresh && <>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>Refresh Page</DialogTitle>
<DialogContent>
<DialogContentText>
You need to refresh the page because you are using a self-signed certificate, in case you have to accept any new certificates. To avoid it in the future, please use Let's Encrypt. {isNotDomain && 'You are also not using a domain name, the server might go offline for a few seconds to remap your docker ports.'}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => {
window.location.reload(true);
}}>Refresh</Button>
</DialogActions>
</Dialog>
</>}
{newRouteWarning && <>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>Certificate Renewal</DialogTitle>
<DialogContent>
<DialogContentText>
You are using Let's Encrypt but you are not using the DNS Challenge with a wildcard certificate. This means the server has to renew the certificate everytime you add a new hostname, causing a few seconds of downtime. To avoid it in the future, please refer to <a target="_blank" rel="noopener noreferrer" href="https://cosmos-cloud.io/doc/9%20Other%20Setups/#dns-challenge-and-wildcard-certificates">this link to the documentation</a>.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => {
setOpenModal(false);
}}>OK</Button>
</DialogActions>
</Dialog>
</>}
</>)
:(<>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>Restart Server</DialogTitle>
<DialogTitle>{!isRestarting ? 'Restart Server?' : 'Restarting Server...'}</DialogTitle>
<DialogContent>
<DialogContentText>
A restart is required to apply changes. Do you want to restart?
{warn && <div>
<Alert severity="warning" icon={<WarningOutlined />}>
The server is taking longer than expected to restart.<br />Consider troubleshouting the logs. If you use a self-signed certificate, you might have to refresh and re-accept it.
</Alert>
</div>}
{isRestarting ?
<div style={{textAlign: 'center', padding: '20px'}}>
<CircularProgress />
</div>
: 'Do you want to restart your server?'}
</DialogContentText>
</DialogContent>
<DialogActions>
{!isRestarting && <DialogActions>
<Button onClick={() => setOpenModal(false)}>Later</Button>
<Button onClick={() => {
setIsRestarting(true);
API.config.restart()
.then(() => {
refresh();
setOpenModal(false);
setTimeout(() => {
window.location.reload();
}, 2000)
})
setTimeout(() => {
checkIsOnline();
}, 1500)
setTimeout(() => {
setWarn(true);
}, 20000)
}}>Restart</Button>
</DialogActions>
</DialogActions>}
</Dialog>
</>;
</>);
};
export default RestartModal;

Some files were not shown because too many files have changed in this diff Show more