v0.0.9 setup wizard

This commit is contained in:
Yann Stepienik 2023-03-29 21:38:50 +01:00
parent 35b6f9b488
commit 822d4bc057
38 changed files with 1243 additions and 133 deletions

1
.gitignore vendored
View file

@ -6,6 +6,7 @@ static
client/dist
client/.vite
config_dev.json
config_dev.old.json
tests
todo.txt
LICENCE

View file

@ -8,7 +8,17 @@ function list() {
},
}))
}
const newDB = () => {
return wrap(fetch('/cosmos/api/newDB', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
export {
list,
newDB,
};

View file

@ -2,9 +2,33 @@ import * as auth from './authentication.jsx';
import * as users from './users.jsx';
import * as config from './config.jsx';
import * as docker from './docker.jsx';
import wrap from './wrap';
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 {
auth,
users,
config,
docker
docker,
getStatus,
newInstall,
};

View file

@ -13,7 +13,9 @@ export default function wrap(apicall) {
return rep;
}
snackit(rep.message);
throw new Error(rep.message);
const e = new Error(rep.message);
e.status = response.status;
throw e;
});
}

View file

@ -9,7 +9,7 @@ import logo from '../icons/cosmos.png';
const AuthBackground = () => {
const theme = useTheme();
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" />
</Box>
);

View file

@ -6,7 +6,10 @@ const isLoggedIn = () => useEffect(() => {
console.log("CHECK LOGIN")
API.auth.me().then((data) => {
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';
}
});
}, []);

View file

@ -10,11 +10,16 @@ import AuthFooter from '../../components/cards/AuthFooter';
// assets
import AuthBackground from '../../assets/images/auth/AuthBackground';
import { useTheme } from '@mui/material/styles';
// ==============================|| AUTHENTICATION - WRAPPER ||============================== //
const AuthWrapper = ({ children }) => (
<Box sx={{ minHeight: '100vh' }}>
const AuthWrapper = ({ children }) => {
const theme = useTheme();
const darkMode = theme.palette.mode === 'dark';
return <Box sx={{ minHeight: '100vh',
background: darkMode ? 'none' : '#f0efef' }}>
<AuthBackground />
<Grid
container
@ -34,7 +39,12 @@ const AuthWrapper = ({ children }) => (
container
justifyContent="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>
<AuthCard>{children}</AuthCard>
@ -46,7 +56,7 @@ const AuthWrapper = ({ children }) => (
</Grid>
</Grid>
</Box>
);
};
AuthWrapper.propTypes = {
children: PropTypes.node

View file

@ -57,6 +57,8 @@ const AuthLogin = () => {
API.auth.me().then((data) => {
if(data.status == 'OK') {
window.location.href = redirectTo;
} else if(data.status == 'NEW_INSTALL') {
window.location.href = '/ui/newInstall';
}
});
});

View file

@ -15,13 +15,19 @@ import {
AccordionDetails,
Accordion,
Chip,
Box,
FormControl,
IconButton,
InputAdornment,
} from '@mui/material';
import { Field } from 'formik';
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}>
<Stack spacing={1}>
<InputLabel htmlFor={name}>{label}</InputLabel>
@ -31,7 +37,12 @@ export const CosmosInputText = ({ name, type, placeholder, label, formik }) => {
value={formik.values[name]}
name={name}
onBlur={formik.handleBlur}
onChange={formik.handleChange}
onChange={(...e) => {
if (onChange) {
onChange(...e);
}
formik.handleChange(...e);
}}
placeholder={placeholder}
fullWidth
error={Boolean(formik.touched[name] && formik.errors[name])}
@ -45,6 +56,78 @@ export const CosmosInputText = ({ name, type, placeholder, label, formik }) => {
</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 }) => {
return <Grid item xs={12}>
<Stack spacing={1}>

View file

@ -42,7 +42,7 @@ const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute })
initialValues={{
Name: routeConfig.Name,
Description: routeConfig.Description,
Mode: routeConfig.Mode,
Mode: routeConfig.Mode || "SERVAPP",
Target: routeConfig.Target,
UseHost: routeConfig.UseHost,
AuthEnabled: routeConfig.AuthEnabled,

View file

@ -1,4 +1,4 @@
import { useState } from 'react';
import { useEffect, useState } from 'react';
// material-ui
import {
@ -36,6 +36,9 @@ import avatar3 from '../../assets/images/users/avatar-3.png';
import avatar4 from '../../assets/images/users/avatar-4.png';
import isLoggedIn from '../../isLoggedIn';
import * as API from '../../api';
import AnimateButton from '../../components/@extended/AnimateButton';
// avatar style
const avatarSX = {
width: 36,
@ -77,10 +80,59 @@ const DashboardDefault = () => {
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 (
<>
<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 style={{filter: 'blur(10px)', marginTop: '30px', pointerEvents: 'none'}}>
<Grid container rowSpacing={4.5} columnSpacing={2.75}>

View 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;

View file

@ -5,6 +5,7 @@ import { lazy } from 'react';
import Loadable from '../components/Loadable';
import MinimalLayout from '../layout/MinimalLayout';
import Logout from '../pages/authentication/Logoff';
import NewInstall from '../pages/newInstall/newInstall';
// render - login
const AuthLogin = Loadable(lazy(() => import('../pages/authentication/Login')));
@ -27,7 +28,12 @@ const LoginRoutes = {
{
path: '/ui/logout',
element: <Logout />
}
},
{
path: '/ui/newInstall',
// redirect to /ui
element: <NewInstall />
},
]
};

View file

@ -51,7 +51,7 @@ const MainRoutes = {
{
path: '/ui/config/proxy',
element: <ProxyManagement />
}
},
]
};

View 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}) => {
}

