[release] v0.8.0-unstable

This commit is contained in:
Yann Stepienik 2023-06-20 18:34:06 +01:00
parent 6f1d395df3
commit 5288f0d954
28 changed files with 810 additions and 74 deletions

View file

@ -1,3 +1,7 @@
## Version 0.8.0
- Stop showing Docker not connected when first loading status
- Add privacy settings to external links
## Version 0.7.1 -> 0.7.10 ## Version 0.7.1 -> 0.7.10
- Fix issue where multiple DBs get created at the setup - Fix issue where multiple DBs get created at the setup
- Add more special characters to be used for password validation - Add more special characters to be used for password validation

View file

@ -4,23 +4,68 @@ import * as React from 'react';
import ThemeCustomization from './themes'; import ThemeCustomization from './themes';
import ScrollTop from './components/ScrollTop'; import ScrollTop from './components/ScrollTop';
import Snackbar from '@mui/material/Snackbar'; import Snackbar from '@mui/material/Snackbar';
import {Alert} from '@mui/material'; import {Alert, Box} from '@mui/material';
import logo from './assets/images/icons/cosmos.png';
import * as API from './api';
import { setSnackit } from './api/wrap'; import { setSnackit } from './api/wrap';
// ==============================|| APP - THEME, ROUTER, LOCAL ||============================== // // ==============================|| 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 App = () => {
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [message, setMessage] = React.useState(''); const [message, setMessage] = React.useState('');
const [severity, setSeverity] = React.useState('error'); 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);
SetPrimaryColor = (color) => {
setPrimaryColor(color);
GlobalPrimaryColor = color;
}
SetSecondaryColor = (color) => {
setSecondaryColor(color);
GlobalSecondaryColor = color;
}
React.useEffect(() => {
API.getStatus(true).then((r) => {
if(r) {
setStatusLoaded(true);
}
setPrimaryColor(API.PRIMARY_COLOR);
setSecondaryColor(API.SECONDARY_COLOR);
}).catch(() => {
setStatusLoaded(true);
setPrimaryColor(API.PRIMARY_COLOR);
setSecondaryColor(API.SECONDARY_COLOR);
});
}, []);
setSnackit((message, severity='error') => { setSnackit((message, severity='error') => {
setMessage(message); setMessage(message);
setOpen(true); setOpen(true);
setSeverity(severity); setSeverity(severity);
}) })
return (
<ThemeCustomization> return statusLoaded ?
<ThemeCustomization PrimaryColor={PrimaryColor} SecondaryColor={SecondaryColor}>
<Snackbar <Snackbar
open={open} open={open}
autoHideDuration={5000} autoHideDuration={5000}
@ -35,7 +80,14 @@ const App = () => {
<Routes /> <Routes />
</ScrollTop> </ScrollTop>
</ThemeCustomization> </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' /> */}
<LoadingAnimation />
</Box>
</div>
} }
export default App; export default App;

View file

@ -60,4 +60,14 @@ export const checkHost = (host) => {
100 100
); );
}); });
} }
export const uploadBackground = (file) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
"status": "ok",
"data": ""
})}, 100 );
});
}

View file

