[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
- Fix issue where multiple DBs get created at the setup
- 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 ScrollTop from './components/ScrollTop';
import Snackbar from '@mui/material/Snackbar';
import {Alert} from '@mui/material';
import {Alert, Box} from '@mui/material';
import logo from './assets/images/icons/cosmos.png';
import * as API from './api';
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);
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') => {
setMessage(message);
setOpen(true);
setSeverity(severity);
})
return (
<ThemeCustomization>
return statusLoaded ?
<ThemeCustomization PrimaryColor={PrimaryColor} SecondaryColor={SecondaryColor}>
<Snackbar
open={open}
autoHideDuration={5000}
@ -35,7 +80,14 @@ 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' /> */}
<LoadingAnimation />
</Box>
</div>
}
export default App;

View file

@ -61,3 +61,13 @@ export const checkHost = (host) => {
);
});
}
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_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', {
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 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';
let auth = _auth;
@ -176,6 +220,7 @@ if(isDemo) {
isOnline = indexDemo.isOnline;
checkHost = indexDemo.checkHost;
getDNS = indexDemo.getDNS;
uploadBackground = indexDemo.uploadBackground;
}
export {
@ -188,5 +233,6 @@ export {
newInstall,
isOnline,
checkHost,
getDNS
getDNS,
uploadBackground
};

View file

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

Binary file not shown.

After

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

View file

@ -2,7 +2,7 @@ import React from 'react';
import { Button } from '@mui/material';
import { UploadOutlined } from '@ant-design/icons';
export default function UploadButtons({OnChange, accept}) {
export default function UploadButtons({OnChange, accept, label}) {
return (
<div>
<input
@ -15,7 +15,7 @@ export default function UploadButtons({OnChange, accept}) {
/>
<label htmlFor="contained-button-file">
<Button variant="contained" component="span" startIcon={<UploadOutlined />}>
Upload
{label || 'Upload'}
</Button>
</label>
</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 {
overflow: hidden;
}
@ -88,3 +140,7 @@
padding: 5px;
border: 1px solid #ccc;
}
.pulsing {
animation: pulsing 2s ease-in-out infinite;
}

View file

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

View file

@ -22,6 +22,7 @@ import {
Collapse,
TextField,
MenuItem,
Skeleton,
} from '@mui/material';
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 CountrySelect, { countries } from '../../../components/countrySelect';
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 [config, setConfig] = React.useState(null);
const [openModal, setOpenModal] = React.useState(false);
const [uploadingBackground, setUploadingBackground] = React.useState(false);
function refresh() {
API.config.get().then((res) => {
@ -82,7 +89,11 @@ const ConfigManagement = () => {
Email_UseTLS : config.EmailConfig.UseTLS,
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({
Hostname: Yup.string().max(255).required('Hostname is required'),
@ -124,7 +135,16 @@ const ConfigManagement = () => {
...config.DockerConfig,
SkipPruneNetwork: values.SkipPruneNetwork,
DefaultDataPath: values.DefaultDataPath
}
},
HomepageConfig: {
...config.HomepageConfig,
Background: values.Background
},
ThemeConfig: {
...config.ThemeConfig,
PrimaryColor: values.PrimaryColor,
SecondaryColor: values.SecondaryColor
},
}
API.config.set(toSave).then((data) => {
@ -153,6 +173,29 @@ const ConfigManagement = () => {
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<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">
<Grid container spacing={3}>
<Grid item xs={12}>
@ -232,6 +275,64 @@ const ConfigManagement = () => {
</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) => {
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">
<Grid container spacing={3}>
<Grid item xs={12}>

View file

@ -1,6 +1,6 @@
import { useParams } from "react-router";
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 * as API from "../../api";
import wallpaper from '../../assets/images/wallpaper2.jpg';
@ -10,19 +10,23 @@ import { getFaviconURL } from "../../utils/routes";
import { Link } from "react-router-dom";
import { getFullOrigin } from "../../utils/routes";
import IsLoggedIn from "../../isLoggedIn";
import { ServAppIcon } from "../../utils/servapp-icon";
import Chart from 'react-apexcharts';
export const HomeBackground = () => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const customPaper = API.HOME_BACKGROUND;
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
// backgroundImage: isDark ?
// `linear-gradient(#371d53, #26143a)` :
// `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>
);
};
@ -31,15 +35,16 @@ export const TransparentHeader = () => {
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const backColor = isDark ? '0,0,0' : '255,255,255';
const textColor = isDark ? 'white' : 'dark';
const backColor = isDark ? '0,0,0' : '255,255,255';
const textColor = isDark ? 'white' : 'dark';
return <style>
{`header {
background: rgba(${backColor},0.35) !important;
border-bottom-color: rgba(${backColor},0.4) !important;
background: rgba(${backColor}, 0.40) !important;
border-bottom-color: rgba(${backColor},0.45) !important;
color: ${textColor} !important;
font-weight: bold;
backdrop-filter: blur(15px);
}
header .MuiChip-label {
@ -52,18 +57,20 @@ export const TransparentHeader = () => {
}
.app {
backdrop-filter: blur(15px);
transition: background 0.1s ease-in-out;
transition: transform 0.1s ease-in-out;
}
.app:hover {
.app-hover:hover {
cursor: pointer;
background: rgba(${backColor},0.8) !important;
transform: scale(1.05);
}
.MuiAlert-standard {
background: rgba(${backColor},0.35) !important;
backdrop-filter: blur(15px);
background: rgba(${backColor},0.40) !important;
color: ${textColor} !important;
font-weight: bold;
}
@ -79,6 +86,7 @@ const HomePage = () => {
const [containers, setContainers] = useState(null);
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
const isMd = useMediaQuery(theme.breakpoints.up('md'));
const blockStyle = {
margin: 0,
@ -88,12 +96,12 @@ const HomePage = () => {
verticalAlign: 'middle',
}
const appColor = isDark ? {
const appColor = isDark ? {
color: 'white',
background: 'rgba(0,0,0,0.35)',
background: 'rgba(0,0,0,0.40)',
} : {
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 || []);
useEffect(() => {
refreshConfig();
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} >
<IsLoggedIn />
<HomeBackground />
<HomeBackground status={coStatus} />
<TransparentHeader />
<Stack style={{ zIndex: 2 }} spacing={1}>
{coStatus && !coStatus.database && (
@ -166,25 +254,93 @@ const HomePage = () => {
</Stack>
<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) => {
let skip = route.Mode == "REDIRECT";
if(route.Mode == "SERVAPP") {
const containerName = route.Target.split(':')[1].slice(2);
const container = servApps.find((c) => c.Names.includes('/' + containerName));
if(!container || container.State != "running") {
let containerName;
let container;
if (route.Mode == "SERVAPP") {
containerName = route.Target.split(':')[1].slice(2);
container = servApps.find((c) => c.Names.includes('/' + containerName));
if (!container || container.State != "running") {
skip = true
}
}
return !skip && <Grid2 item xs={12} sm={6} md={4} lg={3} xl={3} xxl={2} key={route.Name}>
<Box className='app' style={{ padding: 10, borderRadius: 5, ...appColor }}>
return !skip && <Grid2 item xs={12} sm={6} md={4} lg={3} xl={3} xxl={3} key={route.Name}>
<Box className='app app-hover' style={{ padding: 18, borderRadius: 5, ...appColor }}>
<Link to={getFullOrigin(route)} target="_blank" style={{ textDecoration: 'none', ...appColor }}>
<Stack direction="row" spacing={2} alignItems="center">
<img className="loading-image" alt="" src={getFaviconURL(route)} width="64px" height="64px"/>
<div style={{minWidth: 0 }}>
<ServAppIcon container={container} route={route} className="loading-image" width="70px" />
<div style={{ minWidth: 0 }}>
<h3 style={blockStyle}>{route.Name}</h3>
<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>
</Stack>
</Link>

View file

@ -89,6 +89,8 @@ const NewInstall = () => {
component: <div>
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.
<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>,
nextButtonLabel: () => {
return 'Start';
@ -100,16 +102,15 @@ const NewInstall = () => {
<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.
</div>
{(status && status.docker) ?
<Alert severity="success">
Docker is installed and running.
</Alert> :
<Alert severity="error">
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 />
if your docker daemon is running somewhere else, please add <pre>-e DOCKER_HOST=...</pre> to your docker run command.
</Alert>
}
{status && (status.docker ?
<Alert severity="success">
Docker is installed and running.
</Alert> :
<Alert severity="error">
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 />
if your docker daemon is running somewhere else, please add <pre>-e DOCKER_HOST=...</pre> to your docker run command.
</Alert>)}
{(status && status.docker) ? (
<div>
<center>
@ -350,7 +351,7 @@ const NewInstall = () => {
<Alert severity="info">
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
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>
)}
<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.
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 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>,
nextButtonLabel: () => {
return 'Apply and Restart';

View file

@ -19,6 +19,7 @@ import GetActions from './actionBar';
import ResponsiveButton from '../../components/responseiveButton';
import DockerComposeImport from './containers/docker-compose';
import { ContainerNetworkWarning } from '../../components/containers';
import { ServAppIcon } from '../../utils/servapp-icon';
const Item = styled(Paper)(({ theme }) => ({
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) => {
let routes = getContainersRoutes(config, app.Names[0].replace('/', ''));
if(routes.length > 0) {
@ -185,7 +193,7 @@ const ServApps = () => {
}
</Typography>
<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'}}>
<Typography variant="h5" color="text.secondary">
{app.Names[0].replace('/', '')}&nbsp;

View file

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

View file

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

View file

@ -62,7 +62,7 @@ export const DnsChallengeComp = ({ name, configName, style, multiline, type, pla
<Stack spacing={2}>
<Alert severity="info">
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>
<div className="raw-table">
<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/pquerna/otp v1.4.0
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.mongodb.org/mongo-driver v1.11.3
golang.org/x/crypto v0.7.0
golang.org/x/sys v0.6.0
golang.org/x/sys v0.8.0
)
require (
@ -144,7 +144,7 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // 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/tklauser/go-sysconf v0.3.11 // 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/stringprep v1.0.3 // 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.uber.org/ratelimit v0.1.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/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.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.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
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/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=
@ -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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
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.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
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/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.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/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
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.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
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",
"version": "0.7.0-unstable4",
"version": "0.8.0-unstable",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "cosmos-server",
"version": "0.7.0-unstable4",
"version": "0.8.0-unstable",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons": "^4.7.0",
@ -34,6 +34,7 @@
"qrcode": "^1.5.3",
"react": "^18.2.0",
"react-apexcharts": "^1.4.0",
"react-color": "^2.19.3",
"react-copy-to-clipboard": "^5.1.0",
"react-device-detect": "^2.2.2",
"react-dom": "^18.2.0",
@ -2759,6 +2760,14 @@
"integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
"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": {
"version": "1.2.2",
"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_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": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
@ -8487,6 +8501,23 @@
"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": {
"version": "5.1.0",
"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"
}
},
"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": {
"version": "3.0.0",
"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",
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/titleize/-/titleize-3.0.0.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.7.10",
"version": "0.8.0-unstable",
"description": "",
"main": "test-server.js",
"bugs": {
@ -34,6 +34,7 @@
"qrcode": "^1.5.3",
"react": "^18.2.0",
"react-apexcharts": "^1.4.0",
"react-color": "^2.19.3",
"react-copy-to-clipboard": "^5.1.0",
"react-device-detect": "^2.2.2",
"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.BlockPostWithoutReferer)
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.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
utils.Error("Too many requests. Throttling", nil)
@ -290,6 +290,9 @@ func StartServer() {
srapi.HandleFunc("/api/markets", market.MarketGet)
srapi.HandleFunc("/api/background", UploadBackground)
srapi.HandleFunc("/api/background/{ext}", GetBackground)
if(!config.HTTPConfig.AcceptAllInsecureHostname) {
srapi.Use(utils.EnsureHostname)

View file

@ -11,7 +11,9 @@ import (
)
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
}
@ -20,7 +22,7 @@ func StatusRoute(w http.ResponseWriter, req *http.Request) {
databaseStatus := true
if(!utils.GetMainConfig().DisableUserManagement) {
if(!config.DisableUserManagement) {
err := utils.DB()
if err != nil {
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{}{
"status": "OK",
"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,
"docker": docker.DockerIsConnected,
"letsencrypt": utils.GetMainConfig().HTTPConfig.HTTPSCertificateMode == "LETSENCRYPT" && utils.GetMainConfig().HTTPConfig.SSLEmail == "",

View file

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

View file

@ -12,9 +12,12 @@ import (
"io/ioutil"
"fmt"
"sync"
"time"
"path/filepath"
"github.com/shirou/gopsutil/v3/cpu"
"github.com/shirou/gopsutil/v3/mem"
"github.com/shirou/gopsutil/v3/disk"
"github.com/shirou/gopsutil/v3/net"
)
var ConfigLock sync.Mutex
@ -449,3 +452,78 @@ func Max(x, y int) int {
}
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{}
}