Compare commits

..

375 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
254 changed files with 33505 additions and 10858 deletions

View file

@ -2,11 +2,15 @@ version: 2
jobs:
build:
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 +53,24 @@ jobs:
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
@ -122,12 +144,6 @@ workflows:
build-all:
jobs:
- build:
filters:
branches:
only:
- master
- unstable
- buildarm:
filters:
branches:
only:

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"

8
.gitignore vendored
View file

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

263
LICENCE
View file

@ -1,11 +1,256 @@
All Rights Reserved
Software: Cosmos-Server
Copyright (c) 2023
License: Apache 2.0 with Commons Clause and Anti Tampering Clause
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
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
banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 KiB

View file

@ -1,12 +1,24 @@
#!/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
@ -15,4 +27,6 @@ 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,16 +1,322 @@
## 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 - 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
- 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
@ -65,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,23 +4,81 @@ 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('');
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}
@ -35,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

@ -1,4 +1,3 @@
import { resolve } from 'eslint-import-resolver-typescript';
import configDemo from './demo.config.json';
interface Route {

View file

@ -34,12 +34,14 @@ function restart() {
}
function canSendEmail() {
return wrap(fetch('/cosmos/api/can-send-email', {
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> {
@ -81,6 +83,15 @@ 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,
@ -92,4 +103,5 @@ export {
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

@ -21,7 +21,7 @@
"Name": "Jellyfin",
"Description": "Expose Jellyfin to the internet",
"UseHost": true,
"Host": "play.my-server.com",
"Host": "play.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 14400000,
@ -50,7 +50,7 @@
"Name": "Documents Folder",
"Description": "Share my Documents",
"UseHost": true,
"Host": "doc.my-server.com",
"Host": "documents.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 14400000,
@ -76,10 +76,10 @@
"BlockAPIAbuse": false
},
{
"Name": "navidrome",
"Name": "Navidrome",
"Description": "Expose navidrome to the internet",
"UseHost": true,
"Host": "music.my-server.com",
"Host": "navidrome.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 0,
@ -108,7 +108,7 @@
"Name": "Plex",
"Description": "Expose Plex to the internet",
"UseHost": true,
"Host": "plex.my-server.com",
"Host": "plex.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 0,
@ -137,7 +137,7 @@
"Name": "Radarr",
"Description": "Expose Radarr to the internet",
"UseHost": true,
"Host": "dl-movies.my-server.com",
"Host": "radarr.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 30000,
@ -166,7 +166,7 @@
"Name": "Sonarr",
"Description": "Expose Sonarr to the internet",
"UseHost": true,
"Host": "dl-series.my-server.com",
"Host": "sonarr.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 30000,
@ -195,7 +195,7 @@
"Name": "nzbget",
"Description": "Expose nzbget to the internet",
"UseHost": true,
"Host": "dl.my-server.com",
"Host": "nzbget.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 999999999,
@ -224,7 +224,7 @@
"Name": "photoprism",
"Description": "Expose photoprism to the internet",
"UseHost": true,
"Host": "photos.my-server.com",
"Host": "photoprism.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 45000,
@ -253,7 +253,7 @@
"Name": "Owncloud",
"Description": "Expose Owncloud to the internet",
"UseHost": true,
"Host": "cloud.my-server.com",
"Host": "owncloud.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 0,
@ -282,7 +282,7 @@
"Name": "Lidarr",
"Description": "Expose Lidarr to the internet",
"UseHost": true,
"Host": "dl-music.my-server.com",
"Host": "lidarr.xxxxxxxxxxxxx",
"UsePathPrefix": false,
"PathPrefix": "",
"Timeout": 30000,
@ -309,9 +309,86 @@
}
]
},
"Hostname": "my-server.com",
"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",
@ -324,6 +401,23 @@
"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",
@ -344,7 +438,78 @@
],
"ServerCountry": "",
"RequireMFA": false,
"AutoUpdate": 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"
}

View file

@ -1,4 +1,8 @@
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) => {
@ -6,6 +10,12 @@ function list() {
});
}
function get(containerName) {
return new Promise((resolve, reject) => {
resolve(configDemoCont)
});
}
function secure(id, res) {
return new Promise((resolve, reject) => {
resolve({
@ -24,15 +34,197 @@ const newDB = () => {
const manageContainer = () => {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
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
manageContainer,
volumeList,
volumeDelete,
networkList,
networkDelete,
getContainerLogs,
updateContainer,
listContainerNetworks,
createNetwork,
attachNetwork,
detachNetwork,
createVolume,
attachTerminal,
createTerminal,
createService,
pullImage,
autoUpdate,
updateContainerImage,
};

View file

@ -157,6 +157,174 @@ function createVolume(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,
@ -174,4 +342,10 @@ export {
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

@ -3,13 +3,29 @@ 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
"newVersionAvailable": false,
"resources": {},
"theme": {
"PrimaryColor": "rgba(191, 100, 64, 1)",
"SecondaryColor": ""
}
},
"status": "OK"
});
@ -36,4 +52,38 @@ export const newInstall = (req) => {
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

@ -2,22 +2,74 @@ 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';
let 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";
});
}
let isOnline = () => {
@ -42,31 +94,144 @@ let isOnline = () => {
});
}
let newInstall = (req) => {
return wrap(fetch('/cosmos/api/newInstall', {
method: 'POST',
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'
},
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;
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 {
@ -74,7 +239,13 @@ export {
users,
config,
docker,
market,
constellation,
getStatus,
newInstall,
isOnline
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

@ -116,6 +116,23 @@ function resetPassword(values) {
});
}
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,
@ -128,4 +145,6 @@ export {
check2FA,
reset2FA,
resetPassword,
getNotifs,
readNotifs
};

View file

@ -110,6 +110,24 @@ function resetPassword(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,
@ -122,4 +140,6 @@ export {
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,20 +1,32 @@
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;
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 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,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,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

@ -36,7 +36,7 @@ const HostChip = ({route, settings, style}) => {
window.open(window.location.origin + route.PathPrefix, '_blank');
}}
onDelete={settings ? () => {
window.open('/ui/config-url/'+route.Name, '_blank');
window.open('/cosmos-ui/config-url/'+route.Name, '_blank');
} : null}
deleteIcon={settings ? <SettingOutlined /> : null}
/>

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

@ -56,7 +56,6 @@ const LogLine = ({ message, docker, isMobile }) => {
if(docker) {
let parts = html.match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+Z/)
if(!parts) {
console.error('Could not parse log line', html)
return <div dangerouslySetInnerHTML={{ __html: html }} />;
}
let restString = html.replace(parts[0], '')

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

@ -1,7 +1,7 @@
import { Button, useMediaQuery, IconButton } from "@mui/material";
const ResponsiveButton = ({ children, startIcon, size, style, ...props }) => {
const ResponsiveButton = ({ children, startIcon, endIcon, size, style, ...props }) => {
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
let newStyle = style || {};
if (isMobile) {
@ -10,8 +10,18 @@ const ResponsiveButton = ({ children, startIcon, size, style, ...props }) => {
}
return (
<Button className="responsive-button" size={isMobile ? 'large' : size} startIcon={isMobile ? null : startIcon} {...props} style={newStyle}>
{isMobile ? startIcon : children}
<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>
);
}

View file

@ -45,7 +45,7 @@ export const RouteMode = ({route}) => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
let c = routeImages[route.Mode.toUpperCase()];
return <>
return c ? <>
<Chip
icon={<span>{c.icon}</span>}
label={c.label}
@ -56,7 +56,7 @@ export const RouteMode = ({route}) => {
alignItems: "right",
}}
></Chip>
</>
</> : <></>;
}
export const RouteSecurity = ({route}) => {

View file

@ -37,21 +37,26 @@ const a11yProps = (index) => {
};
};
const PrettyTabbedView = ({ tabs, isLoading }) => {
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 ? (
{(isMobile) ? (
<Select value={value} onChange={handleSelectChange} sx={{ minWidth: 120, marginBottom: '15px' }}>
{tabs.map((tab, index) => (
<MenuItem key={index} value={index}>
@ -70,7 +75,7 @@ const PrettyTabbedView = ({ tabs, isLoading }) => {
{tabs.map((tab, index) => (
<Tab
style={{fontWeight: !tab.children ? '1000' : '', }}
disabled={!tab.children} key={index}
disabled={tab.disabled || !tab.children} key={index}
label={tab.title} {...a11yProps(index)}
/>
))}

View file

@ -6,12 +6,12 @@ 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 { Input, InputAdornment, Stack, TextField, useMediaQuery } from '@mui/material';
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 = ({ getKey, data, columns, onRowClick, linkTo, buttons, fullWidth }) => {
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';
@ -43,7 +43,17 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo, buttons, f
/>
{buttons}
</Stack>
<TableContainer style={{width: fullWidth ? '100%': '', background: isDark ? '#252b32' : '', borderTop: '3px solid ' + theme.palette.primary.main}} component={Paper}>
{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>
@ -64,6 +74,10 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo, buttons, f
})
return found;
})
.sort((a, b) => {
if (!sort) return 0;
return sort(a, b);
})
.map((row, key) => (
<TableRow
key={getKey(row)}
@ -98,7 +112,7 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo, buttons, f
))}
</TableBody>
</Table>
</TableContainer>
</TableContainer>}
</Stack>
)
}

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,6 +21,58 @@
}
@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;
}
@ -69,4 +121,31 @@
.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,19 +1,22 @@
import * as API from './api';
import { useEffect } from 'react';
import { redirectToLocal } from './utils/indexs';
const IsLoggedIn = () => useEffect(() => {
console.log("CHECK LOGIN")
const urlSearch = encodeURIComponent(window.location.search);
const redirectToURL = (window.location.pathname + urlSearch);
API.auth.me().then((data) => {
if(data.status != 'OK') {
if(data.status == 'NEW_INSTALL') {
window.location.href = '/ui/newInstall';
redirectToLocal('/cosmos-ui/newInstall');
} else if (data.status == 'error' && data.code == "HTTP004") {
window.location.href = '/ui/login';
redirectToLocal('/cosmos-ui/login?redirect=' + redirectToURL);
} else if (data.status == 'error' && data.code == "HTTP006") {
window.location.href = '/ui/loginmfa';
redirectToLocal('/cosmos-ui/loginmfa?redirect=' + redirectToURL);
} else if (data.status == 'error' && data.code == "HTTP007") {
window.location.href = '/ui/newmfa';
redirectToLocal('/cosmos-ui/newmfa?redirect=' + redirectToURL);
}
}
})

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

@ -1,5 +1,5 @@
// assets
import { HomeOutlined, AppstoreOutlined, DashboardOutlined } from '@ant-design/icons';
import { HomeOutlined, AppstoreOutlined, DashboardOutlined, AppstoreAddOutlined } from '@ant-design/icons';
// icons
const icons = {
@ -17,16 +17,25 @@ const dashboard = {
id: 'home',
title: 'Home',
type: 'item',
url: '/ui/',
url: '/cosmos-ui/',
icon: icons.HomeOutlined,
breadcrumbs: false
},
{
id: 'dashboard',
title: 'Dashboard',
title: 'Monitoring',
type: 'item',
url: '/ui/dashboard',
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, AppstoreOutlined} 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 = {
@ -19,28 +19,46 @@ const pages = {
id: 'servapps',
title: 'ServApps',
type: 'item',
url: '/ui/servapps',
icon: AppstoreOutlined
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: 'constellation',
title: 'Constellation',
type: 'item',
url: '/cosmos-ui/constellation',
icon: () => <img height="28px" width="28px" style={{marginLeft: "-6px"}} src={ConstellationIcon} />,
},
{
id: 'users',
title: 'Users',
type: 'item',
url: '/ui/config-users',
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

@ -31,6 +31,7 @@ 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 ||============================ //
@ -53,14 +54,14 @@ const AuthLogin = () => {
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');
}
});
@ -103,7 +104,7 @@ const AuthLogin = () => {
return API.auth.login(values).then((data) => {
setStatus({ success: true });
setSubmitting(false);
window.location.href = redirectTo;
redirectToLocal(redirectToURL);
}).catch((err) => {
setStatus({ success: false });
if(err.code == 'UL001') {
@ -190,7 +191,7 @@ const AuthLogin = () => {
}
label={<Typography variant="h6">Keep me sign in</Typography>}
/>*/}
{showResetPassword && <Link variant="h6" component={RouterLink} to="/ui/forgot-password" color="primary">
{showResetPassword && <Link variant="h6" component={RouterLink} to="/cosmos-ui/forgot-password" color="primary">
Forgot Your Password?
</Link>}
{!showResetPassword && <Typography variant="h6">

View file

@ -33,6 +33,7 @@ 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 ||============================ //
@ -73,8 +74,8 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
.max(255)
.required('Password is required')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{9,})/,
'Must Contain 9 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 }) => {
@ -85,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 });

View file

@ -109,7 +109,7 @@ const ForgotPassword = () => {
variant="contained"
color="primary"
component={Link}
to="/ui/login"
to="/cosmos-ui/login"
>
Back to login
</Button>

View file

@ -32,17 +32,18 @@ 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 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');
}
});
});
@ -56,7 +57,7 @@ const MFALoginForm = () => {
})}
onSubmit={(values, { setSubmitting, setStatus, setErrors }) => {
API.users.check2FA(values.token).then((data) => {
window.location.href = redirectTo;
redirectToLocal(redirectToURL);
}).catch((error) => {
console.log(error)
setStatus({ success: false });
@ -76,6 +77,7 @@ const MFALoginForm = () => {
{...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}>
@ -149,7 +151,7 @@ const MFASetup = () => {
<MFALoginForm />
</Grid>
<Grid item xs={12}>
<Link to="/ui/logout">
<Link to="/cosmos-ui/logout">
<Typography variant="h5">Logout</Typography>
</Link>
</Grid>

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

@ -8,6 +8,8 @@ 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();
@ -53,17 +55,23 @@ const RouteConfigPage = () => {
submitButton
routeConfig={currentRoute}
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
config={config}
/>
},
{
title: 'Security',
children: <RouteSecurity
routeConfig={currentRoute}
config={config}
/>
},
{
title: 'Permissions',
children: <div>WIP</div>
title: 'Monitoring',
children: <RouteMetrics routeName={routeName} />
},
{
title: 'Events',
children: <EventExplorerStandalone initLevel='info' initSearch={`{"object":"route@${routeName}"}`}/>
},
]}/>}

View file

@ -27,8 +27,10 @@ const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => {
});
}
const routes = config.HTTPConfig.ProxyConfig.Routes || [];
return <>
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} config={config} newRoute />
<Dialog open={openNewModal} onClose={() => setOpenNewModal(false)}>
<DialogTitle>New URL</DialogTitle>
{openNewModal && <>
@ -56,7 +58,7 @@ const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => {
Enabled: true,
}
}}
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
routeNames={routes.map((r) => r.Name)}
setRouteConfig={(_newRoute) => {
setNewRoute(sanitizeRoute(_newRoute));
}}
@ -64,6 +66,7 @@ const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => {
down={() => {}}
deleteRoute={() => {}}
noControls
config={config}
/>
</div>
</Stack>

View file

@ -14,11 +14,11 @@ import RestartModal from '../users/restart';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
import { snackit } from '../../../api/wrap';
const RouteSecurity = ({ routeConfig }) => {
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} />
<RestartModal openModal={openModal} setOpenModal={setOpenModal} config={config} />
{routeConfig && <>
<Formik
@ -31,6 +31,8 @@ const RouteSecurity = ({ routeConfig }) => {
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),
@ -86,10 +88,6 @@ const RouteSecurity = ({ routeConfig }) => {
<Stack spacing={2}>
<MainCard name={routeConfig.Name} title={'Security'}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Alert color='info'>Additional security settings. MFA and Captcha are not yet implemented.</Alert>
</Grid>
<CosmosFormDivider title={'Authentication'} />
<CosmosCheckbox
@ -104,6 +102,20 @@ const RouteSecurity = ({ routeConfig }) => {
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
@ -167,12 +179,12 @@ const RouteSecurity = ({ routeConfig }) => {
<CosmosSelect
name="_SmartShield_PrivilegedGroups"
label="Privileged Groups (comma separated)"
placeholder="Privileged Groups"
label="Privileged Groups "
placeholder="Privileged Group"
options={[
[0, 'Default'],
[1, 'Users'],
[2, 'Admin'],
[1, 'Users & Admins'],
[2, 'Admin Only'],
]}
formik={formik}
/>

View file

@ -11,10 +11,11 @@ import {
FormHelperText,
} from '@mui/material';
import RestartModal from '../users/restart';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
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' }}>
@ -31,11 +32,30 @@ const debounce = (func, wait) => {
};
};
const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false, newRoute }) => {
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} />
<RestartModal openModal={openModal} setOpenModal={setOpenModal} config={config} />
{routeConfig && <>
<Formik
@ -46,17 +66,27 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
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,
@ -162,11 +192,17 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
: <CosmosInputText
name="Target"
label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
placeholder={formik.values.Mode == "PROXY" ? "localhost:8080" : "/path/to/my/app"}
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}>
@ -179,13 +215,21 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
formik={formik}
/>
{formik.values.UseHost && <CosmosInputText
{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"
@ -221,6 +265,44 @@ const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls
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

View file

@ -1,13 +1,19 @@
import * as React from 'react';
import MainCard from '../../../components/MainCard';
import RestartModal from '../users/restart';
import { Chip, Divider, Stack, useMediaQuery } from '@mui/material';
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, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
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)',
@ -16,20 +22,17 @@ const info = {
}
const RouteOverview = ({ routeConfig }) => {
const [openModal, setOpenModal] = React.useState(false);
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(() => {
setOpenModal(true);
redirectToLocal('/cosmos-ui/config-url');
});
}
return <div style={{ maxWidth: '1000px', width: '100%'}}>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
{routeConfig && <>
<MainCard name={routeConfig.Name} title={<div>
{routeConfig.Name} &nbsp;&nbsp;
@ -38,17 +41,34 @@ const RouteOverview = ({ routeConfig }) => {
</div>}>
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
<div>
<img src={getFaviconURL(routeConfig)} width="128px" />
<ImageWithPlaceholder className="loading-image" alt="" src={getFaviconURL(routeConfig)} width="128px" />
</div>
<Stack spacing={2} >
<strong>Description</strong>
<Stack spacing={2} style={{ width: '100%' }}>
<strong><ContainerOutlined />Description</strong>
<div style={info}>{routeConfig.Description}</div>
<strong>URL</strong>
<strong><NodeExpandOutlined /> URL</strong>
<div><HostChip route={routeConfig} /></div>
<strong>Target</strong>
<strong><InfoCircleOutlined /> Target</strong>
<div><RouteMode route={routeConfig} /> <Chip label={routeConfig.Target} /></div>
<strong>Security</strong>
<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>

View file

@ -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 { DeleteOutlined, SyncOutlined } from '@ant-design/icons';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputPassword, CosmosInputText, CosmosSelect } from './formShortcuts';
import CountrySelect, { countries } from '../../../components/countrySelect';
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 = () => {
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,23 +50,48 @@ 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: ''}}>
<IsLoggedIn />
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>Refresh</Button><br /><br />
<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,
@ -67,8 +100,13 @@ 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,
@ -77,69 +115,125 @@ const ConfigManagement = () => {
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,
RequireMFA: values.RequireMFA,
// AutoUpdate: values.AutoUpdate,
BlockedCountries: values.GeoBlocking,
HTTPConfig: {
...config.HTTPConfig,
Hostname: values.Hostname,
GenerateMissingAuthCert: values.GenerateMissingAuthCert,
HTTPPort: values.HTTPPort,
HTTPSPort: values.HTTPSPort,
SSLEmail: values.SSLEmail,
HTTPSCertificateMode: values.HTTPSCertificateMode,
DNSChallengeProvider: values.DNSChallengeProvider,
},
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,
}
}
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}>
<Stack spacing={3}>
{isAdmin && <MainCard>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<LoadingButton
disableElevation
loading={formik.isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
{saveLabel}
</LoadingButton>
</Grid>
</MainCard>}
{!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="General">
<Grid container spacing={3}>
<Grid item xs={12}>
@ -153,12 +247,6 @@ const ConfigManagement = () => {
helperText="Require MFA for all users"
/>
{/* <CosmosCheckbox
label="Auto Update Cosmos"
name="AutoUpdate"
formik={formik}
/> */}
<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>
@ -216,6 +304,95 @@ const ConfigManagement = () => {
</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);
});
}}
/>
<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>
@ -223,7 +400,7 @@ const ConfigManagement = () => {
<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>
<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"
@ -344,22 +521,66 @@ const ConfigManagement = () => {
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 blocked from accessing your server)</InputLabel>
<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" formik={formik} />
<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>
@ -369,38 +590,6 @@ const ConfigManagement = () => {
<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>
<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"
formik={formik}
/>
)
}
{
formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
<CosmosInputText
name="DNSChallengeProvider"
label="DNS provider (if you are using a DNS Challenge)"
formik={formik}
/>
)
}
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Field
@ -413,6 +602,69 @@ const ConfigManagement = () => {
</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}>
@ -431,31 +683,38 @@ const ConfigManagement = () => {
</Stack>
</Grid>
<Grid item xs={12}>
<CosmosCheckbox
label={"Force HTTPS Certificate Renewal On Next Save"}
name="ForceHTTPSCertificateRenewal"
formik={formik}
/>
</Grid>
</Grid>
</MainCard>
<MainCard>
{isAdmin && <MainCard>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button
<LoadingButton
disableElevation
disabled={formik.isSubmitting}
loading={formik.isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
Save
</Button>
</AnimateButton>
{saveLabel}
</LoadingButton>
</Grid>
</MainCard>
</MainCard>}
</Stack>
</form>
)}

View file

@ -33,7 +33,7 @@ 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);
@ -42,8 +42,6 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
const [portsOptions, setPortsOptions] = React.useState(null);
const loading = options === null;
const name = "Target"
const label = "Container Name"
let targetResult = {
container: 'null',
port: "",
@ -155,7 +153,7 @@ export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTa
}
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)
}
})();
@ -173,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])
@ -219,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) ? (<>
<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) ? (<>
<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>
@ -78,7 +101,7 @@ export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoC
<OutlinedInput
id={name}
type={showPassword ? 'text' : 'password'}
value={formik.values[name]}
value={getNestedValue(formik.values, name)}
name={name}
autoComplete={autoComplete}
onBlur={formik.handleBlur}
@ -125,7 +148,7 @@ export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoC
</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>
@ -136,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])
@ -145,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]}>
@ -186,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}
@ -200,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

