v0.0.9 setup wizard
This commit is contained in:
parent
35b6f9b488
commit
822d4bc057
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -6,6 +6,7 @@ static
|
||||||
client/dist
|
client/dist
|
||||||
client/.vite
|
client/.vite
|
||||||
config_dev.json
|
config_dev.json
|
||||||
|
config_dev.old.json
|
||||||
tests
|
tests
|
||||||
todo.txt
|
todo.txt
|
||||||
LICENCE
|
LICENCE
|
|
@ -8,7 +8,17 @@ function list() {
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const newDB = () => {
|
||||||
|
return wrap(fetch('/cosmos/api/newDB', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
list,
|
list,
|
||||||
|
newDB,
|
||||||
};
|
};
|
|
@ -2,9 +2,33 @@ import * as auth from './authentication.jsx';
|
||||||
import * as users from './users.jsx';
|
import * as users from './users.jsx';
|
||||||
import * as config from './config.jsx';
|
import * as config from './config.jsx';
|
||||||
import * as docker from './docker.jsx';
|
import * as docker from './docker.jsx';
|
||||||
|
|
||||||
|
import wrap from './wrap';
|
||||||
|
|
||||||
|
const getStatus = () => {
|
||||||
|
return wrap(fetch('/cosmos/api/status', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
const newInstall = (req) => {
|
||||||
|
return wrap(fetch('/cosmos/api/newInstall', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(req)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
auth,
|
auth,
|
||||||
users,
|
users,
|
||||||
config,
|
config,
|
||||||
docker
|
docker,
|
||||||
|
getStatus,
|
||||||
|
newInstall,
|
||||||
};
|
};
|
|
@ -13,7 +13,9 @@ export default function wrap(apicall) {
|
||||||
return rep;
|
return rep;
|
||||||
}
|
}
|
||||||
snackit(rep.message);
|
snackit(rep.message);
|
||||||
throw new Error(rep.message);
|
const e = new Error(rep.message);
|
||||||
|
e.status = response.status;
|
||||||
|
throw e;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ import logo from '../icons/cosmos.png';
|
||||||
const AuthBackground = () => {
|
const AuthBackground = () => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
return (
|
return (
|
||||||
<Box sx={{ position: 'fixed', float: 'left', height: 'calc(100vh - 50px)', overflow: 'hidden', filter: 'blur(25px)', zIndex: 0, top: 100, left: -500 }}>
|
<Box sx={{ position: 'fixed', float: 'left', height: 'calc(100vh - 50px)', overflow: 'hidden', filter: 'blur(25px)', zIndex: 0, top: 150, left: -500 }}>
|
||||||
<img src={logo} style={{ display:'inline'}} alt="Cosmos" width="1100" />
|
<img src={logo} style={{ display:'inline'}} alt="Cosmos" width="1100" />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|
|
@ -6,7 +6,10 @@ const isLoggedIn = () => useEffect(() => {
|
||||||
console.log("CHECK LOGIN")
|
console.log("CHECK LOGIN")
|
||||||
API.auth.me().then((data) => {
|
API.auth.me().then((data) => {
|
||||||
if(data.status != 'OK') {
|
if(data.status != 'OK') {
|
||||||
window.location.href = '/ui/login';
|
if(data.status == 'NEW_INSTALL') {
|
||||||
|
window.location.href = '/ui/newInstall';
|
||||||
|
} else
|
||||||
|
window.location.href = '/ui/login';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
|
@ -10,11 +10,16 @@ import AuthFooter from '../../components/cards/AuthFooter';
|
||||||
|
|
||||||
// assets
|
// assets
|
||||||
import AuthBackground from '../../assets/images/auth/AuthBackground';
|
import AuthBackground from '../../assets/images/auth/AuthBackground';
|
||||||
|
import { useTheme } from '@mui/material/styles';
|
||||||
|
|
||||||
// ==============================|| AUTHENTICATION - WRAPPER ||============================== //
|
// ==============================|| AUTHENTICATION - WRAPPER ||============================== //
|
||||||
|
|
||||||
const AuthWrapper = ({ children }) => (
|
const AuthWrapper = ({ children }) => {
|
||||||
<Box sx={{ minHeight: '100vh' }}>
|
const theme = useTheme();
|
||||||
|
const darkMode = theme.palette.mode === 'dark';
|
||||||
|
|
||||||
|
return <Box sx={{ minHeight: '100vh',
|
||||||
|
background: darkMode ? 'none' : '#f0efef' }}>
|
||||||
<AuthBackground />
|
<AuthBackground />
|
||||||
<Grid
|
<Grid
|
||||||
container
|
container
|
||||||
|
@ -34,7 +39,12 @@ const AuthWrapper = ({ children }) => (
|
||||||
container
|
container
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
sx={{ minHeight: { xs: 'calc(100vh - 134px)', md: 'calc(100vh - 112px)' } }}
|
sx={{
|
||||||
|
minHeight: {
|
||||||
|
xs: 'calc(100vh - 134px)',
|
||||||
|
md: 'calc(100vh - 112px)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<AuthCard>{children}</AuthCard>
|
<AuthCard>{children}</AuthCard>
|
||||||
|
@ -46,7 +56,7 @@ const AuthWrapper = ({ children }) => (
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
};
|
||||||
|
|
||||||
AuthWrapper.propTypes = {
|
AuthWrapper.propTypes = {
|
||||||
children: PropTypes.node
|
children: PropTypes.node
|
||||||
|
|
|
@ -57,6 +57,8 @@ const AuthLogin = () => {
|
||||||
API.auth.me().then((data) => {
|
API.auth.me().then((data) => {
|
||||||
if(data.status == 'OK') {
|
if(data.status == 'OK') {
|
||||||
window.location.href = redirectTo;
|
window.location.href = redirectTo;
|
||||||
|
} else if(data.status == 'NEW_INSTALL') {
|
||||||
|
window.location.href = '/ui/newInstall';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -15,13 +15,19 @@ import {
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
Accordion,
|
Accordion,
|
||||||
Chip,
|
Chip,
|
||||||
|
Box,
|
||||||
|
FormControl,
|
||||||
|
IconButton,
|
||||||
|
InputAdornment,
|
||||||
|
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { Field } from 'formik';
|
import { Field } from 'formik';
|
||||||
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
import { DownOutlined, UpOutlined } from '@ant-design/icons';
|
||||||
|
import { strengthColor, strengthIndicator } from '../../../utils/password-strength';
|
||||||
|
|
||||||
|
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
export const CosmosInputText = ({ name, type, placeholder, label, formik }) => {
|
export const CosmosInputText = ({ name, type, placeholder, onChange, label, formik }) => {
|
||||||
return <Grid item xs={12}>
|
return <Grid item xs={12}>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
<InputLabel htmlFor={name}>{label}</InputLabel>
|
<InputLabel htmlFor={name}>{label}</InputLabel>
|
||||||
|
@ -31,7 +37,12 @@ export const CosmosInputText = ({ name, type, placeholder, label, formik }) => {
|
||||||
value={formik.values[name]}
|
value={formik.values[name]}
|
||||||
name={name}
|
name={name}
|
||||||
onBlur={formik.handleBlur}
|
onBlur={formik.handleBlur}
|
||||||
onChange={formik.handleChange}
|
onChange={(...e) => {
|
||||||
|
if (onChange) {
|
||||||
|
onChange(...e);
|
||||||
|
}
|
||||||
|
formik.handleChange(...e);
|
||||||
|
}}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
fullWidth
|
fullWidth
|
||||||
error={Boolean(formik.touched[name] && formik.errors[name])}
|
error={Boolean(formik.touched[name] && formik.errors[name])}
|
||||||
|
@ -45,6 +56,78 @@ export const CosmosInputText = ({ name, type, placeholder, label, formik }) => {
|
||||||
</Grid>
|
</Grid>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const CosmosInputPassword = ({ name, type, placeholder, onChange, label, formik }) => {
|
||||||
|
const [level, setLevel] = React.useState();
|
||||||
|
const [showPassword, setShowPassword] = React.useState(false);
|
||||||
|
const handleClickShowPassword = () => {
|
||||||
|
setShowPassword(!showPassword);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseDownPassword = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
|
||||||
|
const changePassword = (value) => {
|
||||||
|
const temp = strengthIndicator(value);
|
||||||
|
setLevel(strengthColor(temp));
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
changePassword('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return <Grid item xs={12}>
|
||||||
|
<Stack spacing={1}>
|
||||||
|
<InputLabel htmlFor={name}>{label}</InputLabel>
|
||||||
|
<OutlinedInput
|
||||||
|
id={name}
|
||||||
|
type={showPassword ? 'text' : 'password'}
|
||||||
|
value={formik.values[name]}
|
||||||
|
name={name}
|
||||||
|
onBlur={formik.handleBlur}
|
||||||
|
onChange={(e) => {
|
||||||
|
changePassword(e.target.value);
|
||||||
|
formik.handleChange(e);
|
||||||
|
}}
|
||||||
|
placeholder="********"
|
||||||
|
endAdornment={
|
||||||
|
<InputAdornment position="end">
|
||||||
|
<IconButton
|
||||||
|
aria-label="toggle password visibility"
|
||||||
|
onClick={handleClickShowPassword}
|
||||||
|
onMouseDown={handleMouseDownPassword}
|
||||||
|
edge="end"
|
||||||
|
size="large"
|
||||||
|
>
|
||||||
|
{showPassword ? <EyeOutlined /> : <EyeInvisibleOutlined />}
|
||||||
|
</IconButton>
|
||||||
|
</InputAdornment>
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
error={Boolean(formik.touched[name] && formik.errors[name])}
|
||||||
|
/>
|
||||||
|
{formik.touched[name] && formik.errors[name] && (
|
||||||
|
<FormHelperText error id="standard-weight-helper-text-name-login">
|
||||||
|
{formik.errors[name]}
|
||||||
|
</FormHelperText>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormControl fullWidth sx={{ mt: 2 }}>
|
||||||
|
<Grid container spacing={2} alignItems="center">
|
||||||
|
<Grid item>
|
||||||
|
<Box sx={{ bgcolor: level?.color, width: 85, height: 8, borderRadius: '7px' }} />
|
||||||
|
</Grid>
|
||||||
|
<Grid item>
|
||||||
|
<Typography variant="subtitle1" fontSize="0.75rem">
|
||||||
|
{level?.label}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</FormControl>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
}
|
||||||
|
|
||||||
export const CosmosSelect = ({ name, label, formik, options }) => {
|
export const CosmosSelect = ({ name, label, formik, options }) => {
|
||||||
return <Grid item xs={12}>
|
return <Grid item xs={12}>
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
|
|
|
@ -42,7 +42,7 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
|
||||||
initialValues={{
|
initialValues={{
|
||||||
Name: routeConfig.Name,
|
Name: routeConfig.Name,
|
||||||
Description: routeConfig.Description,
|
Description: routeConfig.Description,
|
||||||
Mode: routeConfig.Mode,
|
Mode: routeConfig.Mode || "SERVAPP",
|
||||||
Target: routeConfig.Target,
|
Target: routeConfig.Target,
|
||||||
UseHost: routeConfig.UseHost,
|
UseHost: routeConfig.UseHost,
|
||||||
AuthEnabled: routeConfig.AuthEnabled,
|
AuthEnabled: routeConfig.AuthEnabled,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
// material-ui
|
// material-ui
|
||||||
import {
|
import {
|
||||||
|
@ -36,6 +36,9 @@ import avatar3 from '../../assets/images/users/avatar-3.png';
|
||||||
import avatar4 from '../../assets/images/users/avatar-4.png';
|
import avatar4 from '../../assets/images/users/avatar-4.png';
|
||||||
import isLoggedIn from '../../isLoggedIn';
|
import isLoggedIn from '../../isLoggedIn';
|
||||||
|
|
||||||
|
import * as API from '../../api';
|
||||||
|
import AnimateButton from '../../components/@extended/AnimateButton';
|
||||||
|
|
||||||
// avatar style
|
// avatar style
|
||||||
const avatarSX = {
|
const avatarSX = {
|
||||||
width: 36,
|
width: 36,
|
||||||
|
@ -77,10 +80,59 @@ const DashboardDefault = () => {
|
||||||
|
|
||||||
isLoggedIn();
|
isLoggedIn();
|
||||||
|
|
||||||
|
const [coStatus, setCoStatus] = useState(null);
|
||||||
|
const [isCreatingDB, setIsCreatingDB] = useState(false);
|
||||||
|
|
||||||
|
const refreshStatus = () => {
|
||||||
|
API.getStatus().then((res) => {
|
||||||
|
setCoStatus(res.data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshStatus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const setupDB = () => {
|
||||||
|
setIsCreatingDB(true);
|
||||||
|
API.docker.newDB().then((res) => {
|
||||||
|
refreshStatus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Alert severity="info">Dashboard implementation currently in progress! If you want to voice your opinion on where Cosmos is going, please join us on Discord!</Alert>
|
<Stack spacing={1}>
|
||||||
|
{coStatus && !coStatus.database && (
|
||||||
|
<Alert severity="error">
|
||||||
|
No Database is setup for Cosmos! User Management and Authentication will not work.<br />
|
||||||
|
You can either setup the database, or disable user management in the configuration panel.<br />
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{coStatus && coStatus.letsencrypt && (
|
||||||
|
<Alert severity="error">
|
||||||
|
You have enabled Let's Encrypt for automatic HTTPS Certificate. You need to provide the configuration with an email address to use for Let's Encrypt in the configs.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{coStatus && coStatus.domain && (
|
||||||
|
<Alert severity="error">
|
||||||
|
You are using localhost or 0.0.0.0 as a hostname in the configuration. It is recommended that you use a domain name instead.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{coStatus && !coStatus.docker && (
|
||||||
|
<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>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Alert severity="info">Dashboard implementation currently in progress! If you want to voice your opinion on where Cosmos is going, please join us on Discord!</Alert>
|
||||||
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
<div style={{filter: 'blur(10px)', marginTop: '30px', pointerEvents: 'none'}}>
|
<div style={{filter: 'blur(10px)', marginTop: '30px', pointerEvents: 'none'}}>
|
||||||
<Grid container rowSpacing={4.5} columnSpacing={2.75}>
|
<Grid container rowSpacing={4.5} columnSpacing={2.75}>
|
||||||
|
|
466
client/src/pages/newInstall/newInstall.jsx
Normal file
466
client/src/pages/newInstall/newInstall.jsx
Normal file
|
@ -0,0 +1,466 @@
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
import * as Yup from 'yup';
|
||||||
|
|
||||||
|
// material-ui
|
||||||
|
import { Alert, Button, CircularProgress, FormControl, FormHelperText, Grid, Stack, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
// ant-ui icons
|
||||||
|
import { CheckCircleOutlined, LeftOutlined, QuestionCircleFilled, QuestionCircleOutlined, RightOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
// project import
|
||||||
|
import AuthWrapper from '../authentication/AuthWrapper';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
import * as API from '../../api';
|
||||||
|
import { Formik } from 'formik';
|
||||||
|
import { CosmosInputPassword, CosmosInputText, CosmosSelect } from '../config/users/formShortcuts';
|
||||||
|
import AnimateButton from '../../components/@extended/AnimateButton';
|
||||||
|
import { Box } from '@mui/system';
|
||||||
|
// ================================|| LOGIN ||================================ //
|
||||||
|
|
||||||
|
const NewInstall = () => {
|
||||||
|
const [activeStep, setActiveStep] = useState(0);
|
||||||
|
const [status, setStatus] = useState(null);
|
||||||
|
const [counter, setCounter] = useState(0);
|
||||||
|
const refreshStatus = async () => {
|
||||||
|
try {
|
||||||
|
const res = await API.getStatus()
|
||||||
|
setStatus(res.data);
|
||||||
|
} catch(error) {
|
||||||
|
if(error.status == 401)
|
||||||
|
window.location.href = "/ui/login";
|
||||||
|
}
|
||||||
|
if (typeof status !== 'undefined') {
|
||||||
|
setTimeout(() => {
|
||||||
|
setCounter(counter + 1);
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshStatus();
|
||||||
|
}, [counter]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(activeStep == 4 && status && !status.database) {
|
||||||
|
setActiveStep(5);
|
||||||
|
}
|
||||||
|
}, [activeStep, status]);
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
label: 'Welcome! 💖',
|
||||||
|
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.
|
||||||
|
</div>,
|
||||||
|
nextButtonLabel: () => {
|
||||||
|
return 'Start';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Docker 🐋 (step 1/4)',
|
||||||
|
component: <Stack item xs={12} spacing={2}>
|
||||||
|
<div>
|
||||||
|
<QuestionCircleOutlined /> Cosmos is using docker to run applications. It is optionnal, 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) ? (
|
||||||
|
<div>
|
||||||
|
<center>
|
||||||
|
<CheckCircleOutlined
|
||||||
|
style={{ fontSize: '30px', color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
) : (<><div>
|
||||||
|
Rechecking Docker Status...
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<center><CircularProgress color="inherit" /></center>
|
||||||
|
</div></>)}
|
||||||
|
</Stack>,
|
||||||
|
nextButtonLabel: () => {
|
||||||
|
return status && status.docker ? 'Next' : 'Skip';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Database 🗄️ (step 2/4)',
|
||||||
|
component: <Stack item xs={12} spacing={2}>
|
||||||
|
<div>
|
||||||
|
<QuestionCircleOutlined /> Cosmos is using a MongoDB database to store all the data. It is optionnal, but Authentication as well as the UI will not work without a database.
|
||||||
|
</div>
|
||||||
|
{(status && status.database) ?
|
||||||
|
<Alert severity="success">
|
||||||
|
Database is connected.
|
||||||
|
</Alert> :
|
||||||
|
<><Alert severity="error">
|
||||||
|
Database is not connected!
|
||||||
|
</Alert>
|
||||||
|
<div>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
DBMode: "Create"
|
||||||
|
}}
|
||||||
|
validate={(values) => {
|
||||||
|
}}
|
||||||
|
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
const res = await API.newInstall({
|
||||||
|
step: "2",
|
||||||
|
MongoDBMode: values.DBMode,
|
||||||
|
MongoDB: values.MongoDB,
|
||||||
|
});
|
||||||
|
if(res.status == "OK")
|
||||||
|
setStatus({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
setStatus({ success: false });
|
||||||
|
setErrors({ submit: error.message });
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{(formik) => (
|
||||||
|
<form noValidate onSubmit={formik.handleSubmit}>
|
||||||
|
<Stack item xs={12} spacing={2}>
|
||||||
|
<CosmosSelect
|
||||||
|
name="DBMode"
|
||||||
|
label="Select your choice"
|
||||||
|
formik={formik}
|
||||||
|
options={[
|
||||||
|
["Create", "Automatically create a secure database (recommended)"],
|
||||||
|
["Provided", "Supply my own database credentials"],
|
||||||
|
["DisableUserManagement", "Disable User Management and UI"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{formik.values.DBMode === "Provided" && (
|
||||||
|
<>
|
||||||
|
<CosmosInputText
|
||||||
|
name="MongoDB"
|
||||||
|
label="Database URL"
|
||||||
|
placeholder={"mongodb://user:password@localhost:27017"}
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{formik.errors.submit && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<AnimateButton>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
fullWidth>
|
||||||
|
{formik.isSubmitting ? 'Loading' : (
|
||||||
|
formik.values.DBMode === "DisableUserManagement" ? 'Disable' : 'Connect'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</AnimateButton>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
{(status && status.database) ? (
|
||||||
|
<div>
|
||||||
|
<center>
|
||||||
|
<CheckCircleOutlined
|
||||||
|
style={{ fontSize: '30px', color: '#52c41a' }}
|
||||||
|
/>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
) : (<><div>
|
||||||
|
Rechecking Database Status...
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<center><CircularProgress color="inherit" /></center>
|
||||||
|
</div></>)}
|
||||||
|
</Stack>,
|
||||||
|
nextButtonLabel: () => {
|
||||||
|
return (status && status.database) ? 'Next' : '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'HTTPS 🌐 (step 3/4)',
|
||||||
|
component: (<Stack item xs={12} spacing={2}>
|
||||||
|
<div>
|
||||||
|
<QuestionCircleOutlined /> It is recommended to use Let's Encrypt to automatically provide HTTPS Certificates.
|
||||||
|
This requires a valid domain name pointing to this server. If you don't have one, you can use a self-signed certificate.
|
||||||
|
If you enable HTTPS, it will be effective after the next restart.
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{status && <div>
|
||||||
|
HTTPS Certificate Mode is currently: <b>{status.HTTPSCertificateMode}</b>
|
||||||
|
</div>}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
HTTPSCertificateMode: "LETSENCRYPT"
|
||||||
|
}}
|
||||||
|
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
const res = await API.newInstall({
|
||||||
|
step: "3",
|
||||||
|
HTTPSCertificateMode: values.HTTPSCertificateMode,
|
||||||
|
SSLEmail: values.SSLEmail,
|
||||||
|
TLSKey: values.HTTPSCertificateMode === "PROVIDED" ? values.TLSKey : '',
|
||||||
|
TLSCert: values.HTTPSCertificateMode === "PROVIDED" ? values.TLSCert : '',
|
||||||
|
Hostname: values.Hostname,
|
||||||
|
});
|
||||||
|
if(res.status == "OK")
|
||||||
|
setStatus({ success: true });
|
||||||
|
} catch (error) {
|
||||||
|
setStatus({ success: false });
|
||||||
|
setErrors({ submit: error.message });
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{(formik) => (
|
||||||
|
<form noValidate onSubmit={formik.handleSubmit}>
|
||||||
|
<Stack item xs={12} spacing={2}>
|
||||||
|
<CosmosSelect
|
||||||
|
name="HTTPSCertificateMode"
|
||||||
|
label="Select your choice"
|
||||||
|
formik={formik}
|
||||||
|
options={[
|
||||||
|
["LETSENCRYPT", "Use Let's Encrypt automatic HTTPS (recommended)"],
|
||||||
|
["PROVIDED", "Supply my own HTTPS certificate"],
|
||||||
|
["SELFSIGNED", "Generate a self-signed certificate"],
|
||||||
|
["DISABLE", "Use HTTP only (not recommended)"],
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{formik.values.HTTPSCertificateMode === "LETSENCRYPT" && (
|
||||||
|
<>
|
||||||
|
<CosmosInputText
|
||||||
|
name="SSLEmail"
|
||||||
|
label="Let's Encrypt Email"
|
||||||
|
placeholder={"email@domain.com"}
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{formik.values.HTTPSCertificateMode === "PROVIDED" && (
|
||||||
|
<>
|
||||||
|
<CosmosInputText
|
||||||
|
name="TLSKey"
|
||||||
|
label="Private Certificate"
|
||||||
|
placeholder="-----BEGIN CERTIFICATE-----\nMIIEowIBwIBAA...."
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
<CosmosInputText
|
||||||
|
name="TLSCert"
|
||||||
|
label="Public Certificate"
|
||||||
|
placeholder="-----BEGIN RSA PRIVATE KEY-----\nQCdYIUkYi...."
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<CosmosInputText
|
||||||
|
name="Hostname"
|
||||||
|
label="Hostname (Domain required for Let's Encrypt)"
|
||||||
|
placeholder="yourdomain.com, your ip, or localhost"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{formik.errors.submit && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<AnimateButton>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
fullWidth>
|
||||||
|
{formik.isSubmitting ? 'Loading' : (
|
||||||
|
formik.values.HTTPSCertificateMode === "DISABLE" ? 'Disable' : 'Update'
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</AnimateButton>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</div>
|
||||||
|
</Stack>),
|
||||||
|
nextButtonLabel: () => {
|
||||||
|
return status ? 'Next' : 'Skip';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Admin Account 🔑 (step 4/4)',
|
||||||
|
component: <div>
|
||||||
|
<Stack item xs={12} spacing={2}>
|
||||||
|
<div>
|
||||||
|
<QuestionCircleOutlined /> Create a local admin account to manage your server. Email is optional and used for notifications and password recovery.
|
||||||
|
</div>
|
||||||
|
<Formik
|
||||||
|
initialValues={{
|
||||||
|
nickname: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
email: '',
|
||||||
|
}}
|
||||||
|
validationSchema={Yup.object().shape({
|
||||||
|
nickname: Yup.string().required('Nickname is required').min(3).max(32),
|
||||||
|
password: Yup.string().required('Password is required').min(8).max(128).matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/, 'Password must contain at least 1 lowercase, 1 uppercase, 1 number, and 1 special character'),
|
||||||
|
email: Yup.string().email('Must be a valid email').max(255),
|
||||||
|
confirmPassword: Yup.string().oneOf([Yup.ref('password'), null], 'Passwords must match'),
|
||||||
|
})}
|
||||||
|
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||||
|
try {
|
||||||
|
setSubmitting(true);
|
||||||
|
const res = await API.newInstall({
|
||||||
|
step: "4",
|
||||||
|
nickname: values.nickname,
|
||||||
|
password: values.password,
|
||||||
|
email: values.email,
|
||||||
|
});
|
||||||
|
if(res.status == "OK") {
|
||||||
|
setStatus({ success: true });
|
||||||
|
setActiveStep(5);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setStatus({ success: false });
|
||||||
|
setErrors({ submit: error.message });
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{(formik) => (
|
||||||
|
<form noValidate onSubmit={formik.handleSubmit}>
|
||||||
|
<Stack item xs={12} spacing={2}>
|
||||||
|
<CosmosInputText
|
||||||
|
name="nickname"
|
||||||
|
label="Nickname"
|
||||||
|
placeholder="admin"
|
||||||
|
formik={formik}
|
||||||
|
/>
|
||||||
|
<CosmosInputText
|
||||||
|
name="email"
|
||||||
|
label="Email"
|
||||||
|
placeholder="Email (optional)"
|
||||||
|
formik={formik}
|
||||||
|
type="email"
|
||||||
|
/>
|
||||||
|
<CosmosInputPassword
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
placeholder="password"
|
||||||
|
formik={formik}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
<CosmosInputText
|
||||||
|
name="confirmPassword"
|
||||||
|
label="Confirm Password"
|
||||||
|
placeholder="password"
|
||||||
|
formik={formik}
|
||||||
|
type="password"
|
||||||
|
/>
|
||||||
|
{formik.errors.submit && (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<FormHelperText error>{formik.errors.submit}</FormHelperText>
|
||||||
|
</Grid>
|
||||||
|
)}
|
||||||
|
<AnimateButton>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="contained"
|
||||||
|
color="primary"
|
||||||
|
disabled={formik.isSubmitting}
|
||||||
|
fullWidth>
|
||||||
|
{formik.isSubmitting ? 'Loading' : 'Create'}
|
||||||
|
</Button>
|
||||||
|
</AnimateButton>
|
||||||
|
</Stack>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</Stack>
|
||||||
|
</div>,
|
||||||
|
nextButtonLabel: () => {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Finish 🎉',
|
||||||
|
component: <div>
|
||||||
|
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 href="https://discord.gg/PwMWwsrwHA">Discord server</a> and we'll be happy to help!
|
||||||
|
</div>,
|
||||||
|
nextButtonLabel: () => {
|
||||||
|
return 'Apply and Restart';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return <AuthWrapper>
|
||||||
|
<Grid container spacing={3}>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
|
||||||
|
<Typography variant="h3">{steps[activeStep].label}</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
<Grid item xs={12} spacing={2}>
|
||||||
|
{steps[activeStep].component}
|
||||||
|
|
||||||
|
{/*JSON.stringify(status)*/}
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<Stack direction="row" spacing={2} sx={{ '& > *': { flexGrow: 1 } }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
startIcon={<LeftOutlined />}
|
||||||
|
onClick={() => setActiveStep(activeStep - 1)}
|
||||||
|
disabled={activeStep <= 0}
|
||||||
|
>Back</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
endIcon={<RightOutlined />}
|
||||||
|
disabled={steps[activeStep].nextButtonLabel() == ''}
|
||||||
|
onClick={() => {
|
||||||
|
if(activeStep >= steps.length - 1) {
|
||||||
|
API.newInstall({
|
||||||
|
step: "5",
|
||||||
|
})
|
||||||
|
setTimeout(() => {
|
||||||
|
window.location.href = "/ui/login";
|
||||||
|
}, 500);
|
||||||
|
} else
|
||||||
|
setActiveStep(activeStep + 1)
|
||||||
|
}}
|
||||||
|
>{
|
||||||
|
steps[activeStep].nextButtonLabel() ?
|
||||||
|
steps[activeStep].nextButtonLabel() :
|
||||||
|
'Next'
|
||||||
|
}</Button>
|
||||||
|
</Stack>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</AuthWrapper>
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewInstall;
|
|
@ -5,6 +5,7 @@ import { lazy } from 'react';
|
||||||
import Loadable from '../components/Loadable';
|
import Loadable from '../components/Loadable';
|
||||||
import MinimalLayout from '../layout/MinimalLayout';
|
import MinimalLayout from '../layout/MinimalLayout';
|
||||||
import Logout from '../pages/authentication/Logoff';
|
import Logout from '../pages/authentication/Logoff';
|
||||||
|
import NewInstall from '../pages/newInstall/newInstall';
|
||||||
|
|
||||||
// render - login
|
// render - login
|
||||||
const AuthLogin = Loadable(lazy(() => import('../pages/authentication/Login')));
|
const AuthLogin = Loadable(lazy(() => import('../pages/authentication/Login')));
|
||||||
|
@ -27,7 +28,12 @@ const LoginRoutes = {
|
||||||
{
|
{
|
||||||
path: '/ui/logout',
|
path: '/ui/logout',
|
||||||
element: <Logout />
|
element: <Logout />
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
path: '/ui/newInstall',
|
||||||
|
// redirect to /ui
|
||||||
|
element: <NewInstall />
|
||||||
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -51,7 +51,7 @@ const MainRoutes = {
|
||||||
{
|
{
|
||||||
path: '/ui/config/proxy',
|
path: '/ui/config/proxy',
|
||||||
element: <ProxyManagement />
|
element: <ProxyManagement />
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
12
client/src/utils/container-security.jsx
Normal file
12
client/src/utils/container-security.jsx
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
function checkSec(containers, name) {
|
||||||
|
const container = containers.find((container) => container.Names[0] === '/' + p1_.split(":")[0])
|
||||||
|
|
||||||
|
if (container) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerSecurityText = ({containers, name}) => {
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "cosmos-server",
|
"name": "cosmos-server",
|
||||||
"version": "0.0.8",
|
"version": "0.0.9",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "test-server.js",
|
"main": "test-server.js",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
@ -17,7 +17,7 @@ Whether you have a **server**, a **NAS**, or a **Raspberry Pi** with application
|
||||||
* **Anti-Bot** 🤖❌ protections such as Captcha and IP rate limiting
|
* **Anti-Bot** 🤖❌ protections such as Captcha and IP rate limiting
|
||||||
* **Anti-DDOS** 🔥⛔️ protections such as variable timeouts/throttling, IP rate limiting and IP blacklisting
|
* **Anti-DDOS** 🔥⛔️ protections such as variable timeouts/throttling, IP rate limiting and IP blacklisting
|
||||||
* **Proper User Management** 🪪 ❎ to invite your friends and family to your applications without awkardly sharing credentials. Let them request a password change with an email rather than having you unlock their account manually!
|
* **Proper User Management** 🪪 ❎ to invite your friends and family to your applications without awkardly sharing credentials. Let them request a password change with an email rather than having you unlock their account manually!
|
||||||
* **Container Management** 🧱🔧 to easily manage your containers and their settings, keep them up to date as well as audit their security.
|
* **Container Management** 🐋🔧 to easily manage your containers and their settings, keep them up to date as well as audit their security.
|
||||||
* **Modular** 🧩📦 to easily add new features and integrations, but also run only the features you need (for example No docker, no Databases, or no HTTPS)
|
* **Modular** 🧩📦 to easily add new features and integrations, but also run only the features you need (for example No docker, no Databases, or no HTTPS)
|
||||||
* **Visible Source** 📖📝 for full transparency and trust
|
* **Visible Source** 📖📝 for full transparency and trust
|
||||||
|
|
||||||
|
@ -68,6 +68,8 @@ Installation is simple using Docker:
|
||||||
docker run -d -p 80:80 -p 443:443 --name cosmos-server --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/cosmos/config:/config azukaar/cosmos-server:latest
|
docker run -d -p 80:80 -p 443:443 --name cosmos-server --restart=always -v /var/run/docker.sock:/var/run/docker.sock -v /path/to/cosmos/config:/config azukaar/cosmos-server:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
make sure you expose the right ports (by default 80 / 443). It is best to keep those ports intacts, as Cosmos is meant to run as your reverse proxy. Trying to setup Cosmos behind another reverse proxy is possible but will only create headaches.
|
||||||
|
|
||||||
you can use `latest-arm64` for arm architecture (ex: NAS or Raspberry)
|
you can use `latest-arm64` for arm architecture (ex: NAS or Raspberry)
|
||||||
|
|
||||||
You can thing tweak the config file accordingly. Some settings can be changed before end with env var. [see here](https://github.com/azukaar/Cosmos-Server/wiki/Configuration).
|
You can thing tweak the config file accordingly. Some settings can be changed before end with env var. [see here](https://github.com/azukaar/Cosmos-Server/wiki/Configuration).
|
||||||
|
|
45
src/docker/api_newDB.go
Normal file
45
src/docker/api_newDB.go
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
func restart() {
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDBRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if utils.AdminOnly(w, req) != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(req.Method == "GET") {
|
||||||
|
costr, err := NewDB()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("NewDB: Error while creating new DB", err)
|
||||||
|
utils.HTTPError(w, "Error while creating new DB", http.StatusInternalServerError, "DB001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
config := utils.GetBaseMainConfig()
|
||||||
|
config.MongoDB = costr
|
||||||
|
utils.SaveConfigTofile(config)
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
})
|
||||||
|
|
||||||
|
go restart()
|
||||||
|
} else {
|
||||||
|
utils.Error("UserList: Method not allowed" + req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -6,7 +6,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func BootstrapAllContainersFromTags() []error {
|
func BootstrapAllContainersFromTags() []error {
|
||||||
errD := connect()
|
errD := Connect()
|
||||||
if errD != nil {
|
if errD != nil {
|
||||||
return []error{errD}
|
return []error{errD}
|
||||||
}
|
}
|
||||||
|
@ -32,7 +32,7 @@ func BootstrapAllContainersFromTags() []error {
|
||||||
|
|
||||||
|
|
||||||
func BootstrapContainerFromTags(containerID string) error {
|
func BootstrapContainerFromTags(containerID string) error {
|
||||||
errD := connect()
|
errD := Connect()
|
||||||
if errD != nil {
|
if errD != nil {
|
||||||
return errD
|
return errD
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,14 @@ package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/azukaar/cosmos-server/src/utils"
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
|
||||||
"github.com/docker/docker/client"
|
"github.com/docker/docker/client"
|
||||||
natting "github.com/docker/go-connections/nat"
|
// natting "github.com/docker/go-connections/nat"
|
||||||
"github.com/docker/docker/api/types/container"
|
"github.com/docker/docker/api/types/container"
|
||||||
network "github.com/docker/docker/api/types/network"
|
// network "github.com/docker/docker/api/types/network"
|
||||||
"github.com/docker/docker/api/types"
|
"github.com/docker/docker/api/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -35,11 +34,14 @@ func getIdFromName(name string) (string, error) {
|
||||||
return "", errors.New("Container not found")
|
return "", errors.New("Container not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func connect() error {
|
var DockerIsConnected = false
|
||||||
|
|
||||||
|
func Connect() error {
|
||||||
if DockerClient == nil {
|
if DockerClient == nil {
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
DockerIsConnected = false
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer client.Close()
|
defer client.Close()
|
||||||
|
@ -49,8 +51,10 @@ func connect() error {
|
||||||
|
|
||||||
ping, err := DockerClient.Ping(DockerContext)
|
ping, err := DockerClient.Ping(DockerContext)
|
||||||
if ping.APIVersion != "" && err == nil {
|
if ping.APIVersion != "" && err == nil {
|
||||||
|
DockerIsConnected = true
|
||||||
utils.Log("Docker Connected")
|
utils.Log("Docker Connected")
|
||||||
} else {
|
} else {
|
||||||
|
DockerIsConnected = false
|
||||||
utils.Error("Docker Connection - Cannot ping Daemon. Is it running?", nil)
|
utils.Error("Docker Connection - Cannot ping Daemon. Is it running?", nil)
|
||||||
return errors.New("Docker Connection - Cannot ping Daemon. Is it running?")
|
return errors.New("Docker Connection - Cannot ping Daemon. Is it running?")
|
||||||
}
|
}
|
||||||
|
@ -64,91 +68,8 @@ func connect() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func runContainer(imagename string, containername string, port string, inputEnv []string) error {
|
|
||||||
errD := connect()
|
|
||||||
if errD != nil {
|
|
||||||
utils.Error("Docker Connect", errD)
|
|
||||||
return errD
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define a PORT opening
|
|
||||||
newport, err := natting.NewPort("tcp", port)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Unable to create docker port")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configured hostConfig:
|
|
||||||
// https://godoc.org/github.com/docker/docker/api/types/container#HostConfig
|
|
||||||
hostConfig := &container.HostConfig{
|
|
||||||
PortBindings: natting.PortMap{
|
|
||||||
newport: []natting.PortBinding{
|
|
||||||
{
|
|
||||||
HostIP: "0.0.0.0",
|
|
||||||
HostPort: port,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
RestartPolicy: container.RestartPolicy{
|
|
||||||
Name: "always",
|
|
||||||
},
|
|
||||||
LogConfig: container.LogConfig{
|
|
||||||
Type: "json-file",
|
|
||||||
Config: map[string]string{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Define Network config (why isn't PORT in here...?:
|
|
||||||
// https://godoc.org/github.com/docker/docker/api/types/network#NetworkingConfig
|
|
||||||
networkConfig := &network.NetworkingConfig{
|
|
||||||
EndpointsConfig: map[string]*network.EndpointSettings{},
|
|
||||||
}
|
|
||||||
gatewayConfig := &network.EndpointSettings{
|
|
||||||
Gateway: "gatewayname",
|
|
||||||
}
|
|
||||||
networkConfig.EndpointsConfig["bridge"] = gatewayConfig
|
|
||||||
|
|
||||||
// Define ports to be exposed (has to be same as hostconfig.portbindings.newport)
|
|
||||||
exposedPorts := map[natting.Port]struct{}{
|
|
||||||
newport: struct{}{},
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration
|
|
||||||
// https://godoc.org/github.com/docker/docker/api/types/container#Config
|
|
||||||
config := &container.Config{
|
|
||||||
Image: imagename,
|
|
||||||
Env: inputEnv,
|
|
||||||
ExposedPorts: exposedPorts,
|
|
||||||
Hostname: fmt.Sprintf("%s-hostnameexample", imagename),
|
|
||||||
}
|
|
||||||
|
|
||||||
//archi := runtime.GOARCH
|
|
||||||
|
|
||||||
// Creating the actual container. This is "nil,nil,nil" in every example.
|
|
||||||
cont, err := DockerClient.ContainerCreate(
|
|
||||||
context.Background(),
|
|
||||||
config,
|
|
||||||
hostConfig,
|
|
||||||
networkConfig,
|
|
||||||
nil,
|
|
||||||
containername,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
utils.Error("Docker Container Create", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the actual container
|
|
||||||
DockerClient.ContainerStart(context.Background(), cont.ID, types.ContainerStartOptions{})
|
|
||||||
utils.Log("Container created " + cont.ID)
|
|
||||||
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func EditContainer(containerID string, newConfig types.ContainerJSON) (string, error) {
|
func EditContainer(containerID string, newConfig types.ContainerJSON) (string, error) {
|
||||||
errD := connect()
|
errD := Connect()
|
||||||
if errD != nil {
|
if errD != nil {
|
||||||
return "", errD
|
return "", errD
|
||||||
}
|
}
|
||||||
|
@ -212,7 +133,7 @@ func EditContainer(containerID string, newConfig types.ContainerJSON) (string, e
|
||||||
}
|
}
|
||||||
|
|
||||||
func ListContainers() ([]types.Container, error) {
|
func ListContainers() ([]types.Container, error) {
|
||||||
errD := connect()
|
errD := Connect()
|
||||||
if errD != nil {
|
if errD != nil {
|
||||||
return nil, errD
|
return nil, errD
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func DockerListenEvents() error {
|
func DockerListenEvents() error {
|
||||||
errD := connect()
|
errD := Connect()
|
||||||
if errD != nil {
|
if errD != nil {
|
||||||
utils.Error("Docker did not connect. Not listening", errD)
|
utils.Error("Docker did not connect. Not listening", errD)
|
||||||
return errD
|
return errD
|
||||||
|
|
|
@ -58,7 +58,7 @@ func CreateCosmosNetwork() (string, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func ConnectToSecureNetwork(containerConfig types.ContainerJSON) (bool, error) {
|
func ConnectToSecureNetwork(containerConfig types.ContainerJSON) (bool, error) {
|
||||||
errD := connect()
|
errD := Connect()
|
||||||
if errD != nil {
|
if errD != nil {
|
||||||
utils.Error("Docker Connect", errD)
|
utils.Error("Docker Connect", errD)
|
||||||
return false, errD
|
return false, errD
|
||||||
|
|
127
src/docker/run.go
Normal file
127
src/docker/run.go
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
// "github.com/docker/docker/client"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewDB() (string, error) {
|
||||||
|
mongoUser := "cosmos-" + utils.GenerateRandomString(5)
|
||||||
|
mongoPass := utils.GenerateRandomString(24)
|
||||||
|
monHost := "cosmos-mongo-" + utils.GenerateRandomString(3)
|
||||||
|
|
||||||
|
err := RunContainer(
|
||||||
|
"mongo:latest",
|
||||||
|
monHost,
|
||||||
|
[]string{
|
||||||
|
"MONGO_INITDB_ROOT_USERNAME=" + mongoUser,
|
||||||
|
"MONGO_INITDB_ROOT_PASSWORD=" + mongoPass,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return "mongodb://"+mongoUser+":"+mongoPass+"@"+monHost+":27017", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunContainer(imagename string, containername string, inputEnv []string) error {
|
||||||
|
errD := Connect()
|
||||||
|
if errD != nil {
|
||||||
|
utils.Error("Docker Connect", errD)
|
||||||
|
return errD
|
||||||
|
}
|
||||||
|
|
||||||
|
pull, errPull := DockerClient.ImagePull(DockerContext, imagename, types.ImagePullOptions{})
|
||||||
|
if errPull != nil {
|
||||||
|
utils.Error("Docker Pull", errPull)
|
||||||
|
return errPull
|
||||||
|
}
|
||||||
|
io.Copy(os.Stdout, pull)
|
||||||
|
|
||||||
|
|
||||||
|
// Define a PORT opening
|
||||||
|
// newport, err := natting.NewPort("tcp", port)
|
||||||
|
// if err != nil {
|
||||||
|
// fmt.Println("Unable to create docker port")
|
||||||
|
// return err
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Configured hostConfig:
|
||||||
|
// https://godoc.org/github.com/docker/docker/api/types/container#HostConfig
|
||||||
|
hostConfig := &container.HostConfig{
|
||||||
|
// PortBindings: natting.PortMap{
|
||||||
|
// newport: []natting.PortBinding{
|
||||||
|
// {
|
||||||
|
// HostIP: "0.0.0.0",
|
||||||
|
// HostPort: port,
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
RestartPolicy: container.RestartPolicy{
|
||||||
|
Name: "always",
|
||||||
|
},
|
||||||
|
// LogConfig: container.LogConfig{
|
||||||
|
// Type: "json-file",
|
||||||
|
// Config: map[string]string{},
|
||||||
|
// },
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define Network config
|
||||||
|
// https://godoc.org/github.com/docker/docker/api/types/network#NetworkingConfig
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// networkConfig := &network.NetworkingConfig{
|
||||||
|
// EndpointsConfig: map[string]*network.EndpointSettings{},
|
||||||
|
// }
|
||||||
|
// gatewayConfig := &network.EndpointSettings{
|
||||||
|
// Gateway: "gatewayname",
|
||||||
|
// }
|
||||||
|
// networkConfig.EndpointsConfig["bridge"] = gatewayConfig
|
||||||
|
|
||||||
|
// Define ports to be exposed (has to be same as hostconfig.portbindings.newport)
|
||||||
|
// exposedPorts := map[natting.Port]struct{}{
|
||||||
|
// newport: struct{}{},
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Configuration
|
||||||
|
// https://godoc.org/github.com/docker/docker/api/types/container#Config
|
||||||
|
config := &container.Config{
|
||||||
|
Image: imagename,
|
||||||
|
Env: inputEnv,
|
||||||
|
Hostname: containername,
|
||||||
|
Labels: map[string]string{
|
||||||
|
"cosmos-force-network-secured": "true",
|
||||||
|
},
|
||||||
|
// ExposedPorts: exposedPorts,
|
||||||
|
}
|
||||||
|
|
||||||
|
//archi := runtime.GOARCH
|
||||||
|
|
||||||
|
// Creating the actual container. This is "nil,nil,nil" in every example.
|
||||||
|
cont, err := DockerClient.ContainerCreate(
|
||||||
|
DockerContext,
|
||||||
|
config,
|
||||||
|
hostConfig,
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
containername,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("Docker Container Create", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the actual container
|
||||||
|
DockerClient.ContainerStart(DockerContext, cont.ID, types.ContainerStartOptions{})
|
||||||
|
utils.Log("Container created " + cont.ID)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -37,6 +37,7 @@ func startHTTPServer(router *mux.Router) {
|
||||||
func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
||||||
config := utils.GetMainConfig()
|
config := utils.GetMainConfig()
|
||||||
|
|
||||||
|
|
||||||
// check if Docker overwrite Hostname
|
// check if Docker overwrite Hostname
|
||||||
serverHostname := "0.0.0.0" //utils.GetMainConfig().HTTPConfig.Hostname
|
serverHostname := "0.0.0.0" //utils.GetMainConfig().HTTPConfig.Hostname
|
||||||
// if os.Getenv("HOSTNAME") != "" {
|
// if os.Getenv("HOSTNAME") != "" {
|
||||||
|
@ -50,13 +51,16 @@ func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
|
||||||
cfg.SSLEmail = config.HTTPConfig.SSLEmail
|
cfg.SSLEmail = config.HTTPConfig.SSLEmail
|
||||||
cfg.HTTPAddress = serverHostname+":"+serverPortHTTP
|
cfg.HTTPAddress = serverHostname+":"+serverPortHTTP
|
||||||
cfg.TLSAddress = serverHostname+":"+serverPortHTTPS
|
cfg.TLSAddress = serverHostname+":"+serverPortHTTPS
|
||||||
|
cfg.FailedToRenewCertificate = func(err error) {
|
||||||
|
utils.Error("Failed to renew certificate", err)
|
||||||
|
}
|
||||||
|
|
||||||
var certReloader *simplecert.CertReloader
|
var certReloader *simplecert.CertReloader
|
||||||
var errSimCert error
|
var errSimCert error
|
||||||
if(config.HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["LETSENCRYPT"]) {
|
if(config.HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["LETSENCRYPT"]) {
|
||||||
certReloader, errSimCert = simplecert.Init(cfg, nil)
|
certReloader, errSimCert = simplecert.Init(cfg, nil)
|
||||||
if errSimCert != nil {
|
if errSimCert != nil {
|
||||||
utils.Fatal("simplecert init failed: ", errSimCert)
|
utils.Fatal("simplecert init failed", errSimCert)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -185,6 +189,8 @@ func StartServer() {
|
||||||
|
|
||||||
srapi := router.PathPrefix("/cosmos").Subrouter()
|
srapi := router.PathPrefix("/cosmos").Subrouter()
|
||||||
|
|
||||||
|
srapi.HandleFunc("/api/status", StatusRoute)
|
||||||
|
srapi.HandleFunc("/api/newInstall", NewInstallRoute)
|
||||||
srapi.HandleFunc("/api/login", user.UserLogin)
|
srapi.HandleFunc("/api/login", user.UserLogin)
|
||||||
srapi.HandleFunc("/api/logout", user.UserLogout)
|
srapi.HandleFunc("/api/logout", user.UserLogout)
|
||||||
srapi.HandleFunc("/api/register", user.UserRegister)
|
srapi.HandleFunc("/api/register", user.UserRegister)
|
||||||
|
|
188
src/newInstall.go
Normal file
188
src/newInstall.go
Normal file
|
@ -0,0 +1,188 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
"os"
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
"github.com/azukaar/cosmos-server/src/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func waitForDB() {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
err := utils.DB()
|
||||||
|
if err != nil {
|
||||||
|
utils.Warn("DB Not ready yet")
|
||||||
|
waitForDB()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewInstallJSON struct {
|
||||||
|
MongoDBMode string `json:"mongodbMode"`
|
||||||
|
MongoDB string `json:"mongodb"`
|
||||||
|
HTTPSCertificateMode string `json:"httpsCertificateMode"`
|
||||||
|
TLSCert string `json:"tlsCert"`
|
||||||
|
TLSKey string `json:"tlsKey"`
|
||||||
|
Nickname string `json:"nickname"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
Hostname string `json:"hostname"`
|
||||||
|
Step string `json:"step"`
|
||||||
|
SSLEmail string `json:"sslEmail",validate:"if=HTTPSCertificateMode==LetsEncrypt,email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AdminJSON struct {
|
||||||
|
Nickname string `validate:"required,min=3,max=32,alphanum"`
|
||||||
|
Password string `validate:"required,min=8,max=128,containsany=!@#$%^&*()_+,containsany=ABCDEFGHIJKLMNOPQRSTUVWXYZ,containsany=abcdefghijklmnopqrstuvwxyz,containsany=0123456789"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInstallRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if !utils.GetMainConfig().NewInstall {
|
||||||
|
utils.Error("Status: not a new New install", nil)
|
||||||
|
utils.HTTPError(w, "New install", http.StatusForbidden, "NI001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(req.Method == "POST") {
|
||||||
|
var request NewInstallJSON
|
||||||
|
err1 := json.NewDecoder(req.Body).Decode(&request)
|
||||||
|
if err1 != nil {
|
||||||
|
utils.Error("NewInstall: Invalid User Request", err1)
|
||||||
|
utils.HTTPError(w, "New Install: Invalid User Request" + err1.Error(),
|
||||||
|
http.StatusInternalServerError, "NI001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
errV := utils.Validate.Struct(request)
|
||||||
|
if errV != nil {
|
||||||
|
utils.Error("NewInstall: Invalid User Request", errV)
|
||||||
|
utils.HTTPError(w, "New Install: Invalid User Request " + errV.Error(),
|
||||||
|
http.StatusInternalServerError, "NI001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
newConfig := utils.GetBaseMainConfig()
|
||||||
|
|
||||||
|
if(request.Step == "2") {
|
||||||
|
utils.Log("NewInstall: Step Database")
|
||||||
|
// User Management & Mongo DB
|
||||||
|
if(request.MongoDBMode == "DisableUserManagement") {
|
||||||
|
utils.Log("NewInstall: Disable User Management")
|
||||||
|
newConfig.DisableUserManagement = true
|
||||||
|
utils.SaveConfigTofile(newConfig)
|
||||||
|
utils.LoadBaseMainConfig(newConfig)
|
||||||
|
} else if (request.MongoDBMode == "Provided") {
|
||||||
|
utils.Log("NewInstall: DB Provided")
|
||||||
|
newConfig.DisableUserManagement = false
|
||||||
|
newConfig.MongoDB = request.MongoDB
|
||||||
|
utils.SaveConfigTofile(newConfig)
|
||||||
|
utils.LoadBaseMainConfig(newConfig)
|
||||||
|
} else if (request.MongoDBMode == "Create"){
|
||||||
|
utils.Log("NewInstall: Create DB")
|
||||||
|
newConfig.DisableUserManagement = false
|
||||||
|
strco, err := docker.NewDB()
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("NewInstall: Error creating MongoDB", err)
|
||||||
|
utils.HTTPError(w, "New Install: Error creating MongoDB " + err.Error(),
|
||||||
|
http.StatusInternalServerError, "NI001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
newConfig.MongoDB = strco
|
||||||
|
utils.SaveConfigTofile(newConfig)
|
||||||
|
utils.LoadBaseMainConfig(newConfig)
|
||||||
|
utils.Log("NewInstall: MongoDB created, waiting for it to be ready")
|
||||||
|
waitForDB()
|
||||||
|
} else {
|
||||||
|
utils.Log("NewInstall: Invalid MongoDBMode")
|
||||||
|
utils.Error("NewInstall: Invalid MongoDBMode", nil)
|
||||||
|
utils.HTTPError(w, "New Install: Invalid MongoDBMode",
|
||||||
|
http.StatusInternalServerError, "NI001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (request.Step == "3") {
|
||||||
|
// HTTPS Certificate Mode & Certs & Let's Encrypt
|
||||||
|
newConfig.HTTPConfig.HTTPSCertificateMode = request.HTTPSCertificateMode
|
||||||
|
newConfig.HTTPConfig.SSLEmail = request.SSLEmail
|
||||||
|
newConfig.HTTPConfig.TLSCert = request.TLSCert
|
||||||
|
newConfig.HTTPConfig.TLSKey = request.TLSKey
|
||||||
|
|
||||||
|
// Hostname
|
||||||
|
newConfig.HTTPConfig.Hostname = request.Hostname
|
||||||
|
|
||||||
|
utils.SaveConfigTofile(newConfig)
|
||||||
|
utils.LoadBaseMainConfig(newConfig)
|
||||||
|
} else if (request.Step == "4") {
|
||||||
|
|
||||||
|
adminObj := AdminJSON{
|
||||||
|
Nickname: request.Nickname,
|
||||||
|
Password: request.Password,
|
||||||
|
}
|
||||||
|
|
||||||
|
errV2 := utils.Validate.Struct(adminObj)
|
||||||
|
if errV2 != nil {
|
||||||
|
utils.Error("NewInstall: Invalid User Request", errV2)
|
||||||
|
utils.HTTPError(w, errV2.Error(), http.StatusInternalServerError, "UL001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Admin User
|
||||||
|
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
|
||||||
|
if errCo != nil {
|
||||||
|
utils.Error("Database Connect", errCo)
|
||||||
|
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
nickname := utils.Sanitize(request.Nickname)
|
||||||
|
hashedPassword, err2 := bcrypt.GenerateFromPassword([]byte(request.Password), 14)
|
||||||
|
|
||||||
|
if err2 != nil {
|
||||||
|
utils.Error("NewInstall: Error hashing password", err2)
|
||||||
|
utils.HTTPError(w, "New Install: Error hashing password " + err2.Error(),
|
||||||
|
http.StatusInternalServerError, "NI001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// pre-remove every users
|
||||||
|
_, err4 := c.DeleteMany(nil, map[string]interface{}{})
|
||||||
|
if err4 != nil {
|
||||||
|
utils.Error("NewInstall: Error deleting users", err4)
|
||||||
|
utils.HTTPError(w, "New Install: Error deleting users " + err4.Error(),
|
||||||
|
http.StatusInternalServerError, "NI001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err3 := c.InsertOne(nil, map[string]interface{}{
|
||||||
|
"Nickname": nickname,
|
||||||
|
"Email": request.Email,
|
||||||
|
"Password": hashedPassword,
|
||||||
|
"Role": utils.ADMIN,
|
||||||
|
"PasswordCycle": 0,
|
||||||
|
"CreatedAt": time.Now(),
|
||||||
|
"RegisteredAt": time.Now(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err3 != nil {
|
||||||
|
utils.Error("NewInstall: Error creating admin user", err3)
|
||||||
|
utils.HTTPError(w, "New Install: Error creating admin user " + err3.Error(),
|
||||||
|
http.StatusInternalServerError, "NI001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else if (request.Step == "5") {
|
||||||
|
newConfig.NewInstall = false
|
||||||
|
utils.SaveConfigTofile(newConfig)
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
utils.Error("UserList: Method not allowed" + req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
53
src/status.go
Normal file
53
src/status.go
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/azukaar/cosmos-server/src/utils"
|
||||||
|
"github.com/azukaar/cosmos-server/src/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func StatusRoute(w http.ResponseWriter, req *http.Request) {
|
||||||
|
if !utils.GetMainConfig().NewInstall && (utils.AdminOnly(w, req) != nil) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if(req.Method == "GET") {
|
||||||
|
utils.Log("API: Status")
|
||||||
|
|
||||||
|
databaseStatus := true
|
||||||
|
|
||||||
|
if(!utils.GetMainConfig().DisableUserManagement) {
|
||||||
|
err := utils.DB()
|
||||||
|
if err != nil {
|
||||||
|
utils.Error("Status: Database error", err)
|
||||||
|
databaseStatus = false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
utils.Log("Status: User management is disabled, skipping database check")
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!docker.DockerIsConnected) {
|
||||||
|
ed := docker.Connect()
|
||||||
|
if ed != nil {
|
||||||
|
utils.Error("Status: Docker error", ed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "OK",
|
||||||
|
"data": map[string]interface{}{
|
||||||
|
"database": databaseStatus,
|
||||||
|
"docker": docker.DockerIsConnected,
|
||||||
|
"letsencrypt": utils.GetMainConfig().HTTPConfig.HTTPSCertificateMode == "LETSENCRYPT" && utils.GetMainConfig().HTTPConfig.SSLEmail == "",
|
||||||
|
"domain": utils.GetMainConfig().HTTPConfig.Hostname == "localhost" || utils.GetMainConfig().HTTPConfig.Hostname == "0.0.0.0",
|
||||||
|
"HTTPSCertificateMode": utils.GetMainConfig().HTTPConfig.HTTPSCertificateMode,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
utils.Error("UserList: Method not allowed" + req.Method, nil)
|
||||||
|
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
|
@ -43,7 +43,12 @@ func UserCreate(w http.ResponseWriter, req *http.Request) {
|
||||||
nickname := utils.Sanitize(request.Nickname)
|
nickname := utils.Sanitize(request.Nickname)
|
||||||
email := utils.Sanitize(request.Email)
|
email := utils.Sanitize(request.Email)
|
||||||
|
|
||||||
c := utils.GetCollection(utils.GetRootAppId(), "users")
|
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
|
||||||
|
if errCo != nil {
|
||||||
|
utils.Error("Database Connect", errCo)
|
||||||
|
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
user := utils.User{}
|
user := utils.User{}
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,12 @@ func UserDelete(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
if(req.Method == "DELETE") {
|
if(req.Method == "DELETE") {
|
||||||
|
|
||||||
c := utils.GetCollection(utils.GetRootAppId(), "users")
|
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
|
||||||
|
if errCo != nil {
|
||||||
|
utils.Error("Database Connect", errCo)
|
||||||
|
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
utils.Debug("UserDeletion: Deleting user " + nickname)
|
utils.Debug("UserDeletion: Deleting user " + nickname)
|
||||||
|
|
||||||
|
|
|
@ -36,7 +36,12 @@ func UserEdit(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c := utils.GetCollection(utils.GetRootAppId(), "users")
|
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
|
||||||
|
if errCo != nil {
|
||||||
|
utils.Error("Database Connect", errCo)
|
||||||
|
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
utils.Debug("UserEdit: Edit user " + nickname)
|
utils.Debug("UserEdit: Edit user " + nickname)
|
||||||
|
|
||||||
|
|
|
@ -17,11 +17,16 @@ func UserGet(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
if utils.AdminOrItselfOnly(w, req, nickname) != nil {
|
if utils.AdminOrItselfOnly(w, req, nickname) != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if(req.Method == "GET") {
|
if(req.Method == "GET") {
|
||||||
|
|
||||||
c := utils.GetCollection(utils.GetRootAppId(), "users")
|
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
|
||||||
|
if errCo != nil {
|
||||||
|
utils.Error("Database Connect", errCo)
|
||||||
|
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
utils.Debug("UserGet: Get user " + nickname)
|
utils.Debug("UserGet: Get user " + nickname)
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,12 @@ func UserList(w http.ResponseWriter, req *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(req.Method == "GET") {
|
if(req.Method == "GET") {
|
||||||
c := utils.GetCollection(utils.GetRootAppId(), "users")
|
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
|
||||||
|
if errCo != nil {
|
||||||
|
utils.Error("Database Connect", errCo)
|
||||||
|
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
utils.Debug("UserList: List user ")
|
utils.Debug("UserList: List user ")
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,12 @@ func UserLogin(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c := utils.GetCollection(utils.GetRootAppId(), "users")
|
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
|
||||||
|
if errCo != nil {
|
||||||
|
utils.Error("Database Connect", errCo)
|
||||||
|
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
nickname := utils.Sanitize(request.Nickname)
|
nickname := utils.Sanitize(request.Nickname)
|
||||||
password := request.Password
|
password := request.Password
|
||||||
|
|
|
@ -50,7 +50,12 @@ func UserRegister(w http.ResponseWriter, req *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c := utils.GetCollection(utils.GetRootAppId(), "users")
|
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
|
||||||
|
if errCo != nil {
|
||||||
|
utils.Error("Database Connect", errCo)
|
||||||
|
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
user := utils.User{}
|
user := utils.User{}
|
||||||
|
|
||||||
|
|
|
@ -31,7 +31,12 @@ func UserResendInviteLink(w http.ResponseWriter, req *http.Request) {
|
||||||
|
|
||||||
utils.Debug("Re-Sending an invite to " + nickname)
|
utils.Debug("Re-Sending an invite to " + nickname)
|
||||||
|
|
||||||
c := utils.GetCollection(utils.GetRootAppId(), "users")
|
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
|
||||||
|
if errCo != nil {
|
||||||
|
utils.Error("Database Connect", errCo)
|
||||||
|
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
user := utils.User{}
|
user := utils.User{}
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,30 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
"encoding/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, error) {
|
func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, error) {
|
||||||
|
// if(utils.DB != nil) {
|
||||||
|
// return utils.User{
|
||||||
|
// Nickname: "noname",
|
||||||
|
// Role: utils.ADMIN,
|
||||||
|
// }, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// if new install
|
||||||
|
if utils.GetMainConfig().NewInstall {
|
||||||
|
// check route
|
||||||
|
if req.URL.Path != "/cosmos/api/status" && req.URL.Path != "/cosmos/api/newInstall" {
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"status": "NEW_INSTALL",
|
||||||
|
})
|
||||||
|
return utils.User{}, errors.New("New install")
|
||||||
|
} else {
|
||||||
|
return utils.User{}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
cookie, err := req.Cookie("jwttoken")
|
cookie, err := req.Cookie("jwttoken")
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -63,7 +84,12 @@ func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, err
|
||||||
|
|
||||||
userInBase := utils.User{}
|
userInBase := utils.User{}
|
||||||
|
|
||||||
c := utils.GetCollection(utils.GetRootAppId(), "users")
|
c, errCo := utils.GetCollection(utils.GetRootAppId(), "users")
|
||||||
|
if errCo != nil {
|
||||||
|
utils.Error("Database Connect", errCo)
|
||||||
|
utils.HTTPError(w, "Database", http.StatusInternalServerError, "DB001")
|
||||||
|
return utils.User{}, errCo
|
||||||
|
}
|
||||||
|
|
||||||
errDB := c.FindOne(nil, map[string]interface{}{
|
errDB := c.FindOne(nil, map[string]interface{}{
|
||||||
"Nickname": nickname,
|
"Nickname": nickname,
|
||||||
|
@ -101,9 +127,11 @@ func logOutUser(w http.ResponseWriter) {
|
||||||
Domain: utils.GetMainConfig().HTTPConfig.Hostname,
|
Domain: utils.GetMainConfig().HTTPConfig.Hostname,
|
||||||
}
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &cookie)
|
if(utils.GetMainConfig().HTTPConfig.Hostname == "localhost" || utils.GetMainConfig().HTTPConfig.Hostname == "0.0.0.0") {
|
||||||
|
cookie.Domain = ""
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: Remove all other cookies from apps
|
http.SetCookie(w, &cookie)
|
||||||
|
|
||||||
// TODO: logout every other device if asked by increasing passwordcycle
|
// TODO: logout every other device if asked by increasing passwordcycle
|
||||||
}
|
}
|
||||||
|
@ -151,6 +179,10 @@ func SendUserToken(w http.ResponseWriter, user utils.User) {
|
||||||
Domain: utils.GetMainConfig().HTTPConfig.Hostname,
|
Domain: utils.GetMainConfig().HTTPConfig.Hostname,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(utils.GetMainConfig().HTTPConfig.Hostname == "localhost" || utils.GetMainConfig().HTTPConfig.Hostname == "0.0.0.0") {
|
||||||
|
cookie.Domain = ""
|
||||||
|
}
|
||||||
|
|
||||||
http.SetCookie(w, &cookie)
|
http.SetCookie(w, &cookie)
|
||||||
// http.SetCookie(w, &cookie2)
|
// http.SetCookie(w, &cookie2)
|
||||||
}
|
}
|
|
@ -3,6 +3,7 @@ package utils
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"errors"
|
||||||
"go.mongodb.org/mongo-driver/mongo"
|
"go.mongodb.org/mongo-driver/mongo"
|
||||||
"go.mongodb.org/mongo-driver/mongo/options"
|
"go.mongodb.org/mongo-driver/mongo/options"
|
||||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||||
|
@ -11,26 +12,35 @@ import (
|
||||||
|
|
||||||
var client *mongo.Client
|
var client *mongo.Client
|
||||||
|
|
||||||
func DB() {
|
func DB() error {
|
||||||
Log("Connecting to the database...")
|
if(GetBaseMainConfig().DisableUserManagement) {
|
||||||
|
return errors.New("User Management is disabled")
|
||||||
|
}
|
||||||
|
|
||||||
uri := MainConfig.MongoDB + "/?retryWrites=true&w=majority"
|
uri := MainConfig.MongoDB + "/?retryWrites=true&w=majority"
|
||||||
|
|
||||||
|
if(client != nil && client.Ping(context.TODO(), readpref.Primary()) == nil) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
Log("(Re) Connecting to the database...")
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
client, err = mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
|
client, err = mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
Fatal("DB", err)
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer func() {
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// Ping the primary
|
// Ping the primary
|
||||||
if err := client.Ping(context.TODO(), readpref.Primary()); err != nil {
|
if err := client.Ping(context.TODO(), readpref.Primary()); err != nil {
|
||||||
Fatal("DB", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
Log("Successfully connected to the database.")
|
Log("Successfully connected to the database.")
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func Disconnect() {
|
func Disconnect() {
|
||||||
|
@ -39,9 +49,12 @@ func Disconnect() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCollection(applicationId string, collection string) *mongo.Collection {
|
func GetCollection(applicationId string, collection string) (*mongo.Collection, error) {
|
||||||
if client == nil {
|
if client == nil {
|
||||||
DB()
|
errCo := DB()
|
||||||
|
if errCo != nil {
|
||||||
|
return nil, errCo
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
name := os.Getenv("MONGODB_NAME"); if name == "" {
|
name := os.Getenv("MONGODB_NAME"); if name == "" {
|
||||||
|
@ -52,7 +65,7 @@ func GetCollection(applicationId string, collection string) *mongo.Collection {
|
||||||
|
|
||||||
c := client.Database(name).Collection(applicationId + "_" + collection)
|
c := client.Database(name).Collection(applicationId + "_" + collection)
|
||||||
|
|
||||||
return c
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func query(q string) (*sql.Rows, error) {
|
// func query(q string) (*sql.Rows, error) {
|
||||||
|
|
|
@ -74,6 +74,8 @@ type Config struct {
|
||||||
LoggingLevel LoggingLevel `validate:"oneof=DEBUG INFO WARNING ERROR"`
|
LoggingLevel LoggingLevel `validate:"oneof=DEBUG INFO WARNING ERROR"`
|
||||||
MongoDB string
|
MongoDB string
|
||||||
HTTPConfig HTTPConfig
|
HTTPConfig HTTPConfig
|
||||||
|
DisableUserManagement bool
|
||||||
|
NewInstall bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTPConfig struct {
|
type HTTPConfig struct {
|
||||||
|
|
|
@ -16,6 +16,7 @@ var IsHTTPS = false
|
||||||
|
|
||||||
var DefaultConfig = Config{
|
var DefaultConfig = Config{
|
||||||
LoggingLevel: "INFO",
|
LoggingLevel: "INFO",
|
||||||
|
NewInstall: true,
|
||||||
HTTPConfig: HTTPConfig{
|
HTTPConfig: HTTPConfig{
|
||||||
HTTPSCertificateMode: "DISABLED",
|
HTTPSCertificateMode: "DISABLED",
|
||||||
GenerateMissingAuthCert: true,
|
GenerateMissingAuthCert: true,
|
||||||
|
@ -278,5 +279,14 @@ func GetAllHostnames() []string {
|
||||||
hostnames = append(hostnames, proxy.Host)
|
hostnames = append(hostnames, proxy.Host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hostnames
|
// remove doubles
|
||||||
|
seen := make(map[string]bool)
|
||||||
|
uniqueHostnames := []string{}
|
||||||
|
for _, hostname := range hostnames {
|
||||||
|
if _, ok := seen[hostname]; !ok {
|
||||||
|
seen[hostname] = true
|
||||||
|
uniqueHostnames = append(uniqueHostnames, hostname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return uniqueHostnames
|
||||||
}
|
}
|
Loading…
Reference in a new issue