Compare commits

..

No commits in common. "master" and "v0.1.15" have entirely different histories.

284 changed files with 11021 additions and 44565 deletions

View file

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

View file

@ -1,4 +0,0 @@
{
"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."
}

View file

@ -1 +0,0 @@
node_modules

161
.github/dependabot.yml vendored
View file

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

10
.gitignore vendored
View file

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

256
LICENCE
View file

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

BIN
Logo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 KiB

7
build arm64.sh Normal file
View file

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

View file

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

View file

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

View file

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

21
cla.md
View file

@ -1,21 +0,0 @@
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,88 +4,28 @@ import * as React from 'react';
import ThemeCustomization from './themes';
import ScrollTop from './components/ScrollTop';
import Snackbar from '@mui/material/Snackbar';
import {Alert, Box} from '@mui/material';
import logo from './assets/images/icons/cosmos.png';
import {Alert} from '@mui/material';
import * as API from './api';
import { setSnackit, snackit } from './api/wrap';
import { DisconnectOutlined } from '@ant-design/icons';
import { setSnackit } from './api/wrap';
// ==============================|| 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') => {
setSnackit((message) => {
setMessage(message);
setOpen(true);
setSeverity(severity);
})
return statusLoaded ?
<ThemeCustomization PrimaryColor={PrimaryColor} SecondaryColor={SecondaryColor}>
return (
<ThemeCustomization>
<Snackbar
open={open}
autoHideDuration={5000}
onClose={() => {setOpen(false)}}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert className={(open && severity == "error") ? 'shake' : ''} severity={severity} sx={{ width: '100%' }}>
<Alert className={open ? 'shake' : ''} severity="error" sx={{ width: '100%' }}>
{message}
</Alert>
</Snackbar>
@ -93,18 +33,7 @@ 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,31 +0,0 @@
import wrap from './wrap';
function login(values) {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function me() {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
function logout() {
return new Promise((resolve, reject) => {
resolve({
"status": "ok",
})
});
}
export {
login,
logout,
me
};

View file

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

35
client/src/api/config.jsx Normal file
View file

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

View file

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

View file

@ -1,131 +0,0 @@
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

@ -1,110 +0,0 @@
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

@ -1,240 +0,0 @@
{
"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

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

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -1,49 +0,0 @@
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

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

View file

@ -1,237 +1,27 @@
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 * as auth from './authentication.jsx';
import * as users from './users.jsx';
import * as config from './config.jsx';
import * as docker from './docker.jsx';
import wrap from './wrap';
import { redirectToLocal } from '../utils/indexs';
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) => {
const getStatus = () => {
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 = () => {
return fetch('/cosmos/api/status', {
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;
throw e;
});
}
let newInstall = (req, onProgress) => {
if(req.step == '2') {
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
};
return fetch('/cosmos/api/newInstall', requestOptions)
.then(response => {
if (!response.ok) {
throw new Error(response.statusText);
}
// The response body is a ReadableStream. This code reads the stream and passes chunks to the callback.
const reader = response.body.getReader();
// Read the stream and pass chunks to the callback as they arrive
return new ReadableStream({
start(controller) {
function read() {
return reader.read().then(({ done, value }) => {
if (done) {
controller.close();
return;
}
// Decode the UTF-8 text
let text = new TextDecoder().decode(value);
// Split by lines in case there are multiple lines in one chunk
let lines = text.split('\n');
for (let line of lines) {
if (line) {
// Call the progress callback
onProgress(line);
}
}
controller.enqueue(value);
return read();
});
}
return read();
}
});
}).catch((e) => {
console.error(e);
});
} else {
return wrap(fetch('/cosmos/api/newInstall', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
}))
}
}
let checkHost = (host) => {
return fetch('/cosmos/api/dns-check?url=' + host, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (response) => {
let rep;
try {
rep = await response.json();
} catch {
throw new Error('Server error');
}
if (response.status == 200) {
return rep;
}
const e = new Error(rep.message);
e.status = response.status;
e.message = rep.message;
throw e;
});
}
let getDNS = (host) => {
return fetch('/cosmos/api/dns?url=' + host, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (response) => {
let rep;
try {
rep = await response.json();
} catch {
throw new Error('Server error');
}
if (response.status == 200) {
return rep;
}
const e = new Error(rep.message);
e.status = response.status;
e.message = rep.message;
throw e;
});
}
let uploadImage = (file, name) => {
const formData = new FormData();
formData.append('image', file);
return wrap(fetch('/cosmos/api/upload/' + name, {
const newInstall = (req) => {
return wrap(fetch('/cosmos/api/newInstall', {
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;
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(req)
}))
}
export {
@ -239,13 +29,6 @@ export {
users,
config,
docker,
market,
constellation,
getStatus,
newInstall,
isOnline,
checkHost,
getDNS,
metrics,
uploadImage
};

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

@ -1,14 +0,0 @@
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

@ -1,44 +0,0 @@
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

@ -1,170 +0,0 @@
{
"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

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

View file

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

View file

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

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 141 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View file

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

Before

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 245 KiB

View file

@ -15,15 +15,6 @@ 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) => {
@ -32,7 +23,7 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
if (collapse.type && collapse.type === 'collapse') {
getCollapse(collapse);
} else if (collapse.type && collapse.type === 'item') {
if (location.pathname.startsWith(collapse.url)) {
if (location.pathname === collapse.url) {
setMain(menu);
setItem(collapse);
}
@ -91,8 +82,6 @@ const Breadcrumbs = ({ navigation, title, ...others }) => {
</Typography>
{mainContent}
{itemContent}
{subPath && <Typography variant="subtitle1" color="textPrimary">{subPath}</Typography>}
{subItem}
</MuiBreadcrumbs>
</Grid>
{title && (

View file

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

View file

@ -1,94 +0,0 @@
// 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

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

View file

@ -1,49 +0,0 @@
// 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

@ -1,9 +0,0 @@
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

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

View file

@ -1,15 +0,0 @@
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

@ -1,24 +0,0 @@
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

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

View file

@ -1,48 +0,0 @@
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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,62 +0,0 @@
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: '/cosmos-ui',
defaultPath: '/ui',
fontFamily: `'Public Sans', sans-serif`,
i18n: 'en',
miniDrawer: false,

View file

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

View file

@ -1,10 +1,6 @@
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';
@ -21,13 +17,6 @@ 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 ||============================== //
@ -36,10 +25,8 @@ const root = createRoot(container); // createRoot(container!) if you use TypeScr
root.render(
<StrictMode>
<ReduxProvider store={store}>
<BrowserRouter basename="/">
<LocalizationProvider dateAdapter={AdapterDayjs}>
<App />
</LocalizationProvider>
<BrowserRouter basename="/">
<App />
</BrowserRouter>
</ReduxProvider>
</StrictMode>

View file

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

View file

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

View file

@ -9,7 +9,6 @@ import { Avatar, Chip, ListItemButton, ListItemIcon, ListItemText, Typography }
// project import
import { activeItem } from '../../../../../store/reducers/menu';
import { useClientInfos } from '../../../../../utils/hooks';
// ==============================|| NAVIGATION - LIST ITEM ||============================== //
@ -18,23 +17,15 @@ 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}
rel={itemTarget === '_blank' ? 'noopener noreferrer' : ''}
/>) };
let listItemProps = { component: forwardRef((props, ref) => <Link ref={ref} {...props} to={item.url} target={itemTarget} />) };
if (item?.external) {
listItemProps = { component: 'a', href: item.url, target: itemTarget, rel: itemTarget === '_blank' ? 'noopener noreferrer' : '' };
listItemProps = { component: 'a', href: item.url, target: itemTarget };
}
const itemHandler = (id) => {
@ -61,25 +52,6 @@ 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.replace('unstable', '')}
label={version}
size="small"
sx={{ height: 16, '& .MuiChip-label': { fontSize: '0.55rem', py: 0.25 } }}
sx={{ height: 16, '& .MuiChip-label': { fontSize: '0.625rem', py: 0.25 } }}
component="a"
href="/"
clickable

View file

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from 'react';
import { useRef, useState } from 'react';
// material-ui
import { useTheme } from '@mui/material/styles';
@ -19,16 +19,12 @@ 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, ExclamationCircleOutlined, GiftOutlined, InfoCircleOutlined, MessageOutlined, SettingOutlined, WarningOutlined } from '@ant-design/icons';
import * as API from '../../../../api';
import { redirectToLocal } from '../../../../utils/indexs';
import { BellOutlined, CloseOutlined, GiftOutlined, MessageOutlined, SettingOutlined } from '@ant-design/icons';
// sx styles
const avatarSX = {
@ -52,55 +48,10 @@ 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);
};
@ -108,48 +59,12 @@ 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
@ -162,7 +77,7 @@ const Notification = () => {
aria-haspopup="true"
onClick={handleToggle}
>
<Badge badgeContent={nbUnread} color="error">
<Badge badgeContent={4} color="primary">
<BellOutlined />
</Badge>
</IconButton>
@ -212,8 +127,6 @@ const Notification = () => {
<List
component="nav"
sx={{
maxHeight: 350,
overflow: 'auto',
p: 0,
'& .MuiListItemButton-root': {
py: 0.5,
@ -222,43 +135,127 @@ const Notification = () => {
}
}}
>
{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)}
<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>
</ListItemSecondaryAction>
</ListItemButton>
<Divider /></>))}
{/* <ListItemButton sx={{ textAlign: 'center', py: `${12}px !important` }}>
}
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.
</Typography>
</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` }}>
<ListItemText
primary={
<Typography variant="h6" color="primary">
@ -266,7 +263,7 @@ const Notification = () => {
</Typography>
}
/>
</ListItemButton> */}
</ListItemButton>
</List>
</MainCard>
</ClickAwayListener>

View file

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

View file

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

View file

@ -1,5 +1,5 @@
// assets
import { HomeOutlined, AppstoreOutlined, DashboardOutlined, AppstoreAddOutlined } from '@ant-design/icons';
import { HomeOutlined, AppstoreOutlined } from '@ant-design/icons';
// icons
const icons = {
@ -17,26 +17,16 @@ const dashboard = {
id: 'home',
title: 'Home',
type: 'item',
url: '/cosmos-ui/',
url: '/ui',
icon: icons.HomeOutlined,
breadcrumbs: false
},
{
id: 'dashboard',
title: 'Monitoring',
id: 'servapps',
title: 'ServApps',
type: 'item',
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
url: '/ui/servapps',
icon: AppstoreOutlined
},
]
};

View file

@ -1,6 +1,5 @@
// assets
import { ProfileOutlined, PicLeftOutlined, SettingOutlined, NodeExpandOutlined, AppstoreOutlined} from '@ant-design/icons';
import ConstellationIcon from '../assets/images/icons/constellation.png'
import { ProfileOutlined, SettingOutlined, NodeExpandOutlined} from '@ant-design/icons';
// icons
const icons = {
@ -8,6 +7,7 @@ const icons = {
ProfileOutlined,
SettingOutlined
};
// ==============================|| MENU ITEMS - EXTRA PAGES ||============================== //
const pages = {
@ -15,50 +15,25 @@ const pages = {
title: 'Management',
type: 'group',
children: [
{
id: 'servapps',
title: 'ServApps',
type: 'item',
url: '/cosmos-ui/servapps',
icon: AppstoreOutlined,
adminOnly: true
},
{
id: 'url',
title: 'URLs',
type: 'item',
url: '/cosmos-ui/config-url',
url: '/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',
title: 'Manage Users',
type: 'item',
url: '/cosmos-ui/config-users',
url: '/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: '/cosmos-ui/config-general',
url: '/ui/config-general',
icon: icons.SettingOutlined,
}
]

View file

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

View file

@ -7,15 +7,10 @@ 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,7 +9,6 @@ import AuthWrapper from './AuthWrapper';
import { useEffect } from 'react';
import * as API from '../../api';
import { redirectTo, redirectToLocal } from '../../utils/indexs';
// ================================|| REGISTER ||================================ //
@ -18,7 +17,7 @@ const Logout = () => {
API.auth.logout()
.then(() => {
setTimeout(() => {
redirectToLocal('/cosmos-ui/login');
window.location.href = '/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="/cosmos-ui/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
<Typography component={Link} to="/ui/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
Already have an account?
</Typography>
</Stack>

View file

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

View file

@ -32,8 +32,6 @@ 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 ||============================ //
@ -74,8 +72,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])(?=.*[!@#$%^&*])(?=.{8,})/,
'Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and one special case Character'
),
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
@ -86,7 +84,7 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
}).then((res) => {
setStatus({ success: true });
setSubmitting(false);
redirectToLocal('/cosmos-ui/login');
window.location.href = '/ui/login';
}).catch((err) => {
setStatus({ success: false });
setErrors({ submit: err.message });
@ -180,9 +178,10 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
</Grid>
)}
<Grid item xs={12}>
<LoadingButton
<AnimateButton>
<Button
disableElevation
loading={isSubmitting}
disabled={isSubmitting}
fullWidth
size="large"
type="submit"
@ -192,7 +191,8 @@ const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
{
isRegister ? 'Register' : 'Reset Password'
}
</LoadingButton>
</Button>
</AnimateButton>
</Grid>
</Grid>
</form>

View file

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

View file

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

View file

@ -1,113 +0,0 @@
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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -33,15 +33,17 @@ import defaultport from '../../servapps/defaultport.json';
import * as API from '../../../api';
export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetContainer, onTargetChange, label = "Container Name", name = "Target"}) {
export function CosmosContainerPicker({formik, lockTarget, TargetContainer, onTargetChange}) {
const [open, setOpen] = React.useState(false);
const [containers, setContainers] = React.useState([]);
const [hasPublicPorts, setHasPublicPorts] = React.useState(false);
const [isOnBridge, setIsOnBridge] = React.useState(false);
const [options, setOptions] = React.useState(null);
const [portsOptions, setPortsOptions] = React.useState(null);
const [portsOptions, setPortsOptions] = React.useState([]);
const loading = options === null;
const name = "Target"
const label = "Container Name"
let targetResult = {
container: 'null',
port: "",
@ -76,37 +78,35 @@ export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetConta
})
setPortsOptions(portsTemp)
if(targetResult.port == '') {
targetResult.port = '80'
if(portsTemp.length > 0) {
// pick best default port
// set default to first port
targetResult.port = portsTemp[0]
// first, check if a manual override exists
let override = Object.keys(defaultport.overrides).find((key) => {
let keyMatch = new RegExp(key, "i");
return newContainer.Image.match(keyMatch) && portsTemp.includes(defaultport.overrides[key])
});
if(override) {
targetResult.port = defaultport.overrides[override]
} else {
// if not, check the default list of common ports
let priorityList = defaultport.priority;
priorityList.find((_portReg) => {
return portsTemp.find((portb) => {
let portReg = new RegExp(_portReg, "i");
if(portb.toString().match(portReg)) {
targetResult.port = portb
return true;
}
})
targetResult.port = '80'
if(portsTemp.length > 0) {
// pick best default port
// set default to first port
targetResult.port = portsTemp[0]
// first, check if a manual override exists
let override = Object.keys(defaultport.overrides).find((key) => {
let keyMatch = new RegExp(key, "i");
return newContainer.Image.match(keyMatch) && portsTemp.includes(defaultport.overrides[key])
});
if(override) {
targetResult.port = defaultport.overrides[override]
} else {
// if not, check the default list of common ports
let priorityList = defaultport.priority;
priorityList.find((_portReg) => {
return portsTemp.find((portb) => {
let portReg = new RegExp(_portReg, "i");
if(portb.toString().match(portReg)) {
targetResult.port = portb
return true;
}
})
}
})
}
}
formik.setFieldValue(name, getTarget());
if(newContainer.NetworkSettings.Networks["bridge"]) {
@ -151,9 +151,8 @@ export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetConta
if (active) {
setOptions([...names]);
}
if (targetResult.container !== 'null') {
postContainerChange(res.data.find((container) => container.Names[0] === targetResult.container) || targetResult.containerObject)
postContainerChange(res.data.find((container) => container.Names[0] === targetResult.container))
}
})();
@ -171,7 +170,7 @@ export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetConta
const newTarget = formik.values[name];
React.useEffect(() => {
if(onTargetChange) {
onTargetChange(newTarget, targetResult.container.replace("/", ""), targetResult)
onTargetChange(newTarget)
}
}, [newTarget])
@ -217,63 +216,62 @@ export function CosmosContainerPicker({formik, nameOnly, lockTarget, TargetConta
/>
)}
/>}
{!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>}
<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())
}}
/>
{(portsOptions.length > 0) ? (<>
<InputLabel htmlFor={name + "-port"}>Container Port</InputLabel>
<Autocomplete
className="px-2 my-2"
variant="outlined"
name={name + "-port"}
id={name + "-port"}
value={targetResult.port}
options={portsOptions.map((option) => (option))}
placeholder='Select a port'
freeSolo
filterOptions={(x) => x} // disable filtering
getOptionLabel={(option) => '' + option}
isOptionEqualToValue={(option, value) => {
return ('' + option) === value
}}
onChange={(event, newValue) => {
targetResult.port = newValue
formik.setFieldValue(name, getTarget())
}}
renderInput={(params) => <TextField {...params} />}
/>
{targetResult.port == '' && targetResult.port == 0 && <FormHelperText error id="standard-weight-helper-text-name-login">
Please select a port
</FormHelperText>}
</>) : ''}
{(portsOptions.length > 0) ? (<>
<InputLabel htmlFor={name + "-protocol"}>Container Protocol (use HTTP if unsure)</InputLabel>
<TextField
type="text"
name={name + "-protocol"}
defaultValue={targetResult.protocol}
onChange={(event) => {
targetResult.protocol = event.target.value && event.target.value.toLowerCase()
formik.setFieldValue(name, getTarget())
}}
/>
</>) : ''}
<InputLabel htmlFor={name}>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,46 +27,23 @@ import { strengthColor, strengthIndicator } from '../../../utils/password-streng
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
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 }) => {
export const CosmosInputText = ({ name, style, multiline, type, placeholder, onChange, label, formik }) => {
return <Grid item xs={12}>
<Stack spacing={1} style={style}>
{label && <InputLabel htmlFor={name}>{label}</InputLabel>}
<InputLabel htmlFor={name}>{label}</InputLabel>
<OutlinedInput
id={name}
type={type ? type : 'text'}
value={value || (formik && getNestedValue(formik.values, name))}
value={formik.values[name]}
name={name}
multiline={multiline}
onBlur={(...ar) => {
return formik && formik.handleBlur(...ar);
}}
onChange={(...ar) => {
onChange && onChange(...ar);
return formik && formik.handleChange(...ar);
}}
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder={placeholder}
fullWidth
error={Boolean(formik && formik.touched[name] && formik.errors[name])}
error={Boolean(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 && (
{formik.touched[name] && formik.errors[name] && (
<FormHelperText error id="standard-weight-helper-text-name-login">
{formik.errors[name]}
</FormHelperText>
@ -75,7 +52,7 @@ export const CosmosInputText = ({ name, style, value, errors, multiline, type, p
</Grid>
}
export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoComplete, onChange, label, formik }) => {
export const CosmosInputPassword = ({ name, type, placeholder, onChange, label, formik }) => {
const [level, setLevel] = React.useState();
const [showPassword, setShowPassword] = React.useState(false);
const handleClickShowPassword = () => {
@ -101,9 +78,8 @@ export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoC
<OutlinedInput
id={name}
type={showPassword ? 'text' : 'password'}
value={getNestedValue(formik.values, name)}
value={formik.values[name]}
name={name}
autoComplete={autoComplete}
onBlur={formik.handleBlur}
onChange={(e) => {
changePassword(e.target.value);
@ -132,7 +108,7 @@ export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoC
</FormHelperText>
)}
{!noStrength && <FormControl fullWidth sx={{ mt: 2 }}>
<FormControl fullWidth sx={{ mt: 2 }}>
<Grid container spacing={2} alignItems="center">
<Grid item>
<Box sx={{ bgcolor: level?.color, width: 85, height: 8, borderRadius: '7px' }} />
@ -143,12 +119,12 @@ export const CosmosInputPassword = ({ name, noStrength, type, placeholder, autoC
</Typography>
</Grid>
</Grid>
</FormControl>}
</FormControl>
</Stack>
</Grid>
}
export const CosmosSelect = ({ name, onChange, label, formik, disabled, options, style }) => {
export const CosmosSelect = ({ name, label, formik, disabled, options }) => {
return <Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor={name}>{label}</InputLabel>
@ -159,11 +135,8 @@ export const CosmosSelect = ({ name, onChange, label, formik, disabled, options,
id={name}
disabled={disabled}
select
value={getNestedValue(formik.values, name)}
onChange={(...ar) => {
onChange && onChange(...ar);
formik.handleChange(...ar);
}}
value={formik.values[name]}
onChange={formik.handleChange}
error={
formik.touched[name] &&
Boolean(formik.errors[name])
@ -171,7 +144,6 @@ export const CosmosSelect = ({ name, onChange, label, formik, disabled, options,
helperText={
formik.touched[name] && formik.errors[name]
}
style={style}
>
{options.map((option) => (
<MenuItem key={option[0]} value={option[0]}>
@ -213,9 +185,8 @@ export const CosmosCollapse = ({ children, title }) => {
aria-controls="panel1a-content"
id="panel1a-header"
>
<Typography variant="h6" style={{width: '100%', marginRight: '20px'}}>
{title}
</Typography>
<Typography variant="h6">
{title}</Typography>
</AccordionSummary>
<AccordionDetails>
{children}
@ -228,7 +199,7 @@ export const CosmosCollapse = ({ children, title }) => {
export function CosmosFormDivider({title}) {
return <Grid item xs={12}>
<Divider>
{title && <Chip label={title} />}
<Chip label={title} />
</Divider>
</Grid>
}

View file

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

View file

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

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