@ -39,6 +39,9 @@ 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',
@ -64,6 +67,15 @@ const ProxyManagement = () => {
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 = {
@ -144,21 +156,31 @@ const ProxyManagement = () => {
</Stack>
{config && <>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
<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) => '/ui/config-url/' + r.Name}
linkTo={(r) => '/cosmos-ui/config-url/' + r.Name}
columns={[
{
title: '',
field: (r) => <img src={getFaviconURL(r)} width="64px" />,
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: {
@ -170,16 +192,24 @@ const ProxyManagement = () => {
<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={k}
up={(event) => up(event, k)}
down={(event) => down(event, k)}
deleteRoute={(event) => deleteRoute(event, k)}
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',
@ -215,20 +245,6 @@ const ProxyManagement = () => {
disableElevation
fullWidth
onClick={() => {
if(routes.some((route, key) => {
let errors = ValidateRoute(route, config);
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);

View file

@ -22,6 +22,7 @@ import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import IsLoggedIn from '../../../isLoggedIn';
import { useEffect, useState } from 'react';
import { isDomain } from '../../../utils/indexs';
function checkIsOnline() {
API.isOnline().then((res) => {
@ -33,25 +34,62 @@ function checkIsOnline() {
});
}
const RestartModal = ({openModal, setOpenModal}) => {
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 <>
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>{!isRestarting ? 'Restart Server?' : 'Restarting Server...'}</DialogTitle>
<DialogContent>
<DialogContentText>
{warn && <div>
<Alert severity="warning" icon={<WarningOutlined />}>
The server is taking longer than expected to restart.<br />Consider troubleshouting the logs.
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>
: 'A restart is required to apply changes. Do you want to restart?'}
: 'Do you want to restart your server?'}
</DialogContentText>
</DialogContent>
{!isRestarting && <DialogActions>
@ -68,7 +106,7 @@ const RestartModal = ({openModal, setOpenModal}) => {
}}>Restart</Button>
</DialogActions>}
</Dialog>
</>;
</>);
};
export default RestartModal;

View file

@ -57,7 +57,7 @@ const UserManagement = () => {
formType: ""+formType,
})
.then((values) => {
let sendLink = window.location.origin + '/ui/register?t='+formType+'&nickname='+nickname+'&key=' + values.data.registerKey;
let sendLink = window.location.origin + '/cosmos-ui/register?t='+formType+'&nickname='+nickname+'&key=' + values.data.registerKey;
setToAction({...values.data, nickname, sendLink, formType, formAction: formType === 2 ? 'invite them to the server' : 'let them reset their password'});
setOpenInviteForm(true);
});

View file

@ -0,0 +1,302 @@
// material-ui
import { Alert, Button, InputLabel, OutlinedInput, Stack, TextField } 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 { useState } from 'react';
import ResponsiveButton from '../../components/responseiveButton';
import { PlusCircleFilled } from '@ant-design/icons';
import { Formik } from 'formik';
import * as yup from 'yup';
import * as API from '../../api';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
import { DownloadFile } from '../../api/downloadButton';
import QRCode from 'qrcode';
import { useClientInfos } from '../../utils/hooks';
const getDocker = (data, isCompose) => {
let lighthouses = '';
for (let i = 0; i < data.LighthousesList.length; i++) {
const l = data.LighthousesList[i];
lighthouses += l.publicHostname + ";" + l.ip + ":" + l.port + ";" + l.isRelay + ",";
}
let containerName = "cosmos-constellation-lighthouse";
let imageName = "cosmos-constellation-lighthouse:latest";
let volPath = "/var/lib/cosmos-constellation";
if (isCompose) {
return `
version: "3.8"
services:
${containerName}:
image: ${imageName}
container_name: ${containerName}
restart: unless-stopped
network_mode: bridge
ports:
- "${data.Port}:4242"
volumes:
- ${volPath}:/config
environment:
- CA=${JSON.stringify(data.CA)}
- CERT=${JSON.stringify(data.PrivateKey)}
- KEY=${JSON.stringify(data.PublicKey)}
- LIGHTHOUSES=${lighthouses}
- PUBLIC_HOSTNAME=${data.PublicHostname}
- IS_RELAY=${data.IsRelay}
- IP=${data.IP}
`;
} else {
return `
docker run -d \\
--name ${containerName} \\
--restart unless-stopped \\
--network bridge \\
-v ${volPath}:/config \\
-e CA=${JSON.stringify(data.CA)} \\
-e CERT=${JSON.stringify(data.PrivateKey)} \\
-e KEY=${JSON.stringify(data.PublicKey)} \\
-e LIGHTHOUSES=${lighthouses} \\
-e PUBLIC_HOSTNAME=${data.PublicHostname} \\
-e IS_RELAY=${data.IsRelay} \\
-e IP=${data.IP} \\
-p ${data.Port}:4242 \\
${imageName}
`;
}
}
const AddDeviceModal = ({ users, config, refreshConfig, devices }) => {
const [openModal, setOpenModal] = useState(false);
const [isDone, setIsDone] = useState(null);
const canvasRef = React.useRef(null);
const {role, nickname} = useClientInfos();
const isAdmin = role === "2";
let firstIP = "192.168.201.2/24";
if (devices && devices.length > 0) {
const isIpFree = (ip) => {
return devices.filter((d) => d.ip === ip).length === 0;
}
let i = 1;
let j = 201;
while (!isIpFree(firstIP)) {
i++;
if (i > 254) {
i = 0;
j++;
}
firstIP = "192.168." + j + "." + i + "/24";
}
}
const renderCanvas = (data) => {
if (!canvasRef.current) return setTimeout(() => {
renderCanvas(data);
}, 500);
QRCode.toCanvas(canvasRef.current, JSON.stringify(data),
{
width: 600,
color: {
dark: "#000",
light: '#fff'
}
}, function (error) {
if (error) console.error(error)
})
}
return <>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<Formik
initialValues={{
nickname: nickname,
deviceName: '',
ip: firstIP,
publicKey: '',
Port: "4242",
PublicHostname: '',
IsRelay: true,
isLighthouse: false,
}}
validationSchema={yup.object({
})}
onSubmit={(values, { setSubmitting, setStatus, setErrors }) => {
if(values.isLighthouse) values.nickname = null;
return API.constellation.addDevice(values).then(({data}) => {
setIsDone(data);
refreshConfig();
renderCanvas(data.Config);
}).catch((err) => {
setErrors(err.response.data);
});
}}
>
{(formik) => (
<form onSubmit={formik.handleSubmit}>
<DialogTitle>Add Device</DialogTitle>
{isDone ? <DialogContent>
<DialogContentText>
<p>
Device added successfully!
Download scan the QR Code from the Cosmos app or download the relevant
files to your device along side the config and network certificate to
connect:
</p>
<Stack spacing={2} direction={"column"}>
{/* {isDone.isLighthouse ? <>
<CosmosFormDivider title={"Docker"} />
<TextField
fullWidth
multiline
value={getDocker(isDone, false)}
variant="outlined"
size="small"
disabled
/>
<CosmosFormDivider title={"File (Docker-Compose)"} />
<DownloadFile
filename={`docker-compose.yml`}
content={getDocker(isDone, true)}
label={"Download docker-compose.yml"}
/>
</> : <> */}
<CosmosFormDivider title={"QR Code"} />
<div style={{textAlign: 'center'}}>
<canvas style={{borderRadius: '15px'}} ref={canvasRef} />
</div>
{/* </>} */}
<CosmosFormDivider title={"File"} />
<DownloadFile
filename={`constellation.yml`}
content={isDone.Config}
label={"Download constellation.yml"}
/>
</Stack>
</DialogContentText>
</DialogContent> : <DialogContent>
<DialogContentText>
<p>Add a Device to the constellation using either the Cosmos or Nebula client</p>
<div>
<Stack spacing={2} style={{}}>
<CosmosCheckbox
name="isLighthouse"
label="Lighthouse"
formik={formik}
/>
{!formik.values.isLighthouse &&
(isAdmin ? <CosmosSelect
name="nickname"
label="Owner"
formik={formik}
// disabled={!isAdmin}
options={
users.map((u) => {
return [u.nickname, u.nickname]
})
}
/> : <>
<InputLabel>Owner</InputLabel>
<OutlinedInput
fullWidth
multiline
value={nickname}
variant="outlined"
size="small"
disabled
/>
</>)}
<CosmosInputText
name="deviceName"
label="Device Name"
formik={formik}
/>
<CosmosInputText
name="ip"
label="Constellation IP Address"
formik={formik}
/>
{/* <CosmosInputText
name="Port"
label="VPN Port (default: 4242)"
formik={formik}
/> */}
<CosmosInputText
multiline
name="publicKey"
label="Public Key (Optional)"
formik={formik}
/>
{formik.values.isLighthouse && <>
<CosmosFormDivider title={"Lighthouse Setup"} />
<CosmosInputText
name="PublicHostname"
label="Public Hostname"
formik={formik}
/>
<CosmosCheckbox
name="IsRelay"
label="Can Relay Traffic"
formik={formik}
/>
</>}
<div>
{formik.errors && formik.errors.length > 0 && <Stack spacing={2} direction={"column"}>
<Alert severity="error">{formik.errors.map((err) => {
return <div>{err}</div>
})}</Alert>
</Stack>}
</div>
</Stack>
</div>
</DialogContentText>
</DialogContent>}
<DialogActions>
<Button onClick={() => setOpenModal(false)}>Close</Button>
{!isDone && <Button color="primary" variant="contained" type="submit">Add</Button>}
</DialogActions>
</form>
)}
</Formik>
</Dialog>
<ResponsiveButton
color="primary"
onClick={() => {
setIsDone(null);
setOpenModal(true);
}}
variant={
"contained"
}
startIcon={<PlusCircleFilled />}
>
Add Device
</ResponsiveButton>
</>;
};
export default AddDeviceModal;

View file

@ -0,0 +1,174 @@
import React from "react";
import { useEffect, useState } from "react";
import * as API from "../../api";
import AddDeviceModal from "./addDevice";
import PrettyTableView from "../../components/tableView/prettyTableView";
import { DeleteButton } from "../../components/delete";
import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
import IsLoggedIn from "../../isLoggedIn";
import { Alert, Button, CircularProgress, InputLabel, Stack } from "@mui/material";
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
import MainCard from "../../components/MainCard";
import { Formik } from "formik";
import { LoadingButton } from "@mui/lab";
import ApiModal from "../../components/apiModal";
import { isDomain } from "../../utils/indexs";
import ConfirmModal from "../../components/confirmModal";
import UploadButtons from "../../components/fileUpload";
export const ConstellationDNS = () => {
const [isAdmin, setIsAdmin] = useState(false);
const [config, setConfig] = useState(null);
const refreshConfig = async () => {
let configAsync = await API.config.get();
setConfig(configAsync.data);
setIsAdmin(configAsync.isAdmin);
};
useEffect(() => {
refreshConfig();
}, []);
return <>
{(config) ? <>
<Stack spacing={2} style={{maxWidth: "1000px"}}>
<div>
<MainCard title={"Constellation Internal DNS"} content={config.constellationIP}>
<Stack spacing={2}>
<Formik
initialValues={{
Fallback: config.ConstellationConfig.DNSFallback,
DNSBlockBlacklist: config.ConstellationConfig.DNSBlockBlacklist,
DNSAdditionalBlocklists: config.ConstellationConfig.DNSAdditionalBlocklists || [],
CustomDNSEntries: config.ConstellationConfig.CustomDNSEntries || []
}}
onSubmit={(values) => {
let newConfig = { ...config };
newConfig.ConstellationConfig.DNSFallback = values.Fallback;
newConfig.ConstellationConfig.DNSBlockBlacklist = values.DNSBlockBlacklist;
newConfig.ConstellationConfig.DNSAdditionalBlocklists = values.DNSAdditionalBlocklists;
newConfig.ConstellationConfig.CustomDNSEntries = values.CustomDNSEntries;
return API.config.set(newConfig);
}}
>
{(formik) => (
<form onSubmit={formik.handleSubmit}>
<Stack spacing={2}>
<Alert severity="info">This is a DNS that runs inside your Constellation network. It automatically
rewrites your domains DNS entries to be local to your network, and also allows you to do things like block ads
and trackers on all devices connected to your network. You can also add custom DNS entries to resolve to specific
IP addresses. This DNS server is only accessible from inside your network.</Alert>
<CosmosInputText formik={formik} name="Fallback" label="DNS Fallback" placeholder={'8.8.8.8:53'} />
<CosmosFormDivider title={"DNS Blocklists"} />
<CosmosCheckbox formik={formik} name="DNSBlockBlacklist" label="Use Blacklists to block domains" />
<Alert severity="warning">When changing your DNS records, always use private mode on your browser and allow some times for various caches to expire.</Alert>
<InputLabel>DNS Blocklist URLs</InputLabel>
{formik.values.DNSAdditionalBlocklists && formik.values.DNSAdditionalBlocklists.map((item, index) => (
<Stack direction={"row"} spacing={2} key={`DNSAdditionalBlocklists${item}`} width={"100%"}>
<DeleteButton onDelete={() => {
formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists.slice(0, index), ...formik.values.DNSAdditionalBlocklists.slice(index + 1)]);
}} />
<div style={{flexGrow: 1}}>
<CosmosInputText
value={item}
name={`DNSAdditionalBlocklists${index}`}
placeholder={'https://example.com/blocklist.txt'}
onChange={(e) => {
formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists.slice(0, index), e.target.value, ...formik.values.DNSAdditionalBlocklists.slice(index + 1)]);
}}
/>
</div>
</Stack>
))}
<Stack direction="row" spacing={2}>
<Button variant="outlined" onClick={() => {
formik.setFieldValue("DNSAdditionalBlocklists", [...formik.values.DNSAdditionalBlocklists, ""]);
}}>Add</Button>
<Button variant="outlined" onClick={() => {
formik.setFieldValue("DNSAdditionalBlocklists", [
"https://s3.amazonaws.com/lists.disconnect.me/simple_tracking.txt",
"https://s3.amazonaws.com/lists.disconnect.me/simple_ad.txt",
"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts",
"https://raw.githubusercontent.com/StevenBlack/hosts/master/alternates/fakenews-only/hosts"
]);
}}>Reset Default</Button>
</Stack>
<CosmosFormDivider title={"DNS Custom Entries"} />
<InputLabel>DNS Custom Entries</InputLabel>
{formik.values.CustomDNSEntries && formik.values.CustomDNSEntries.map((item, index) => (
<Stack direction={"row"} spacing={2} key={`CustomDNSEntries${item}`} width={"100%"}>
<DeleteButton onDelete={() => {
formik.setFieldValue("CustomDNSEntries", [...formik.values.CustomDNSEntries.slice(0, index), ...formik.values.CustomDNSEntries.slice(index + 1)]);
}} />
<div style={{flexGrow: 1}}>
<CosmosInputText
value={item.Key}
name={`CustomDNSEntries${index}-key`}
placeholder={'domain.com'}
onChange={(e) => {
const updatedCustomDNSEntries = [...formik.values.CustomDNSEntries];
updatedCustomDNSEntries[index].Key = e.target.value;
formik.setFieldValue("CustomDNSEntries", updatedCustomDNSEntries);
}}
/>
</div>
<div style={{flexGrow: 1}}>
<CosmosInputText
value={item.Value}
name={`CustomDNSEntries${index}-value`}
placeholder={'1213.123.123.123'}
onChange={(e) => {
const updatedCustomDNSEntries = [...formik.values.CustomDNSEntries];
updatedCustomDNSEntries[index].Value = e.target.value;
formik.setFieldValue("CustomDNSEntries", updatedCustomDNSEntries);
}}
/>
</div>
</Stack>
))}
<Stack direction="row" spacing={2}>
<Button variant="outlined" onClick={() => {
formik.setFieldValue("CustomDNSEntries", [...formik.values.CustomDNSEntries, {
Key: "",
Value: "",
Type: "A"
}]);
}}>Add</Button>
<Button variant="outlined" onClick={() => {
formik.setFieldValue("CustomDNSEntries", [
]);
}}>Reset</Button>
</Stack>
<LoadingButton
disableElevation
loading={formik.isSubmitting}
type="submit"
variant="contained"
color="primary"
>
Save
</LoadingButton>
</Stack>
</form>
)}
</Formik>
</Stack>
</MainCard>
</div>
</Stack>
</> : <center>
<CircularProgress color="inherit" size={20} />
</center>}
</>
};

View file

@ -0,0 +1,57 @@
import * as React from 'react';
import MainCard from '../../components/MainCard';
import { Alert, Chip, Divider, 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, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
import IsLoggedIn from '../../isLoggedIn';
import PrettyTabbedView from '../../components/tabbedView/tabbedView';
import { useClientInfos } from '../../utils/hooks';
import { ConstellationVPN } from './vpn';
import { ConstellationDNS } from './dns';
const ConstellationIndex = () => {
const {role} = useClientInfos();
const isAdmin = role === "2";
return isAdmin ? <div>
<IsLoggedIn />
<PrettyTabbedView path="/cosmos-ui/constellation/:tab" tabs={[
{
title: 'VPN',
children: <ConstellationVPN />,
path: 'vpn'
},
{
title: 'DNS',
children: <ConstellationDNS />,
path: 'dns'
},
{
title: 'Firewall',
children: <div>
<Alert severity="info">
Coming soon. This feature will allow you to open and close ports individually
on each device and decide who can access them.
</Alert>
</div>,
},
{
title: 'Unsafe Routes',
children: <div>
<Alert severity="info">
Coming soon. This feature will allow you to tunnel your traffic through
your devices to things outside of your constellation.
</Alert>
</div>,
}
]}/>
</div> : <ConstellationVPN />;
}
export default ConstellationIndex;

View file

@ -0,0 +1,220 @@
import React from "react";
import { useEffect, useState } from "react";
import * as API from "../../api";
import AddDeviceModal from "./addDevice";
import PrettyTableView from "../../components/tableView/prettyTableView";
import { DeleteButton } from "../../components/delete";
import { CloudOutlined, CloudServerOutlined, CompassOutlined, DesktopOutlined, LaptopOutlined, MobileOutlined, TabletOutlined } from "@ant-design/icons";
import IsLoggedIn from "../../isLoggedIn";
import { Alert, Button, CircularProgress, Stack } from "@mui/material";
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText } from "../config/users/formShortcuts";
import MainCard from "../../components/MainCard";
import { Formik } from "formik";
import { LoadingButton } from "@mui/lab";
import ApiModal from "../../components/apiModal";
import { isDomain } from "../../utils/indexs";
import ConfirmModal from "../../components/confirmModal";
import UploadButtons from "../../components/fileUpload";
import { useClientInfos } from "../../utils/hooks";
const getDefaultConstellationHostname = (config) => {
// if domain is set, use it
if(isDomain(config.HTTPConfig.Hostname)) {
return "vpn." + config.HTTPConfig.Hostname;
} else {
return config.HTTPConfig.Hostname;
}
}
export const ConstellationVPN = () => {
const [config, setConfig] = useState(null);
const [users, setUsers] = useState(null);
const [devices, setDevices] = useState(null);
const {role} = useClientInfos();
const isAdmin = role === "2";
const refreshConfig = async () => {
let configAsync = await API.config.get();
setConfig(configAsync.data);
setDevices((await API.constellation.list()).data || []);
if(isAdmin)
setUsers((await API.users.list()).data || []);
else
setUsers([]);
};
useEffect(() => {
refreshConfig();
}, []);
const getIcon = (r) => {
if (r.deviceName.toLowerCase().includes("mobile") || r.deviceName.toLowerCase().includes("phone")) {
return <MobileOutlined />
}
else if (r.deviceName.toLowerCase().includes("laptop") || r.deviceName.toLowerCase().includes("computer")) {
return <LaptopOutlined />
} else if (r.deviceName.toLowerCase().includes("desktop")) {
return <DesktopOutlined />
} else if (r.deviceName.toLowerCase().includes("tablet")) {
return <TabletOutlined />
} else if (r.deviceName.toLowerCase().includes("lighthouse") || r.deviceName.toLowerCase().includes("server")) {
return <CompassOutlined />
} else {
return <CloudOutlined />
}
}
return <>
{(devices && config && users) ? <>
<Stack spacing={2} style={{maxWidth: "1000px"}}>
<div>
<Alert severity="info">
Constellation is a VPN that runs inside your Cosmos network. It automatically
connects all your devices together, and allows you to access them from anywhere.
Please refer to the <a href="https://cosmos-cloud.io/doc/61 Constellation VPN/" target="_blank">documentation</a> for more information.
In order to connect, please use the <a href="https://cosmos-cloud.io/clients" target="_blank">Constellation App</a>.
Constellation is currently free to use until the end of the beta, planned January 2024.
</Alert>
<MainCard title={"Constellation Setup"} content={config.constellationIP}>
<Stack spacing={2}>
{config.ConstellationConfig.Enabled && config.ConstellationConfig.SlaveMode && <>
<Alert severity="info">
You are currently connected to an external constellation network. Use your main Cosmos server to manage your constellation network and devices.
</Alert>
</>}
<Formik
initialValues={{
Enabled: config.ConstellationConfig.Enabled,
PrivateNode: config.ConstellationConfig.PrivateNode,
IsRelay: config.ConstellationConfig.NebulaConfig.Relay.AMRelay,
ConstellationHostname: (config.ConstellationConfig.ConstellationHostname && config.ConstellationConfig.ConstellationHostname != "") ? config.ConstellationConfig.ConstellationHostname :
getDefaultConstellationHostname(config)
}}
onSubmit={(values) => {
let newConfig = { ...config };
newConfig.ConstellationConfig.Enabled = values.Enabled;
newConfig.ConstellationConfig.PrivateNode = values.PrivateNode;
newConfig.ConstellationConfig.NebulaConfig.Relay.AMRelay = values.IsRelay;
newConfig.ConstellationConfig.ConstellationHostname = values.ConstellationHostname;
setTimeout(() => {
refreshConfig();
}, 1500);
return API.config.set(newConfig);
}}
>
{(formik) => (
<form onSubmit={formik.handleSubmit}>
<Stack spacing={2}>
{formik.values.Enabled && <Stack spacing={2} direction="row">
<Button
disableElevation
variant="outlined"
color="primary"
onClick={async () => {
await API.constellation.restart();
}}
>
Restart VPN Service
</Button>
<ApiModal callback={API.constellation.getLogs} label={"Show VPN logs"} />
<ApiModal callback={API.constellation.getConfig} label={"Show VPN Config"} />
<ConfirmModal
variant="outlined"
color="warning"
label={"Reset Network"}
content={"This will completely reset the network, and disconnect all the clients. You will need to reconnect them. This cannot be undone."}
callback={async () => {
await API.constellation.reset();
refreshConfig();
}}
/>
</Stack>}
<CosmosCheckbox formik={formik} name="Enabled" label="Constellation Enabled" />
{config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
{formik.values.Enabled && <>
<CosmosCheckbox formik={formik} name="IsRelay" label="Relay requests via this Node" />
<CosmosCheckbox formik={formik} name="PrivateNode" label="This node is Private (no public IP)" />
{!formik.values.PrivateNode && <>
<Alert severity="info">This is your Constellation hostname, that you will use to connect. If you are using a domain name, this needs to be different from your server's hostname. Whatever the domain you choose, it is very important that you make sure there is a A entry in your domain DNS pointing to this server. <strong>If you change this value, you will need to reset your network and reconnect all the clients!</strong></Alert>
<CosmosInputText formik={formik} name="ConstellationHostname" label="Constellation Hostname" />
</>}
</>}
</>}
<LoadingButton
disableElevation
loading={formik.isSubmitting}
type="submit"
variant="contained"
color="primary"
>
Save
</LoadingButton>
<UploadButtons
accept=".yml,.yaml"
label={"Upload External Constellation Network File"}
variant="outlined"
fullWidth
OnChange={async (e) => {
let file = e.target.files[0];
await API.constellation.connect(file);
setTimeout(() => {
refreshConfig();
}, 1000);
}}
/>
</Stack>
</form>
)}
</Formik>
</Stack>
</MainCard>
</div>
{config.ConstellationConfig.Enabled && !config.ConstellationConfig.SlaveMode && <>
<CosmosFormDivider title={"Devices"} />
<PrettyTableView
data={devices.filter((d) => !d.blocked)}
getKey={(r) => r.deviceName}
buttons={[
<AddDeviceModal users={users} config={config} refreshConfig={refreshConfig} devices={devices}/>,
]}
columns={[
{
title: '',
field: getIcon,
},
{
title: 'Device Name',
field: (r) => <strong>{r.deviceName}</strong>,
},
{
title: 'Owner',
field: (r) => <strong>{r.nickname}</strong>,
},
{
title: 'Type',
field: (r) => <strong>{r.isLighthouse ? "Lighthouse" : "Client"}</strong>,
},
{
title: 'Constellation IP',
screenMin: 'md',
field: (r) => r.ip,
},
{
title: '',
clickable: true,
field: (r) => {
return <DeleteButton onDelete={async () => {
await API.constellation.block(r.nickname, r.deviceName, true);
refreshConfig();
}}></DeleteButton>
}
}
]}
/>
</>}
</Stack>
</> : <center>
<CircularProgress color="inherit" size={20} />
</center>}
</>
};

View file

@ -0,0 +1,507 @@
import * as React from 'react';
import IsLoggedIn from '../../isLoggedIn';
import * as API from '../../api';
import MainCard from '../../components/MainCard';
import { Formik, Field, useFormik, FormikProvider } from 'formik';
import * as Yup from 'yup';
import {
Alert,
Button,
Checkbox,
FormControlLabel,
Grid,
InputLabel,
OutlinedInput,
Stack,
FormHelperText,
TextField,
MenuItem,
Skeleton,
CircularProgress,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Box,
} from '@mui/material';
import { ExclamationCircleOutlined, InfoCircleOutlined, PlusCircleOutlined, SyncOutlined, WarningOutlined } from '@ant-design/icons';
import PrettyTableView from '../../components/tableView/prettyTableView';
import { DeleteButton } from '../../components/delete';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
import { MetricPicker } from './MetricsPicker';
const DisplayOperator = (operator) => {
switch (operator) {
case 'gt':
return '>';
case 'lt':
return '<';
case 'eq':
return '=';
default:
return '?';
}
}
const AlertValidationSchema = Yup.object().shape({
name: Yup.string().required('Name is required'),
trackingMetric: Yup.string().required('Tracking metric is required'),
conditionOperator: Yup.string().required('Condition operator is required'),
conditionValue: Yup.number().required('Condition value is required'),
period: Yup.string().required('Period is required'),
});
const EditAlertModal = ({ open, onClose, onSave }) => {
const formik = useFormik({
initialValues: {
name: open.Name || 'New Alert',
trackingMetric: open.TrackingMetric || '',
conditionOperator: (open.Condition && open.Condition.Operator) || 'gt',
conditionValue: (open.Condition && open.Condition.Value) || 0,
conditionPercent: (open.Condition && open.Condition.Percent) || false,
period: open.Period || 'latest',
actions: open.Actions || [],
throttled: typeof open.Throttled === 'boolean' ? open.Throttled : true,
severity: open.Severity || 'error',
},
validationSchema: AlertValidationSchema,
onSubmit: (values) => {
values.actions = values.actions.filter((a) => !a.removed);
onSave(values);
onClose();
},
});
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>Edit Alert</DialogTitle>
<FormikProvider value={formik}>
<form onSubmit={formik.handleSubmit}>
<DialogContent>
<Stack spacing={2}>
<CosmosInputText
name="name"
label="Name of the alert"
formik={formik}
required
/>
<MetricPicker
name="trackingMetric"
label="Metric to track"
formik={formik}
required
/>
<Stack direction="row" spacing={2} alignItems="center">
<CosmosSelect
name="conditionOperator"
label="Trigger Condition Operator"
formik={formik}
options={[
['gt', '>'],
['lt', '<'],
['eq', '='],
]}
>
</CosmosSelect>
<CosmosInputText
name="conditionValue"
label="Trigger Condition Value"
formik={formik}
required
/>
<CosmosCheckbox
style={{paddingTop: '20px'}}
name="conditionPercent"
label="Condition is a percent of max value"
formik={formik}
/>
</Stack>
<CosmosSelect
name="period"
label="Period (how often to check the metric)"
formik={formik}
options={[
['latest', 'Latest'],
['hourly', 'Hourly'],
['daily', 'Daily'],
]}></CosmosSelect>
<CosmosSelect
name="severity"
label="Severity"
formik={formik}
options={[
['info', 'Info'],
['warn', 'Warning'],
['error', 'Error'],
]}></CosmosSelect>
<CosmosCheckbox
name="throttled"
label="Throttle (only triggers a maximum of once a day)"
formik={formik}
/>
<CosmosFormDivider title={'Action Triggers'} />
<Stack direction="column" spacing={2}>
{formik.values.actions
.map((action, index) => {
return !action.removed && <>
{action.Type === 'stop' &&
<Alert severity="info">Stop action will attempt to stop/disable any resources (ex. Containers, routes, etc... ) attachted to the metric.
This will only have an effect on metrics specific to a resources (ex. CPU of a specific container). It will not do anything on global metric such as global used CPU</Alert>
}
<Stack direction="row" spacing={2} key={index}>
<Box style={{
width: '100%',
}}>
<CosmosSelect
name={`actions.${index}.Type`}
label="Action Type"
formik={formik}
options={[
['notification', 'Send a notification'],
['email', 'Send an email'],
['stop', 'Stop resources causing the alert'],
]}
/>
</Box>
<Box style={{
height: '95px',
display: 'flex',
alignItems: 'center',
}}>
<DeleteButton
onDelete={() => {
formik.setFieldValue(`actions.${index}.removed`, true);
}}
/>
</Box>
</Stack>
</>
})}
<Button
variant="outlined"
color="primary"
startIcon={<PlusCircleOutlined />}
onClick={() => {
formik.setFieldValue('actions', [
...formik.values.actions,
{
Type: 'notification',
},
]);
}}>
Add Action
</Button>
</Stack>
</Stack>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button variant='contained' type="submit">Save</Button>
</DialogActions>
</form>
</FormikProvider>
</Dialog>
);
};
const AlertPage = () => {
const [config, setConfig] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
const [openModal, setOpenModal] = React.useState(false);
const [metrics, setMetrics] = React.useState({});
function refresh() {
API.config.get().then((res) => {
setConfig(res.data);
setIsLoading(false);
});
API.metrics.list().then((res) => {
setMetrics(res.data);
});
}
React.useEffect(() => {
refresh();
}, []);
const setEnabled = (name) => (event) => {
setIsLoading(true);
let toSave = {
...config,
MonitoringAlerts: {
...config.MonitoringAlerts,
[name]: {
...config.MonitoringAlerts[name],
Enabled: event.target.checked,
}
}
};
API.config.set(toSave).then(() => {
refresh();
});
}
const deleteAlert = (name) => {
setIsLoading(true);
let toSave = {
...config,
MonitoringAlerts: {
...config.MonitoringAlerts,
}
};
delete toSave.MonitoringAlerts[name];
API.config.set(toSave).then(() => {
refresh();
});
}
const saveAlert = (data) => {
setIsLoading(true);
data.conditionValue = parseInt(data.conditionValue);
let toSave = {
...config,
MonitoringAlerts: {
...config.MonitoringAlerts,
[data.name]: {
Name: data.name,
Enabled: true,
TrackingMetric: data.trackingMetric,
Condition: {
Operator: data.conditionOperator,
Value: data.conditionValue,
Percent: data.conditionPercent,
},
Period: data.period,
Actions: data.actions,
LastTriggered: null,
Throttled: data.throttled,
Severity: data.severity,
}
}
};
API.config.set(toSave).then(() => {
refresh();
});
}
const resetTodefault = () => {
setIsLoading(true);
let toSave = {
...config,
MonitoringAlerts: {
"Anti Crypto-Miner": {
"Name": "Anti Crypto-Miner",
"Enabled": false,
"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": false,
"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"
}
}
};
API.config.set(toSave).then(() => {
refresh();
});
}
const GetSevIcon = ({level}) => {
switch (level) {
case 'info':
return <span style={{color: '#2196f3'}}><InfoCircleOutlined /></span>;
case 'warn':
return <span style={{color: '#ff9800'}}><WarningOutlined /></span>;
case 'error':
return <span style={{color: '#f44336'}}><ExclamationCircleOutlined /></span>;
default:
return '';
}
}
return <div style={{maxWidth: '1200px', margin: ''}}>
<IsLoggedIn />
{openModal && <EditAlertModal open={openModal} onClose={() => setOpenModal(false)} onSave={saveAlert} />}
<Stack direction="row" spacing={2} style={{marginBottom: '15px'}}>
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>Refresh</Button>
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
setOpenModal(true);
}}>Create</Button>
<Button variant="outlined" color="warning" startIcon={<WarningOutlined />} onClick={() => {
resetTodefault();
}}>Reset to default</Button>
</Stack>
{config && <>
<Formik
initialValues={{
Actions: config.MonitoringAlerts
}}
// validationSchema={Yup.object().shape({
// })}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
setSubmitting(true);
let toSave = {
...config,
MonitoringAlerts: values.Actions
};
return API.config.set(toSave);
}}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<Stack spacing={3}>
{!config && <Skeleton variant="rectangular" height={300} />}
{config && (!config.MonitoringAlerts || !Object.values(config.MonitoringAlerts).length) ? <Alert severity="info">No alerts configured.</Alert> : ''}
{config && config.MonitoringAlerts && Object.values(config.MonitoringAlerts).length ? <PrettyTableView
data={Object.values(config.MonitoringAlerts)}
getKey={(r) => r.Name + r.Target + r.Mode}
onRowClick={(r, k) => {
setOpenModal(r);
}}
columns={[
{
title: 'Enabled',
clickable:true,
field: (r, k) => <Checkbox disabled={isLoading} size='large' color={r.Enabled ? 'success' : 'default'}
onChange={setEnabled(Object.keys(config.MonitoringAlerts)[k])}
checked={r.Enabled}
/>,
style: {
},
},
{
title: 'Name',
field: (r) => <><GetSevIcon level={r.Severity} /> {r.Name}</>,
},
{
title: 'Tracking Metric',
field: (r) => metrics[r.TrackingMetric] ? metrics[r.TrackingMetric] : r.TrackingMetric,
},
{
title: 'Condition',
screenMin: 'md',
field: (r) => DisplayOperator(r.Condition.Operator) + ' ' + r.Condition.Value + (r.Condition.Percent ? '%' : ''),
},
{
title: 'Period',
field: (r) => r.Period,
},
{
title: 'Last Triggered',
screenMin: 'md',
field: (r) => (r.LastTriggered != "0001-01-01T00:00:00Z") ? new Date(r.LastTriggered).toLocaleString() : 'Never',
},
{
title: 'Actions',
field: (r) => r.Actions.map((a) => a.Type).join(', '),
screenMin: 'md',
},
{ title: '', clickable:true, field: (r, k) => <DeleteButton disabled={isLoading} onDelete={() => {
deleteAlert(Object.keys(config.MonitoringAlerts)[k])
}}/>,
style: {
textAlign: 'right',
}
},
]}
/> : ''}
</Stack>
</form>
)}
</Formik>
</>}
</div>;
}
export default AlertPage;