View file

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.0.8",
"version": "0.0.9",
"description": "",
"main": "test-server.js",
"bugs": {

View file

@ -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-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!
* **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)
* **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
```
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 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
View 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
}
}

View file

@ -6,7 +6,7 @@ import (
)
func BootstrapAllContainersFromTags() []error {
errD := connect()
errD := Connect()
if errD != nil {
return []error{errD}
}
@ -32,7 +32,7 @@ func BootstrapAllContainersFromTags() []error {
func BootstrapContainerFromTags(containerID string) error {
errD := connect()
errD := Connect()
if errD != nil {
return errD
}

View file

@ -2,15 +2,14 @@ package docker
import (
"context"
"fmt"
"errors"
"github.com/azukaar/cosmos-server/src/utils"
"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"
network "github.com/docker/docker/api/types/network"
// network "github.com/docker/docker/api/types/network"
"github.com/docker/docker/api/types"
)
@ -35,11 +34,14 @@ func getIdFromName(name string) (string, error) {
return "", errors.New("Container not found")
}
func connect() error {
var DockerIsConnected = false
func Connect() error {
if DockerClient == nil {
ctx := context.Background()
client, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
DockerIsConnected = false
return err
}
defer client.Close()
@ -49,8 +51,10 @@ func connect() error {
ping, err := DockerClient.Ping(DockerContext)
if ping.APIVersion != "" && err == nil {
DockerIsConnected = true
utils.Log("Docker Connected")
} else {
DockerIsConnected = false
utils.Error("Docker Connection - Cannot ping Daemon. Is it running?", nil)
return errors.New("Docker Connection - Cannot ping Daemon. Is it running?")
}
@ -64,91 +68,8 @@ func connect() error {
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) {
errD := connect()
errD := Connect()
if errD != nil {
return "", errD
}
@ -212,7 +133,7 @@ func EditContainer(containerID string, newConfig types.ContainerJSON) (string, e
}
func ListContainers() ([]types.Container, error) {
errD := connect()
errD := Connect()
if errD != nil {
return nil, errD
}

View file

@ -9,7 +9,7 @@ import (
)
func DockerListenEvents() error {
errD := connect()
errD := Connect()
if errD != nil {
utils.Error("Docker did not connect. Not listening", errD)
return errD

View file

@ -58,7 +58,7 @@ func CreateCosmosNetwork() (string, error) {
}
func ConnectToSecureNetwork(containerConfig types.ContainerJSON) (bool, error) {
errD := connect()
errD := Connect()
if errD != nil {
utils.Error("Docker Connect", errD)
return false, errD

127
src/docker/run.go Normal file
View 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
}

View file

@ -37,6 +37,7 @@ func startHTTPServer(router *mux.Router) {
func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
config := utils.GetMainConfig()
// check if Docker overwrite Hostname
serverHostname := "0.0.0.0" //utils.GetMainConfig().HTTPConfig.Hostname
// if os.Getenv("HOSTNAME") != "" {
@ -50,13 +51,16 @@ func startHTTPSServer(router *mux.Router, tlsCert string, tlsKey string) {
cfg.SSLEmail = config.HTTPConfig.SSLEmail
cfg.HTTPAddress = serverHostname+":"+serverPortHTTP
cfg.TLSAddress = serverHostname+":"+serverPortHTTPS
cfg.FailedToRenewCertificate = func(err error) {
utils.Error("Failed to renew certificate", err)
}
var certReloader *simplecert.CertReloader
var errSimCert error
if(config.HTTPConfig.HTTPSCertificateMode == utils.HTTPSCertModeList["LETSENCRYPT"]) {
certReloader, errSimCert = simplecert.Init(cfg, 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.HandleFunc("/api/status", StatusRoute)
srapi.HandleFunc("/api/newInstall", NewInstallRoute)
srapi.HandleFunc("/api/login", user.UserLogin)
srapi.HandleFunc("/api/logout", user.UserLogout)
srapi.HandleFunc("/api/register", user.UserRegister)

188
src/newInstall.go Normal file
View 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
View 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
}
}

View file

@ -43,7 +43,12 @@ func UserCreate(w http.ResponseWriter, req *http.Request) {
nickname := utils.Sanitize(request.Nickname)
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{}

View file

@ -18,7 +18,12 @@ func UserDelete(w http.ResponseWriter, req *http.Request) {
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)

View file

@ -36,7 +36,12 @@ func UserEdit(w http.ResponseWriter, req *http.Request) {
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)

View file

@ -17,11 +17,16 @@ func UserGet(w http.ResponseWriter, req *http.Request) {
if utils.AdminOrItselfOnly(w, req, nickname) != nil {
return
}
}
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)

View file

@ -24,7 +24,12 @@ func UserList(w http.ResponseWriter, req *http.Request) {
}
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 ")

View file

@ -28,7 +28,12 @@ func UserLogin(w http.ResponseWriter, req *http.Request) {
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)
password := request.Password

View file

@ -50,7 +50,12 @@ func UserRegister(w http.ResponseWriter, req *http.Request) {
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{}

View file

@ -31,7 +31,12 @@ func UserResendInviteLink(w http.ResponseWriter, req *http.Request) {
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{}

View file

@ -7,9 +7,30 @@ import (
"errors"
"strings"
"time"
"encoding/json"
)
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")
if err != nil {
@ -63,7 +84,12 @@ func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, err
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{}{
"Nickname": nickname,
@ -101,9 +127,11 @@ func logOutUser(w http.ResponseWriter) {
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
}
@ -151,6 +179,10 @@ func SendUserToken(w http.ResponseWriter, user utils.User) {
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, &cookie2)
}

View file

@ -3,6 +3,7 @@ package utils
import (
"context"
"os"
"errors"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.mongodb.org/mongo-driver/mongo/readpref"
@ -11,26 +12,35 @@ import (
var client *mongo.Client
func DB() {
Log("Connecting to the database...")
func DB() error {
if(GetBaseMainConfig().DisableUserManagement) {
return errors.New("User Management is disabled")
}
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
client, err = mongo.Connect(context.TODO(), options.Client().ApplyURI(uri))
if err != nil {
Fatal("DB", err)
return err
}
defer func() {
}()
// Ping the primary
if err := client.Ping(context.TODO(), readpref.Primary()); err != nil {
Fatal("DB", err)
return err
}
Log("Successfully connected to the database.")
return nil
}
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 {
DB()
errCo := DB()
if errCo != nil {
return nil, errCo
}
}
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)
return c
return c, nil
}
// func query(q string) (*sql.Rows, error) {

View file

@ -74,6 +74,8 @@ type Config struct {
LoggingLevel LoggingLevel `validate:"oneof=DEBUG INFO WARNING ERROR"`
MongoDB string
HTTPConfig HTTPConfig
DisableUserManagement bool
NewInstall bool
}
type HTTPConfig struct {

View file

@ -16,6 +16,7 @@ var IsHTTPS = false
var DefaultConfig = Config{
LoggingLevel: "INFO",
NewInstall: true,
HTTPConfig: HTTPConfig{
HTTPSCertificateMode: "DISABLED",
GenerateMissingAuthCert: true,
@ -278,5 +279,14 @@ func GetAllHostnames() []string {
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
}