@ -16,17 +16,52 @@ import wrap from './wrap';
export let CPU_ARCH = 'amd64'; export let CPU_ARCH = 'amd64';
export let CPU_AVX = true; export let CPU_AVX = true;
let getStatus = () => { export let HOME_BACKGROUND;
export let PRIMARY_COLOR;
export let SECONDARY_COLOR;
export let FIRST_LOAD = false;
let getStatus = (initial) => {
return wrap(fetch('/cosmos/api/status', { return wrap(fetch('/cosmos/api/status', {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
})) }), initial)
.then(async (response) => { .then(async (response) => {
CPU_ARCH = response.data.CPU; CPU_ARCH = response.data.CPU;
CPU_AVX = response.data.AVX; 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 return response
}).catch((response) => {
const urlSearch = encodeURIComponent(window.location.search);
const redirectTo = (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') {
window.location.href = '/cosmos-ui/newInstall';
} else if (response.status == 'error' && response.code == "HTTP004") {
window.location.href = '/cosmos-ui/login?redirect=' + redirectTo;
} else if (response.status == 'error' && response.code == "HTTP006") {
window.location.href = '/cosmos-ui/loginmfa?redirect=' + redirectTo;
} else if (response.status == 'error' && response.code == "HTTP007") {
window.location.href = '/cosmos-ui/newmfa?redirect=' + redirectTo;
}
} else {
return "nothing";
}
}
}); });
} }
@ -157,6 +192,15 @@ let getDNS = (host) => {
}); });
} }
let uploadBackground = (file) => {
const formData = new FormData();
formData.append('background', file);
return wrap(fetch('/cosmos/api/background', {
method: 'POST',
body: formData
}));
};
const isDemo = import.meta.env.MODE === 'demo'; const isDemo = import.meta.env.MODE === 'demo';
let auth = _auth; let auth = _auth;
@ -176,6 +220,7 @@ if(isDemo) {
isOnline = indexDemo.isOnline; isOnline = indexDemo.isOnline;
checkHost = indexDemo.checkHost; checkHost = indexDemo.checkHost;
getDNS = indexDemo.getDNS; getDNS = indexDemo.getDNS;
uploadBackground = indexDemo.uploadBackground;
} }
export { export {
@ -188,5 +233,6 @@ export {
newInstall, newInstall,
isOnline, isOnline,
checkHost, checkHost,
getDNS getDNS,
uploadBackground
}; };

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

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

View file

@ -2,7 +2,7 @@ import React from 'react';
import { Button } from '@mui/material'; import { Button } from '@mui/material';
import { UploadOutlined } from '@ant-design/icons'; import { UploadOutlined } from '@ant-design/icons';
export default function UploadButtons({OnChange, accept}) { export default function UploadButtons({OnChange, accept, label}) {
return ( return (
<div> <div>
<input <input
@ -15,7 +15,7 @@ export default function UploadButtons({OnChange, accept}) {
/> />
<label htmlFor="contained-button-file"> <label htmlFor="contained-button-file">
<Button variant="contained" component="span" startIcon={<UploadOutlined />}> <Button variant="contained" component="span" startIcon={<UploadOutlined />}>
Upload {label || 'Upload'}
</Button> </Button>
</label> </label>
</div> </div>

View file

@ -21,6 +21,58 @@
} }
@keyframes pulsing {
0% { -webkit-transform: scale(1); }
50% { -webkit-transform: scale(1.1); }
100% { -webkit-transform: scale(1); }
}
@keyframes bounce {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1.0);
}
}
.dot {
background-color: #222;
border-radius: 100%;
width: 1.5rem;
height: 1.5rem;
margin: 0 0.25rem;
/* Animation */
animation: bounce 1.4s infinite;
animation-fill-mode: both;
}
@media (prefers-color-scheme: dark) {
.dot {
background-color: #eee;
}
}
.dot:nth-child(1) {
animation-delay: -0.32s;
}
.dot:nth-child(2) {
animation-delay: -0.16s;
}
.dot:nth-child(3) {
animation-delay: 0s;
}
.loader {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
.shinyButton { .shinyButton {
overflow: hidden; overflow: hidden;
} }
@ -87,4 +139,8 @@
.raw-table table th, .raw-table table td { .raw-table table th, .raw-table table td {
padding: 5px; padding: 5px;
border: 1px solid #ccc; border: 1px solid #ccc;
}
.pulsing {
animation: pulsing 2s ease-in-out infinite;
} }

View file

@ -23,7 +23,9 @@ const NavItem = ({ item, level }) => {
itemTarget = '_blank'; itemTarget = '_blank';
} }
let listItemProps = { component: forwardRef((props, ref) => <Link ref={ref} {...props} to={item.url} target={itemTarget} />) }; let listItemProps = { component: forwardRef((props, ref) => <Link ref={ref} {...props} to={item.url} target={itemTarget}
rel={itemTarget === '_blank' ? 'noopener noreferrer' : ''}
/>) };
if (item?.external) { if (item?.external) {
listItemProps = { component: 'a', href: item.url, target: itemTarget }; listItemProps = { component: 'a', href: item.url, target: itemTarget };
} }

View file

@ -22,6 +22,7 @@ import {
Collapse, Collapse,
TextField, TextField,
MenuItem, MenuItem,
Skeleton,
} from '@mui/material'; } from '@mui/material';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
@ -31,11 +32,17 @@ import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOut
import { CosmosCheckbox, CosmosFormDivider, CosmosInputPassword, CosmosInputText, CosmosSelect } from './formShortcuts'; import { CosmosCheckbox, CosmosFormDivider, CosmosInputPassword, CosmosInputText, CosmosSelect } from './formShortcuts';
import CountrySelect, { countries } from '../../../components/countrySelect'; import CountrySelect, { countries } from '../../../components/countrySelect';
import { DnsChallengeComp } from '../../../utils/dns-challenge-comp'; import { DnsChallengeComp } from '../../../utils/dns-challenge-comp';
import { values } from 'lodash';
import UploadButtons from '../../../components/fileUpload';
import { TwitterPicker
} from 'react-color';
import {SetPrimaryColor, SetSecondaryColor} from '../../../App';
const ConfigManagement = () => { const ConfigManagement = () => {
const [config, setConfig] = React.useState(null); const [config, setConfig] = React.useState(null);
const [openModal, setOpenModal] = React.useState(false); const [openModal, setOpenModal] = React.useState(false);
const [uploadingBackground, setUploadingBackground] = React.useState(false);
function refresh() { function refresh() {
API.config.get().then((res) => { API.config.get().then((res) => {
@ -82,7 +89,11 @@ const ConfigManagement = () => {
Email_UseTLS : config.EmailConfig.UseTLS, Email_UseTLS : config.EmailConfig.UseTLS,
SkipPruneNetwork: config.DockerConfig.SkipPruneNetwork, SkipPruneNetwork: config.DockerConfig.SkipPruneNetwork,
DefaultDataPath: config.DockerConfig.DefaultDataPath || "/usr" DefaultDataPath: config.DockerConfig.DefaultDataPath || "/usr",
Background: config && config.HomepageConfig && config.HomepageConfig.Background,
PrimaryColor: config && config.ThemeConfig && config.ThemeConfig.PrimaryColor,
SecondaryColor: config && config.ThemeConfig && config.ThemeConfig.SecondaryColor,
}} }}
validationSchema={Yup.object().shape({ validationSchema={Yup.object().shape({
Hostname: Yup.string().max(255).required('Hostname is required'), Hostname: Yup.string().max(255).required('Hostname is required'),
@ -124,7 +135,16 @@ const ConfigManagement = () => {
...config.DockerConfig, ...config.DockerConfig,
SkipPruneNetwork: values.SkipPruneNetwork, SkipPruneNetwork: values.SkipPruneNetwork,
DefaultDataPath: values.DefaultDataPath DefaultDataPath: values.DefaultDataPath
} },
HomepageConfig: {
...config.HomepageConfig,
Background: values.Background
},
ThemeConfig: {
...config.ThemeConfig,
PrimaryColor: values.PrimaryColor,
SecondaryColor: values.SecondaryColor
},
} }
API.config.set(toSave).then((data) => { API.config.set(toSave).then((data) => {
@ -153,6 +173,29 @@ const ConfigManagement = () => {
{(formik) => ( {(formik) => (
<form noValidate onSubmit={formik.handleSubmit}> <form noValidate onSubmit={formik.handleSubmit}>
<Stack spacing={3}> <Stack spacing={3}>
<MainCard>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button
disableElevation
disabled={formik.isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
Save
</Button>
</AnimateButton>
</Grid>
</MainCard>
<MainCard title="General"> <MainCard title="General">
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12}> <Grid item xs={12}>
@ -231,6 +274,64 @@ const ConfigManagement = () => {
</Grid> </Grid>
</Grid> </Grid>
</MainCard> </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) => {
const file = e.target.files[0];
API.uploadBackground(file).then((data) => {
formik.setFieldValue('Background', "/cosmos/api/background/" + data.data.extension.replace(".", ""));
});
}}
/>
<Button
variant="outlined"
onClick={() => {
formik.setFieldValue('Background', "");
}}
>
Reset Wallpaper
</Button>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel style={{marginBottom: '10px'}} htmlFor="PrimaryColor">Primary Color</InputLabel>
<TwitterPicker
id="PrimaryColor"
color={formik.values.PrimaryColor}
onChange={color => {
let colorRGB = `rgb(${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>
<TwitterPicker
id="SecondaryColor"
color={formik.values.SecondaryColor}
onChange={color => formik.setFieldValue('SecondaryColor', color.rgb)}
/>
</Stack>
</Grid>
</Grid>
</MainCard>
<MainCard title="HTTP"> <MainCard title="HTTP">
<Grid container spacing={3}> <Grid container spacing={3}>

View file

@ -1,6 +1,6 @@
import { useParams } from "react-router"; import { useParams } from "react-router";
import Back from "../../components/back"; import Back from "../../components/back";
import { Alert, Box, CircularProgress, Grid, Stack, useTheme } from "@mui/material"; import { Alert, Box, CircularProgress, Grid, Stack, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import * as API from "../../api"; import * as API from "../../api";
import wallpaper from '../../assets/images/wallpaper2.jpg'; import wallpaper from '../../assets/images/wallpaper2.jpg';
@ -10,19 +10,23 @@ import { getFaviconURL } from "../../utils/routes";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { getFullOrigin } from "../../utils/routes"; import { getFullOrigin } from "../../utils/routes";
import IsLoggedIn from "../../isLoggedIn"; import IsLoggedIn from "../../isLoggedIn";
import { ServAppIcon } from "../../utils/servapp-icon";
import Chart from 'react-apexcharts';
export const HomeBackground = () => { export const HomeBackground = () => {
const theme = useTheme(); const theme = useTheme();
const isDark = theme.palette.mode === 'dark'; const isDark = theme.palette.mode === 'dark';
const customPaper = API.HOME_BACKGROUND;
return ( return (
<Box sx={{ position: 'fixed', float: 'left', overflow: 'hidden', zIndex: 0, top: 0, left: 0, right: 0, bottom: 0, <Box sx={{
position: 'fixed', float: 'left', overflow: 'hidden', zIndex: 0, top: 0, left: 0, right: 0, bottom: 0,
// gradient // gradient
// backgroundImage: isDark ? // backgroundImage: isDark ?
// `linear-gradient(#371d53, #26143a)` : // `linear-gradient(#371d53, #26143a)` :
// `linear-gradient(#e6d3fb, #c8b0e2)`, // `linear-gradient(#e6d3fb, #c8b0e2)`,
}}> }}>
<img src={isDark ? wallpaper : wallpaperLight } style={{ display: 'inline' }} alt="" draggable="false" width="100%" height="100%" /> <img src={customPaper ? customPaper : (isDark ? wallpaper : wallpaperLight)} style={{ display: 'inline' }} alt="" draggable="false" width="100%" height="100%" />
</Box> </Box>
); );
}; };
@ -31,15 +35,16 @@ export const TransparentHeader = () => {
const theme = useTheme(); const theme = useTheme();
const isDark = theme.palette.mode === 'dark'; const isDark = theme.palette.mode === 'dark';
const backColor = isDark ? '0,0,0' : '255,255,255'; const backColor = isDark ? '0,0,0' : '255,255,255';
const textColor = isDark ? 'white' : 'dark'; const textColor = isDark ? 'white' : 'dark';
return <style> return <style>
{`header { {`header {
background: rgba(${backColor},0.35) !important; background: rgba(${backColor}, 0.40) !important;
border-bottom-color: rgba(${backColor},0.4) !important; border-bottom-color: rgba(${backColor},0.45) !important;
color: ${textColor} !important; color: ${textColor} !important;
font-weight: bold; font-weight: bold;
backdrop-filter: blur(15px);
} }
header .MuiChip-label { header .MuiChip-label {
@ -52,18 +57,20 @@ export const TransparentHeader = () => {
} }
.app { .app {
backdrop-filter: blur(15px);
transition: background 0.1s ease-in-out; transition: background 0.1s ease-in-out;
transition: transform 0.1s ease-in-out; transition: transform 0.1s ease-in-out;
} }
.app:hover { .app-hover:hover {
cursor: pointer; cursor: pointer;
background: rgba(${backColor},0.8) !important; background: rgba(${backColor},0.8) !important;
transform: scale(1.05); transform: scale(1.05);
} }
.MuiAlert-standard { .MuiAlert-standard {
background: rgba(${backColor},0.35) !important; backdrop-filter: blur(15px);
background: rgba(${backColor},0.40) !important;
color: ${textColor} !important; color: ${textColor} !important;
font-weight: bold; font-weight: bold;
} }
@ -79,6 +86,7 @@ const HomePage = () => {
const [containers, setContainers] = useState(null); const [containers, setContainers] = useState(null);
const theme = useTheme(); const theme = useTheme();
const isDark = theme.palette.mode === 'dark'; const isDark = theme.palette.mode === 'dark';
const isMd = useMediaQuery(theme.breakpoints.up('md'));
const blockStyle = { const blockStyle = {
margin: 0, margin: 0,
@ -88,12 +96,12 @@ const HomePage = () => {
verticalAlign: 'middle', verticalAlign: 'middle',
} }
const appColor = isDark ? { const appColor = isDark ? {
color: 'white', color: 'white',
background: 'rgba(0,0,0,0.35)', background: 'rgba(0,0,0,0.40)',
} : { } : {
color: 'black', color: 'black',
background: 'rgba(255,255,255,0.35)', background: 'rgba(255,255,255,0.40)',
} }
@ -112,17 +120,97 @@ const HomePage = () => {
}); });
}; };
let routes = config && (config.HTTPConfig.ProxyConfig.Routes || []); let routes = config && (config.HTTPConfig.ProxyConfig.Routes || []);
useEffect(() => { useEffect(() => {
refreshConfig(); refreshConfig();
refreshStatus(); refreshStatus();
// const interval = setInterval(() => {
// refreshStatus();
// }, 5000);
// return () => clearInterval(interval);
}, []); }, []);
const optionsRadial = {
plotOptions: {
radialBar: {
startAngle: -135,
endAngle: 225,
hollow: {
margin: 0,
size: "70%",
background: "#fff",
image: undefined,
imageOffsetX: 0,
imageOffsetY: 0,
position: "front",
dropShadow: {
enabled: true,
top: 3,
left: 0,
blur: 4,
opacity: 0.24
}
},
track: {
background: "#fff",
strokeWidth: "67%",
margin: 0, // margin is in pixels
dropShadow: {
enabled: true,
top: -3,
left: 0,
blur: 4,
opacity: 0.35
}
},
dataLabels: {
showOn: "always",
name: {
show: false,
color: "#888",
fontSize: "13px"
},
value: {
formatter: function (val) {
return val + "%";
},
color: "#111",
offsetY: 9,
fontSize: "24px",
show: true
}
}
}
},
fill: {
type: "gradient",
gradient: {
shade: "dark",
type: "horizontal",
shadeIntensity: 0.5,
gradientToColors: ["#ABE5A1"],
inverseColors: true,
opacityFrom: 1,
opacityTo: 1,
stops: [0, 100]
}
},
stroke: {
lineCap: "round"
},
labels: []
};
return <Stack spacing={2} > return <Stack spacing={2} >
<IsLoggedIn /> <IsLoggedIn />
<HomeBackground /> <HomeBackground status={coStatus} />
<TransparentHeader /> <TransparentHeader />
<Stack style={{ zIndex: 2 }} spacing={1}> <Stack style={{ zIndex: 2 }} spacing={1}>
{coStatus && !coStatus.database && ( {coStatus && !coStatus.database && (
@ -166,25 +254,93 @@ const HomePage = () => {
</Stack> </Stack>
<Grid2 container spacing={2} style={{ zIndex: 2 }}> <Grid2 container spacing={2} style={{ zIndex: 2 }}>
{/* {(!isMd || !coStatus || !coStatus.resources || !coStatus.resources.cpu ) && (<>
<Grid2 item xs={12} sm={6} md={4} lg={3} xl={3} xxl={3} key={'000'}>
<Box className='app' style={{ padding: '20px', height: "107px", borderRadius: 5, ...appColor }}>
<Stack style={{flexGrow: 1}} spacing={0}>
<div style={{fontSize: '18px', fontWeight: "bold"}}>CPU</div>
<div>--</div>
<div>--</div>
</Stack>
</Box>
</Grid2>
<Grid2 item xs={12} sm={6} md={4} lg={3} xl={3} xxl={3} key={'001'}>
<Box className='app' style={{ padding: '20px', height: "107px", borderRadius: 5, ...appColor }}>
<Stack style={{flexGrow: 1}} spacing={0}>
<div style={{fontSize: '18px', fontWeight: "bold"}}>RAM</div>
<div>Available: --GB</div>
<div>Used: --GB</div>
</Stack>
</Box>
</Grid2>
</>)}
{isMd && coStatus && coStatus.resources.cpu && (<>
<Grid2 item xs={12} sm={6} md={4} lg={3} xl={3} xxl={3} key={'000'}>
<Box className='app' style={{height: '106px', borderRadius: 5, ...appColor }}>
<Stack direction="row" justifyContent={'center'} alignItems={'center'} style={{ height: "100%" }}>
<div style={{width: '110px', height: '97px'}}>
<Chart
options={optionsRadial}
series={[parseInt(coStatus.resources.cpu)]}
type="radialBar"
height={120}
width={120}
/>
</div>
<Stack style={{flexGrow: 1}} spacing={0}>
<div style={{fontSize: '18px', fontWeight: "bold"}}>CPU</div>
<div>{coStatus.CPU}</div>
<div>{coStatus.AVX ? "AVX Supported" : "No AVX Support"}</div>
</Stack>
</Stack>
</Box>
</Grid2>
<Grid2 item xs={12} sm={6} md={4} lg={3} xl={3} xxl={3} key={'001'}>
<Box className='app' style={{height: '106px',borderRadius: 5, ...appColor }}>
<Stack direction="row" justifyContent={'center'} alignItems={'center'} style={{ height: "100%" }}>
<div style={{width: '110px', height: '97px'}}>
<Chart
options={optionsRadial}
series={[parseInt(
coStatus.resources.ram / (coStatus.resources.ram + coStatus.resources.ramFree) * 100
)]}
type="radialBar"
height={120}
width={120}
/>
</div>
<Stack style={{flexGrow: 1}} spacing={0}>
<div style={{fontSize: '18px', fontWeight: "bold"}}>RAM</div>
<div>Available: {Math.ceil((coStatus.resources.ram + coStatus.resources.ramFree) / 1024 / 1024 / 1024)}GB</div>
<div>Used: {Math.ceil(coStatus.resources.ram / 1024 / 1024 / 1024)}GB</div>
</Stack>
</Stack>
</Box>
</Grid2>
</>)} */}
{config && servApps && routes.map((route) => { {config && servApps && routes.map((route) => {
let skip = route.Mode == "REDIRECT"; let skip = route.Mode == "REDIRECT";
if(route.Mode == "SERVAPP") { let containerName;
const containerName = route.Target.split(':')[1].slice(2); let container;
const container = servApps.find((c) => c.Names.includes('/' + containerName)); if (route.Mode == "SERVAPP") {
if(!container || container.State != "running") { containerName = route.Target.split(':')[1].slice(2);
container = servApps.find((c) => c.Names.includes('/' + containerName));
if (!container || container.State != "running") {
skip = true skip = true
} }
} }
return !skip && <Grid2 item xs={12} sm={6} md={4} lg={3} xl={3} xxl={2} key={route.Name}> return !skip && <Grid2 item xs={12} sm={6} md={4} lg={3} xl={3} xxl={3} key={route.Name}>
<Box className='app' style={{ padding: 10, borderRadius: 5, ...appColor }}> <Box className='app app-hover' style={{ padding: 18, borderRadius: 5, ...appColor }}>
<Link to={getFullOrigin(route)} target="_blank" style={{ textDecoration: 'none', ...appColor }}> <Link to={getFullOrigin(route)} target="_blank" style={{ textDecoration: 'none', ...appColor }}>
<Stack direction="row" spacing={2} alignItems="center"> <Stack direction="row" spacing={2} alignItems="center">
<img className="loading-image" alt="" src={getFaviconURL(route)} width="64px" height="64px"/> <ServAppIcon container={container} route={route} className="loading-image" width="70px" />
<div style={{ minWidth: 0 }}>
<div style={{minWidth: 0 }}>
<h3 style={blockStyle}>{route.Name}</h3> <h3 style={blockStyle}>{route.Name}</h3>
<p style={blockStyle}>{route.Description}</p> <p style={blockStyle}>{route.Description}</p>
<p style={{...blockStyle, fontSize: '90%', paddingTop: '3px', opacity: '0.45'}}>{route.Target}</p> <p style={{ ...blockStyle, fontSize: '90%', paddingTop: '3px', opacity: '0.45' }}>{route.Target}</p>
</div> </div>
</Stack> </Stack>
</Link> </Link>

View file

@ -89,6 +89,8 @@ const NewInstall = () => {
component: <div> component: <div>
First of all, thanks a lot for trying out Cosmos! And Welcome to the setup wizard. First of all, thanks a lot for trying out Cosmos! And Welcome to the setup wizard.
This wizard will guide you through the setup of Cosmos. It will take about 2-3 minutes and you will be ready to go. This wizard will guide you through the setup of Cosmos. It will take about 2-3 minutes and you will be ready to go.
<br /><br />
<a target='_blank' rel="noopener noreferrer" href="https://cosmos-cloud.io/doc/2%20setup">📄 Don't hesitate to rely on the documentation to guide you!</a>
</div>, </div>,
nextButtonLabel: () => { nextButtonLabel: () => {
return 'Start'; return 'Start';
@ -100,16 +102,15 @@ const NewInstall = () => {
<div> <div>
<QuestionCircleOutlined /> Cosmos is using docker to run applications. It is optional, but Cosmos will run in reverse-proxy-only mode if it cannot connect to Docker. <QuestionCircleOutlined /> Cosmos is using docker to run applications. It is optional, but Cosmos will run in reverse-proxy-only mode if it cannot connect to Docker.
</div> </div>
{(status && status.docker) ? {status && (status.docker ?
<Alert severity="success"> <Alert severity="success">
Docker is installed and running. Docker is installed and running.
</Alert> : </Alert> :
<Alert severity="error"> <Alert severity="error">
Docker is not connected! Please check your docker connection.<br/> Docker is not connected! Please check your docker connection.<br/>
Did you forget to add <pre>-v /var/run/docker.sock:/var/run/docker.sock</pre> to your docker run command?<br /> Did you forget to add <pre>-v /var/run/docker.sock:/var/run/docker.sock</pre> to your docker run command?<br />
if your docker daemon is running somewhere else, please add <pre>-e DOCKER_HOST=...</pre> to your docker run command. if your docker daemon is running somewhere else, please add <pre>-e DOCKER_HOST=...</pre> to your docker run command.
</Alert> </Alert>)}
}
{(status && status.docker) ? ( {(status && status.docker) ? (
<div> <div>
<center> <center>
@ -350,7 +351,7 @@ const NewInstall = () => {
<Alert severity="info"> <Alert severity="info">
You have enabled the DNS challenge. Make sure you have set the environment variables for your DNS provider. You have enabled the DNS challenge. Make sure you have set the environment variables for your DNS provider.
You can enable it now, but make sure you have set up your API tokens accordingly before attempting to access You can enable it now, but make sure you have set up your API tokens accordingly before attempting to access
Cosmos after this installer. See doc here: <a target="_blank" href="https://go-acme.github.io/lego/dns/">https://go-acme.github.io/lego/dns/</a> Cosmos after this installer. See doc here: <a target="_blank" rel="noopener noreferrer" href="https://go-acme.github.io/lego/dns/">https://go-acme.github.io/lego/dns/</a>
</Alert> </Alert>
)} )}
<DnsChallengeComp <DnsChallengeComp
@ -536,7 +537,7 @@ const NewInstall = () => {
Well done! You have successfully installed Cosmos. You can now login to your server using the admin account you created. Well done! You have successfully installed Cosmos. You can now login to your server using the admin account you created.
If you have changed the hostname, don't forget to use that URL to access your server after the restart. If you have changed the hostname, don't forget to use that URL to access your server after the restart.
If you have are running into issues, check the logs for any error messages and edit the file in the /config folder. If you have are running into issues, check the logs for any error messages and edit the file in the /config folder.
If you still don't manage, please join our <a target="_blank" href="https://discord.gg/PwMWwsrwHA">Discord server</a> and we'll be happy to help! If you still don't manage, please join our <a target="_blank" rel="noopener noreferrer" href="https://discord.gg/PwMWwsrwHA">Discord server</a> and we'll be happy to help!
</div>, </div>,
nextButtonLabel: () => { nextButtonLabel: () => {
return 'Apply and Restart'; return 'Apply and Restart';

View file

@ -19,6 +19,7 @@ import GetActions from './actionBar';
import ResponsiveButton from '../../components/responseiveButton'; import ResponsiveButton from '../../components/responseiveButton';
import DockerComposeImport from './containers/docker-compose'; import DockerComposeImport from './containers/docker-compose';
import { ContainerNetworkWarning } from '../../components/containers'; import { ContainerNetworkWarning } from '../../components/containers';
import { ServAppIcon } from '../../utils/servapp-icon';
const Item = styled(Paper)(({ theme }) => ({ const Item = styled(Paper)(({ theme }) => ({
backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff', backgroundColor: theme.palette.mode === 'dark' ? '#1A2027' : '#fff',
@ -108,6 +109,13 @@ const ServApps = () => {
} }
} }
const getFirstRoute = (app) => {
let routes = getContainersRoutes(config, app.Names[0].replace('/', ''));
if(routes.length > 0) {
return routes[0];
}
}
const getFirstRouteFavIcon = (app) => { const getFirstRouteFavIcon = (app) => {
let routes = getContainersRoutes(config, app.Names[0].replace('/', '')); let routes = getContainersRoutes(config, app.Names[0].replace('/', ''));
if(routes.length > 0) { if(routes.length > 0) {
@ -185,7 +193,7 @@ const ServApps = () => {
} }
</Typography> </Typography>
<Stack direction="row" spacing={2} alignItems="center"> <Stack direction="row" spacing={2} alignItems="center">
<img className="loading-image" alt="" src={getFirstRouteFavIcon(app)} width="40px" /> <ServAppIcon container={app} route={getFirstRoute(app)} className="loading-image" width="40px"/>
<Stack direction="column" spacing={0} alignItems="flex-start" style={{height: '40px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'no-wrap'}}> <Stack direction="column" spacing={0} alignItems="flex-start" style={{height: '40px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'no-wrap'}}>
<Typography variant="h5" color="text.secondary"> <Typography variant="h5" color="text.secondary">
{app.Names[0].replace('/', '')}&nbsp; {app.Names[0].replace('/', '')}&nbsp;

View file

@ -13,11 +13,10 @@ import componentsOverride from './overrides';
// ==============================|| DEFAULT THEME - MAIN ||============================== // // ==============================|| DEFAULT THEME - MAIN ||============================== //
export default function ThemeCustomization({ children }) { export default function ThemeCustomization({ children, PrimaryColor, SecondaryColor }) {
const theme = Palette( const theme = Palette(
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ?
'dark' : 'light'); 'dark' : 'light', PrimaryColor, SecondaryColor);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const themeTypography = Typography(`'Public Sans', sans-serif`); const themeTypography = Typography(`'Public Sans', sans-serif`);

View file

@ -7,9 +7,11 @@ import { presetPalettes } from '@ant-design/colors';
// project import // project import
import ThemeOption from './theme'; import ThemeOption from './theme';
import * as API from '../api';
// ==============================|| DEFAULT THEME - PALETTE ||============================== // // ==============================|| DEFAULT THEME - PALETTE ||============================== //
const Palette = (mode) => { const Palette = (mode, PrimaryColor, SecondaryColor) => {
const colors = presetPalettes; const colors = presetPalettes;
const greyPrimary = [ const greyPrimary = [
@ -32,6 +34,13 @@ const Palette = (mode) => {
const paletteColor = ThemeOption(colors, mode === 'dark'); const paletteColor = ThemeOption(colors, mode === 'dark');
if(PrimaryColor) {
paletteColor.primary.main = PrimaryColor;
}
if(SecondaryColor) {
paletteColor.secondary.main = SecondaryColor;
}
return createTheme(mode === 'dark' ? { return createTheme(mode === 'dark' ? {
palette: { palette: {
mode, mode,

View file

@ -62,7 +62,7 @@ export const DnsChallengeComp = ({ name, configName, style, multiline, type, pla
<Stack spacing={2}> <Stack spacing={2}>
<Alert severity="info"> <Alert severity="info">
Please be careful you are filling the correct values. Check the doc if unsure. Leave blank unused variables. <br /> Please be careful you are filling the correct values. Check the doc if unsure. Leave blank unused variables. <br />
Doc link: <a href={dnsConfig[formik.values[name]].url} target="_blank">{dnsConfig[formik.values[name]].url}</a> Doc link: <a href={dnsConfig[formik.values[name]].url} rel="noopener noreferrer" target="_blank">{dnsConfig[formik.values[name]].url}</a>
</Alert> </Alert>
<div className="raw-table"> <div className="raw-table">
<div dangerouslySetInnerHTML={{__html: dnsConfig[formik.values[name]].docs}}></div> <div dangerouslySetInnerHTML={{__html: dnsConfig[formik.values[name]].docs}}></div>

View file

@ -0,0 +1,7 @@
import { getFaviconURL } from "./routes";
export const ServAppIcon = ({route, container, ...pprops}) => {
return (container && container.Labels["cosmos-icon"]) ?
<img src={container.Labels["cosmos-icon"]} {...pprops}></img>
: <img src={getFaviconURL(route)} {...pprops}></img>
};

8
go.mod
View file

@ -20,11 +20,11 @@ require (
github.com/oschwald/geoip2-golang v1.8.0 github.com/oschwald/geoip2-golang v1.8.0
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
github.com/roberthodgen/spa-server v0.0.0-20171007154335-bb87b4ff3253 github.com/roberthodgen/spa-server v0.0.0-20171007154335-bb87b4ff3253
github.com/shirou/gopsutil/v3 v3.23.3 github.com/shirou/gopsutil/v3 v3.23.5
go.deanishe.net/favicon v0.1.0 go.deanishe.net/favicon v0.1.0
go.mongodb.org/mongo-driver v1.11.3 go.mongodb.org/mongo-driver v1.11.3
golang.org/x/crypto v0.7.0 golang.org/x/crypto v0.7.0
golang.org/x/sys v0.6.0 golang.org/x/sys v0.8.0
) )
require ( require (
@ -144,7 +144,7 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/objx v0.5.0 // indirect github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.2 // indirect github.com/stretchr/testify v1.8.3 // indirect
github.com/subosito/gotenv v1.2.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect
github.com/tklauser/go-sysconf v0.3.11 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect
github.com/tklauser/numcpus v0.6.0 // indirect github.com/tklauser/numcpus v0.6.0 // indirect
@ -154,7 +154,7 @@ require (
github.com/xdg-go/scram v1.1.1 // indirect github.com/xdg-go/scram v1.1.1 // indirect
github.com/xdg-go/stringprep v1.0.3 // indirect github.com/xdg-go/stringprep v1.0.3 // indirect
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect
go.opencensus.io v0.22.5 // indirect go.opencensus.io v0.22.5 // indirect
go.uber.org/ratelimit v0.1.0 // indirect go.uber.org/ratelimit v0.1.0 // indirect
golang.org/x/mod v0.8.0 // indirect golang.org/x/mod v0.8.0 // indirect

10
go.sum
View file

@ -1154,8 +1154,12 @@ github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAm
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE= github.com/shirou/gopsutil/v3 v3.23.3 h1:Syt5vVZXUDXPEXpIBt5ziWsJ4LdSAAxF4l/xZeQgSEE=
github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU= github.com/shirou/gopsutil/v3 v3.23.3/go.mod h1:lSBNN6t3+D6W5e5nXTxc8KIMMVxAcS+6IJlffjRRlMU=
github.com/shirou/gopsutil/v3 v3.23.5 h1:5SgDCeQ0KW0S4N0znjeM/eFHXXOKyv2dVNgRq/c9P6Y=
github.com/shirou/gopsutil/v3 v3.23.5/go.mod h1:Ng3Maa27Q2KARVJ0SPZF5NdrQSC3XHKP8IIWrHgMeLY=
github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ= github.com/shoenig/go-m1cpu v0.1.4/go.mod h1:Wwvst4LR89UxjeFtLRMrpgRiyY4xPsejnVZym39dbAQ=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shoenig/test v0.6.3/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ=
@ -1238,6 +1242,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.1.1/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s= github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
@ -1302,6 +1308,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.deanishe.net/favicon v0.1.0 h1:Afy941gjRik+DjUUcYHUxcztFEeFse2ITBkMMOlgefM= go.deanishe.net/favicon v0.1.0 h1:Afy941gjRik+DjUUcYHUxcztFEeFse2ITBkMMOlgefM=
@ -1602,6 +1610,8 @@ golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

48
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "cosmos-server", "name": "cosmos-server",
"version": "0.7.0-unstable4", "version": "0.8.0-unstable",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cosmos-server", "name": "cosmos-server",
"version": "0.7.0-unstable4", "version": "0.8.0-unstable",
"dependencies": { "dependencies": {
"@ant-design/colors": "^6.0.0", "@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.7.0", "@ant-design/icons": "^4.7.0",
@ -34,6 +34,7 @@
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-apexcharts": "^1.4.0", "react-apexcharts": "^1.4.0",
"react-color": "^2.19.3",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-device-detect": "^2.2.2", "react-device-detect": "^2.2.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -2759,6 +2760,14 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"dev": true "dev": true
}, },
"node_modules/@icons/material": {
"version": "0.2.4",
"resolved": "https://registry.npmjs.org/@icons/material/-/material-0.2.4.tgz",
"integrity": "sha512-QPcGmICAPbGLGb6F/yNf/KzKqvFx8z5qx3D1yFqVAjoFmXK35EgyW+cJ57Te3CNsmzblwtzakLGFqHPqrfb4Tw==",
"peerDependencies": {
"react": "*"
}
},
"node_modules/@jamesives/github-sponsors-readme-action": { "node_modules/@jamesives/github-sponsors-readme-action": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/@jamesives/github-sponsors-readme-action/-/github-sponsors-readme-action-1.2.2.tgz", "resolved": "https://registry.npmjs.org/@jamesives/github-sponsors-readme-action/-/github-sponsors-readme-action-1.2.2.tgz",
@ -7735,6 +7744,11 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/material-colors": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="
},
"node_modules/memoize-one": { "node_modules/memoize-one": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@ -8487,6 +8501,23 @@
"react": ">=0.13" "react": ">=0.13"
} }
}, },
"node_modules/react-color": {
"version": "2.19.3",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
"integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==",
"dependencies": {
"@icons/material": "^0.2.4",
"lodash": "^4.17.15",
"lodash-es": "^4.17.15",
"material-colors": "^1.2.1",
"prop-types": "^15.5.10",
"reactcss": "^1.2.0",
"tinycolor2": "^1.4.1"
},
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-copy-to-clipboard": { "node_modules/react-copy-to-clipboard": {
"version": "5.1.0", "version": "5.1.0",
"resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz",
@ -8777,6 +8808,14 @@
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0" "react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
} }
}, },
"node_modules/reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
"integrity": "sha512-KiwVUcFu1RErkI97ywr8nvx8dNOpT03rbnma0SSalTYjkrPYaEajR4a/MRt6DZ46K6arDRbWMNHF+xH7G7n/8A==",
"dependencies": {
"lodash": "^4.0.1"
}
},
"node_modules/redent": { "node_modules/redent": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@ -9609,6 +9648,11 @@
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
}, },
"node_modules/tinycolor2": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
},
"node_modules/titleize": { "node_modules/titleize": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz", "resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",

View file

@ -1,6 +1,6 @@
{ {
"name": "cosmos-server", "name": "cosmos-server",
"version": "0.7.10", "version": "0.8.0-unstable",
"description": "", "description": "",
"main": "test-server.js", "main": "test-server.js",
"bugs": { "bugs": {
@ -34,6 +34,7 @@
"qrcode": "^1.5.3", "qrcode": "^1.5.3",
"react": "^18.2.0", "react": "^18.2.0",
"react-apexcharts": "^1.4.0", "react-apexcharts": "^1.4.0",
"react-color": "^2.19.3",
"react-copy-to-clipboard": "^5.1.0", "react-copy-to-clipboard": "^5.1.0",
"react-device-detect": "^2.2.2", "react-device-detect": "^2.2.2",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",

112
src/background.go Normal file
View file

@ -0,0 +1,112 @@
package main
import (
"io/ioutil"
"os"
"net/http"
"path/filepath"
"io"
"encoding/json"
"github.com/gorilla/mux"
"github.com/azukaar/cosmos-server/src/utils"
)
func UploadBackground(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return
}
if(req.Method == "POST") {
// parse the form data
err := req.ParseMultipartForm(1 << 20)
if err != nil {
utils.HTTPError(w, "Error parsing form data", http.StatusInternalServerError, "FORM001")
return
}
// retrieve the file part of the form
file, header, err := req.FormFile("background")
if err != nil {
utils.HTTPError(w, "Error retrieving file from form data", http.StatusInternalServerError, "FORM002")
return
}
defer file.Close()
// get the file extension
ext := filepath.Ext(header.Filename)
// create a new file in the config directory
dst, err := os.Create("/config/background" + ext)
if err != nil {
utils.HTTPError(w, "Error creating destination file", http.StatusInternalServerError, "FILE004")
return
}
defer dst.Close()
// copy the uploaded file to the destination file
if _, err := io.Copy(dst, file); err != nil {
utils.HTTPError(w, "Error writing to destination file", http.StatusInternalServerError, "FILE005")
return
}
// return a response to the client
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
"data": map[string]interface{}{
"filename": header.Filename,
"size": header.Size,
"extension": ext,
},
})
} else {
utils.Error("UploadBackground: Method not allowed - " + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}
func GetBackground(w http.ResponseWriter, req *http.Request) {
if utils.LoggedInOnly(w, req) != nil {
return
}
vars := mux.Vars(req)
ext := vars["ext"]
validExtensions := map[string]bool{
"jpg": true,
"jpeg": true,
"png": true,
"gif": true,
"bmp": true,
"svg": true,
"webp": true,
"tiff": true,
"avif": true,
}
if !validExtensions[ext] {
utils.HTTPError(w, "Invalid file extension", http.StatusBadRequest, "FILE001")
return
}
if(req.Method == "GET") {
// get the background image
bg, err := ioutil.ReadFile("/config/background." + ext)
if err != nil {
utils.HTTPError(w, "Error reading background image", http.StatusInternalServerError, "FILE003")
return
}
// return a response to the client
w.Header().Set("Content-Type", "image/" + ext)
w.Write(bg)
} else {
utils.Error("GetBackground: Method not allowed - " + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}

View file

@ -176,7 +176,7 @@ func SecureAPI(userRouter *mux.Router, public bool) {
userRouter.Use(utils.MiddlewareTimeout(45 * time.Second)) userRouter.Use(utils.MiddlewareTimeout(45 * time.Second))
userRouter.Use(utils.BlockPostWithoutReferer) userRouter.Use(utils.BlockPostWithoutReferer)
userRouter.Use(proxy.BotDetectionMiddleware) userRouter.Use(proxy.BotDetectionMiddleware)
userRouter.Use(httprate.Limit(60, 1*time.Minute, userRouter.Use(httprate.Limit(120, 1*time.Minute,
httprate.WithKeyFuncs(httprate.KeyByIP), httprate.WithKeyFuncs(httprate.KeyByIP),
httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) { httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
utils.Error("Too many requests. Throttling", nil) utils.Error("Too many requests. Throttling", nil)
@ -290,6 +290,9 @@ func StartServer() {
srapi.HandleFunc("/api/markets", market.MarketGet) srapi.HandleFunc("/api/markets", market.MarketGet)
srapi.HandleFunc("/api/background", UploadBackground)
srapi.HandleFunc("/api/background/{ext}", GetBackground)
if(!config.HTTPConfig.AcceptAllInsecureHostname) { if(!config.HTTPConfig.AcceptAllInsecureHostname) {
srapi.Use(utils.EnsureHostname) srapi.Use(utils.EnsureHostname)

View file

@ -11,7 +11,9 @@ import (
) )
func StatusRoute(w http.ResponseWriter, req *http.Request) { func StatusRoute(w http.ResponseWriter, req *http.Request) {
if !utils.GetMainConfig().NewInstall && (utils.LoggedInOnly(w, req) != nil) { config := utils.GetMainConfig()
if !config.NewInstall && (utils.LoggedInOnly(w, req) != nil) {
return return
} }
@ -20,7 +22,7 @@ func StatusRoute(w http.ResponseWriter, req *http.Request) {
databaseStatus := true databaseStatus := true
if(!utils.GetMainConfig().DisableUserManagement) { if(!config.DisableUserManagement) {
err := utils.DB() err := utils.DB()
if err != nil { if err != nil {
utils.Error("Status: Database error", err) utils.Error("Status: Database error", err)
@ -40,6 +42,15 @@ func StatusRoute(w http.ResponseWriter, req *http.Request) {
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK", "status": "OK",
"data": map[string]interface{}{ "data": map[string]interface{}{
"homepage": config.HomepageConfig,
"theme": config.ThemeConfig,
"resources": map[string]interface{}{
// "ram": utils.GetRAMUsage(),
// "ramFree": utils.GetAvailableRAM(),
// "cpu": utils.GetCPUUsage(),
// "disk": utils.GetDiskUsage(),
// "network": utils.GetNetworkUsage(),
},
"database": databaseStatus, "database": databaseStatus,
"docker": docker.DockerIsConnected, "docker": docker.DockerIsConnected,
"letsencrypt": utils.GetMainConfig().HTTPConfig.HTTPSCertificateMode == "LETSENCRYPT" && utils.GetMainConfig().HTTPConfig.SSLEmail == "", "letsencrypt": utils.GetMainConfig().HTTPConfig.HTTPSCertificateMode == "LETSENCRYPT" && utils.GetMainConfig().HTTPConfig.SSLEmail == "",

View file

@ -87,6 +87,18 @@ type Config struct {
AutoUpdate bool AutoUpdate bool
OpenIDClients []OpenIDClient OpenIDClients []OpenIDClient
MarketConfig MarketConfig MarketConfig MarketConfig
HomepageConfig HomepageConfig
ThemeConfig ThemeConfig
}
type HomepageConfig struct {
Background string
Widgets []string
}
type ThemeConfig struct {
PrimaryColor string
SecondaryColor string
} }
type HTTPConfig struct { type HTTPConfig struct {

View file

@ -12,9 +12,12 @@ import (
"io/ioutil" "io/ioutil"
"fmt" "fmt"
"sync" "sync"
"time"
"path/filepath" "path/filepath"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/mem" "github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/net"
) )
var ConfigLock sync.Mutex var ConfigLock sync.Mutex
@ -449,3 +452,78 @@ func Max(x, y int) int {
} }
return x return x
} }
func GetCPUUsage() ([]float64) {
percentages, _ := cpu.Percent(time.Second, false)
return percentages
}
func GetRAMUsage() (uint64) {
v, _ := mem.VirtualMemory()
return v.Used
}
type DiskStatus struct {
Path string
TotalBytes uint64
UsedBytes uint64
}
func GetDiskUsage() []DiskStatus {
partitions, err := disk.Partitions(false)
if err != nil {
Error("Error getting disk partitions", err)
return nil
}
var diskStatuses []DiskStatus
for _, partition := range partitions {
usageStat, err := disk.Usage(partition.Mountpoint)
if err != nil {
Error("Error getting disk usage", err)
return nil
}
diskStatus := DiskStatus{
Path: partition.Mountpoint,
TotalBytes: usageStat.Total,
UsedBytes: usageStat.Used,
}
diskStatuses = append(diskStatuses, diskStatus)
}
return diskStatuses
}
type NetworkStatus struct {
BytesSent uint64
BytesRecv uint64
}
func GetNetworkUsage() NetworkStatus {
initialStat, err := net.IOCounters(true)
if err != nil {
Error("Error getting network usage", err)
return NetworkStatus{}
}
time.Sleep(1 * time.Second)
finalStat, err := net.IOCounters(true)
if err != nil {
Error("Error getting network usage", err)
return NetworkStatus{}
}
res := NetworkStatus{
BytesSent: 0,
BytesRecv: 0,
}
for i := range initialStat {
res.BytesSent += finalStat[i].BytesSent - initialStat[i].BytesSent
res.BytesRecv += finalStat[i].BytesRecv - initialStat[i].BytesRecv
}
return NetworkStatus{}
}