View file

@ -1,121 +0,0 @@
import PropTypes from 'prop-types';
import { useState, useEffect } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
// third-party
import ReactApexChart from 'react-apexcharts';
// chart options
const areaChartOptions = {
chart: {
height: 450,
type: 'area',
toolbar: {
show: false
}
},
dataLabels: {
enabled: false
},
stroke: {
curve: 'smooth',
width: 2
},
grid: {
strokeDashArray: 0
}
};
// ==============================|| INCOME AREA CHART ||============================== //
const IncomeAreaChart = ({ slot }) => {
const theme = useTheme();
const { primary, secondary } = theme.palette.text;
const line = theme.palette.divider;
const [options, setOptions] = useState(areaChartOptions);
useEffect(() => {
setOptions((prevState) => ({
...prevState,
colors: [theme.palette.primary.main, theme.palette.primary[700]],
xaxis: {
categories:
slot === 'month'
? ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'],
labels: {
style: {
colors: [
secondary,
secondary,
secondary,
secondary,
secondary,
secondary,
secondary,
secondary,
secondary,
secondary,
secondary,
secondary
]
}
},
axisBorder: {
show: true,
color: line
},
tickAmount: slot === 'month' ? 11 : 7
},
yaxis: {
labels: {
style: {
colors: [secondary]
}
}
},
grid: {
borderColor: line
},
tooltip: {
theme: 'light'
}
}));
}, [primary, secondary, line, theme, slot]);
const [series, setSeries] = useState([
{
name: 'Page Views',
data: [0, 86, 28, 115, 48, 210, 136]
},
{
name: 'Sessions',
data: [0, 43, 14, 56, 24, 105, 68]
}
]);
useEffect(() => {
setSeries([
{
name: 'Page Views',
data: slot === 'month' ? [76, 85, 101, 98, 87, 105, 91, 114, 94, 86, 115, 35] : [31, 40, 28, 51, 42, 109, 100]
},
{
name: 'Sessions',
data: slot === 'month' ? [110, 60, 150, 35, 60, 36, 26, 45, 65, 52, 53, 41] : [11, 32, 45, 32, 34, 52, 41]
}
]);
}, [slot]);
return <ReactApexChart options={options} series={series} type="area" height={450} />;
};
IncomeAreaChart.propTypes = {
slot: PropTypes.string
};
export default IncomeAreaChart;

View file

@ -0,0 +1,88 @@
import { useEffect, useState } from 'react';
// material-ui
import {
Button,
Stack,
} from '@mui/material';
import { formatDate } from './components/utils';
const MetricHeaders = ({loaded, slot, setSlot, zoom, setZoom}) => {
const resetZoom = () => {
setZoom({
xaxis: {}
});
}
let xAxis = [];
if(slot === 'latest') {
for(let i = 0; i < 100; i++) {
xAxis.unshift(i);
}
}
else if(slot === 'hourly') {
for(let i = 0; i < 48; i++) {
let now = new Date();
now.setHours(now.getHours() - i);
now.setMinutes(0);
now.setSeconds(0);
xAxis.unshift(formatDate(now, true));
}
} else if(slot === 'daily') {
for(let i = 0; i < 30; i++) {
let now = new Date();
now.setDate(now.getDate() - i);
xAxis.unshift(formatDate(now));
}
}
return (
<>
{loaded && <div style={{zIndex:2, position: 'relative'}}>
<Stack direction="row" alignItems="center" spacing={0} style={{marginTop: 10}}>
<Button
size="small"
onClick={() => {setSlot('latest'); resetZoom()}}
color={slot === 'latest' ? 'primary' : 'secondary'}
variant={slot === 'latest' ? 'outlined' : 'text'}
>
Latest
</Button>
<Button
size="small"
onClick={() => {setSlot('hourly'); resetZoom()}}
color={slot === 'hourly' ? 'primary' : 'secondary'}
variant={slot === 'hourly' ? 'outlined' : 'text'}
>
Hourly
</Button>
<Button
size="small"
onClick={() => {setSlot('daily'); resetZoom()}}
color={slot === 'daily' ? 'primary' : 'secondary'}
variant={slot === 'daily' ? 'outlined' : 'text'}
>
Daily
</Button>
{zoom.xaxis.min && <Button
size="small"
onClick={() => {
setZoom({
xaxis: {}
});
}}
color={'primary'}
variant={'outlined'}
>
Reset Zoom
</Button>}
</Stack>
</div>}
</>
);
};
export default MetricHeaders;

View file

@ -0,0 +1,121 @@
import * as React from 'react';
import {
Checkbox,
Divider,
FormControlLabel,
Grid,
InputLabel,
OutlinedInput,
Stack,
Typography,
FormHelperText,
TextField,
MenuItem,
AccordionSummary,
AccordionDetails,
Accordion,
Chip,
Box,
FormControl,
IconButton,
InputAdornment,
Autocomplete,
} from '@mui/material';
import { Field } from 'formik';
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import * as API from '../../api';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
export const MetricPicker = ({ metricsInit, name, style, value, errors, placeholder, onChange, label, formik }) => {
const [metrics, setMetrics] = React.useState(metricsInit || {});
function refresh() {
API.metrics.list().then((res) => {
let m = [];
let wildcards = {};
Object.keys(res.data).forEach((key) => {
m.push({
label: res.data[key],
value: key,
});
let keysplit = key.split('.');
if (keysplit.length > 1) {
for (let i = 0; i < keysplit.length - 1; i++) {
let wildcard = keysplit.slice(0, i + 1).join('.') + '.*';
wildcards[wildcard] = true;
}
}
});
Object.keys(wildcards).forEach((key) => {
m.push({
label: "Wildcard for " + key.split('.*')[0],
value: key,
});
});
setMetrics(m);
});
}
React.useEffect(() => {
if (!metricsInit)
refresh();
}, []);
return <Grid item xs={12}>
<Stack spacing={1} style={style}>
{label && <InputLabel htmlFor={name}>{label}</InputLabel>}
{/* <OutlinedInput
id={name}
type={'text'}
value={value || (formik && formik.values[name])}
name={name}
onBlur={(...ar) => {
return formik && formik.handleBlur(...ar);
}}
onChange={(...ar) => {
onChange && onChange(...ar);
return formik && formik.handleChange(...ar);
}}
placeholder={placeholder}
fullWidth
error={Boolean(formik && formik.touched[name] && formik.errors[name])}
/> */}
<Autocomplete
disablePortal
name={name}
value={value || (formik && formik.values[name])}
id="combo-box-demo"
isOptionEqualToValue={(option, value) => option.value === value}
options={metrics}
freeSolo
getOptionLabel={(option) => {
return option.label ?
`${option.value} - ${option.label}` : (formik && formik.values[name]);
}}
onChange={(event, newValue) => {
onChange && onChange(newValue.value);
return formik && formik.setFieldValue(name, newValue.value);
}}
renderInput={(params) => <TextField {...params} />}
/>
{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>
)}
</Stack>
</Grid>
}

View file

@ -1,85 +0,0 @@
import { useEffect, useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
// third-party
import ReactApexChart from 'react-apexcharts';
// chart options
const barChartOptions = {
chart: {
type: 'bar',
height: 365,
toolbar: {
show: false
}
},
plotOptions: {
bar: {
columnWidth: '45%',
borderRadius: 4
}
},
dataLabels: {
enabled: false
},
xaxis: {
categories: ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'],
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
show: false
},
grid: {
show: false
}
};
// ==============================|| MONTHLY BAR CHART ||============================== //
const MonthlyBarChart = () => {
const theme = useTheme();
const { primary, secondary } = theme.palette.text;
const info = theme.palette.info.light;
const [series] = useState([
{
data: [80, 95, 70, 42, 65, 55, 78]
}
]);
const [options, setOptions] = useState(barChartOptions);
useEffect(() => {
setOptions((prevState) => ({
...prevState,
colors: [info],
xaxis: {
labels: {
style: {
colors: [secondary, secondary, secondary, secondary, secondary, secondary, secondary]
}
}
},
tooltip: {
theme: 'light'
}
}));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [primary, info, secondary]);
return (
<div id="chart">
<ReactApexChart options={options} series={series} type="bar" height={365} />
</div>
);
};
export default MonthlyBarChart;

View file

@ -1,224 +0,0 @@
import PropTypes from 'prop-types';
import { useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
// material-ui
import { Box, Link, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Typography } from '@mui/material';
// third-party
import NumberFormat from 'react-number-format';
// project import
import Dot from '../../components/@extended/Dot';
function createData(trackingNo, name, fat, carbs, protein) {
return { trackingNo, name, fat, carbs, protein };
}
const rows = [
createData(84564564, 'Camera Lens', 40, 2, 40570),
createData(98764564, 'Laptop', 300, 0, 180139),
createData(98756325, 'Mobile', 355, 1, 90989),
createData(98652366, 'Handset', 50, 1, 10239),
createData(13286564, 'Computer Accessories', 100, 1, 83348),
createData(86739658, 'TV', 99, 0, 410780),
createData(13256498, 'Keyboard', 125, 2, 70999),
createData(98753263, 'Mouse', 89, 2, 10570),
createData(98753275, 'Desktop', 185, 1, 98063),
createData(98753291, 'Chair', 100, 0, 14001)
];
function descendingComparator(a, b, orderBy) {
if (b[orderBy] < a[orderBy]) {
return -1;
}
if (b[orderBy] > a[orderBy]) {
return 1;
}
return 0;
}
function getComparator(order, orderBy) {
return order === 'desc' ? (a, b) => descendingComparator(a, b, orderBy) : (a, b) => -descendingComparator(a, b, orderBy);
}
function stableSort(array, comparator) {
const stabilizedThis = array.map((el, index) => [el, index]);
stabilizedThis.sort((a, b) => {
const order = comparator(a[0], b[0]);
if (order !== 0) {
return order;
}
return a[1] - b[1];
});
return stabilizedThis.map((el) => el[0]);
}
// ==============================|| ORDER TABLE - HEADER CELL ||============================== //
const headCells = [
{
id: 'trackingNo',
align: 'left',
disablePadding: false,
label: 'Tracking No.'
},
{
id: 'name',
align: 'left',
disablePadding: true,
label: 'Product Name'
},
{
id: 'fat',
align: 'right',
disablePadding: false,
label: 'Total Order'
},
{
id: 'carbs',
align: 'left',
disablePadding: false,
label: 'Status'
},
{
id: 'protein',
align: 'right',
disablePadding: false,
label: 'Total Amount'
}
];
// ==============================|| ORDER TABLE - HEADER ||============================== //
function OrderTableHead({ order, orderBy }) {
return (
<TableHead>
<TableRow>
{headCells.map((headCell) => (
<TableCell
key={headCell.id}
align={headCell.align}
padding={headCell.disablePadding ? 'none' : 'normal'}
sortDirection={orderBy === headCell.id ? order : false}
>
{headCell.label}
</TableCell>
))}
</TableRow>
</TableHead>
);
}
OrderTableHead.propTypes = {
order: PropTypes.string,
orderBy: PropTypes.string
};
// ==============================|| ORDER TABLE - STATUS ||============================== //
const OrderStatus = ({ status }) => {
let color;
let title;
switch (status) {
case 0:
color = 'warning';
title = 'Pending';
break;
case 1:
color = 'success';
title = 'Approved';
break;
case 2:
color = 'error';
title = 'Rejected';
break;
default:
color = 'primary';
title = 'None';
}
return (
<Stack direction="row" spacing={1} alignItems="center">
<Dot color={color} />
<Typography>{title}</Typography>
</Stack>
);
};
OrderStatus.propTypes = {
status: PropTypes.number
};
// ==============================|| ORDER TABLE ||============================== //
export default function OrderTable() {
const [order] = useState('asc');
const [orderBy] = useState('trackingNo');
const [selected] = useState([]);
const isSelected = (trackingNo) => selected.indexOf(trackingNo) !== -1;
return (
<Box>
<TableContainer
sx={{
width: '100%',
overflowX: 'auto',
position: 'relative',
display: 'block',
maxWidth: '100%',
'& td, & th': { whiteSpace: 'nowrap' }
}}
>
<Table
aria-labelledby="tableTitle"
sx={{
'& .MuiTableCell-root:first-child': {
pl: 2
},
'& .MuiTableCell-root:last-child': {
pr: 3
}
}}
>
<OrderTableHead order={order} orderBy={orderBy} />
<TableBody>
{stableSort(rows, getComparator(order, orderBy)).map((row, index) => {
const isItemSelected = isSelected(row.trackingNo);
const labelId = `enhanced-table-checkbox-${index}`;
return (
<TableRow
hover
role="checkbox"
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
aria-checked={isItemSelected}
tabIndex={-1}
key={row.trackingNo}
selected={isItemSelected}
>
<TableCell component="th" id={labelId} scope="row" align="left">
<Link color="secondary" component={RouterLink} to="">
{row.trackingNo}
</Link>
</TableCell>
<TableCell align="left">{row.name}</TableCell>
<TableCell align="right">{row.fat}</TableCell>
<TableCell align="left">
<OrderStatus status={row.carbs} />
</TableCell>
<TableCell align="right">
<NumberFormat value={row.protein} displayType="text" thousandSeparator prefix="$" />
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</Box>
);
}

View file

@ -1,105 +0,0 @@
import { useEffect, useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
// third-party
import ReactApexChart from 'react-apexcharts';
// chart options
const areaChartOptions = {
chart: {
height: 340,
type: 'line',
toolbar: {
show: false
}
},
dataLabels: {
enabled: false
},
stroke: {
curve: 'smooth',
width: 1.5
},
grid: {
strokeDashArray: 4
},
xaxis: {
type: 'datetime',
categories: [
'2018-05-19T00:00:00.000Z',
'2018-06-19T00:00:00.000Z',
'2018-07-19T01:30:00.000Z',
'2018-08-19T02:30:00.000Z',
'2018-09-19T03:30:00.000Z',
'2018-10-19T04:30:00.000Z',
'2018-11-19T05:30:00.000Z',
'2018-12-19T06:30:00.000Z'
],
labels: {
format: 'MMM'
},
axisBorder: {
show: false
},
axisTicks: {
show: false
}
},
yaxis: {
show: false
},
tooltip: {
x: {
format: 'MM'
}
}
};
// ==============================|| REPORT AREA CHART ||============================== //
const ReportAreaChart = () => {
const theme = useTheme();
const { primary, secondary } = theme.palette.text;
const line = theme.palette.divider;
const [options, setOptions] = useState(areaChartOptions);
useEffect(() => {
setOptions((prevState) => ({
...prevState,
colors: [theme.palette.warning.main],
xaxis: {
labels: {
style: {
colors: [secondary, secondary, secondary, secondary, secondary, secondary, secondary, secondary]
}
}
},
grid: {
borderColor: line
},
tooltip: {
theme: 'light'
},
legend: {
labels: {
colors: 'grey.500'
}
}
}));
}, [primary, secondary, line, theme]);
const [series] = useState([
{
name: 'Series 1',
data: [58, 115, 28, 83, 63, 75, 35, 55]
}
]);
return <ReactApexChart options={options} series={series} type="line" height={345} />;
};
export default ReportAreaChart;

View file

@ -1,148 +0,0 @@
import { useEffect, useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
// third-party
import ReactApexChart from 'react-apexcharts';
// chart options
const columnChartOptions = {
chart: {
type: 'bar',
height: 430,
toolbar: {
show: false
}
},
plotOptions: {
bar: {
columnWidth: '30%',
borderRadius: 4
}
},
dataLabels: {
enabled: false
},
stroke: {
show: true,
width: 8,
colors: ['transparent']
},
xaxis: {
categories: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun']
},
yaxis: {
title: {
text: '$ (thousands)'
}
},
fill: {
opacity: 1
},
tooltip: {
y: {
formatter(val) {
return `$ ${val} thousands`;
}
}
},
legend: {
show: true,
fontFamily: `'Public Sans', sans-serif`,
offsetX: 10,
offsetY: 10,
labels: {
useSeriesColors: false
},
markers: {
width: 16,
height: 16,
radius: '50%',
offsexX: 2,
offsexY: 2
},
itemMargin: {
horizontal: 15,
vertical: 50
}
},
responsive: [
{
breakpoint: 600,
options: {
yaxis: {
show: false
}
}
}
]
};
// ==============================|| SALES COLUMN CHART ||============================== //
const SalesColumnChart = () => {
const theme = useTheme();
const { primary, secondary } = theme.palette.text;
const line = theme.palette.divider;
const warning = theme.palette.warning.main;
const primaryMain = theme.palette.primary.main;
const successDark = theme.palette.success.dark;
const [series] = useState([
{
name: 'Net Profit',
data: [180, 90, 135, 114, 120, 145]
},
{
name: 'Revenue',
data: [120, 45, 78, 150, 168, 99]
}
]);
const [options, setOptions] = useState(columnChartOptions);
useEffect(() => {
setOptions((prevState) => ({
...prevState,
colors: [warning, primaryMain],
xaxis: {
labels: {
style: {
colors: [secondary, secondary, secondary, secondary, secondary, secondary]
}
}
},
yaxis: {
labels: {
style: {
colors: [secondary]
}
}
},
grid: {
borderColor: line
},
tooltip: {
theme: 'light'
},
legend: {
position: 'top',
horizontalAlign: 'right',
labels: {
colors: 'grey.500'
}
}
}));
}, [primary, secondary, line, warning, primaryMain, successDark]);
return (
<div id="chart">
<ReactApexChart options={options} series={series} type="bar" height={430} />
</div>
);
};
export default SalesColumnChart;

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