Proxy settings + register page

This commit is contained in:
Yann Stepienik 2023-03-16 18:56:36 +00:00
parent 17e42b8ae0
commit 28a44da3ad
43 changed files with 1239 additions and 287 deletions

View file

@ -58,7 +58,7 @@ jobs:
- run: - run:
name: Build UI name: Build UI
command: node .bin/vite build command: node .bin/vite build --base=/ui/
- run: - run:
name: Build Linux (ARM) name: Build Linux (ARM)

View file

@ -1,13 +1,13 @@
import wrap from './wrap';
function login(values) { function login(values) {
return fetch('/cosmos/api/login', { return wrap(fetch('/cosmos/api/login', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(values) body: JSON.stringify(values)
}) }))
.then((res) => res.json())
} }
function me() { function me() {
@ -20,7 +20,17 @@ function me() {
.then((res) => res.json()) .then((res) => res.json())
} }
function logout() {
return wrap(fetch('/cosmos/api/logout/', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}))
}
export { export {
login, login,
logout,
me me
}; };

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

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

View file

@ -1,6 +1,8 @@
import * as auth from './authentication.jsx'; import * as auth from './authentication.jsx';
import * as users from './users.jsx'; import * as users from './users.jsx';
import * as config from './config.jsx';
export { export {
auth, auth,
users users,
config
}; };

View file

@ -1,17 +1,15 @@
import wrap from './wrap'; import wrap from './wrap';
function list() { function list() {
return fetch('/cosmos/api/users', { return wrap(fetch('/cosmos/api/users', {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
}) }))
.then((res) => res.json())
} }
function create(values) { function create(values) {
alert(JSON.stringify(values))
return wrap(fetch('/cosmos/api/users', { return wrap(fetch('/cosmos/api/users', {
method: 'POST', method: 'POST',
headers: { headers: {
@ -22,16 +20,13 @@ function create(values) {
} }
function register(values) { function register(values) {
return fetch('/cosmos/api/register', { return wrap(fetch('/cosmos/api/register', {
method: 'POST', method: 'POST',
headers: { headers: {
headers: { 'Content-Type': 'application/json'
'Content-Type': 'application/json'
},
body: JSON.stringify(values),
}, },
}) body: JSON.stringify(values),
.then((res) => res.json()) }))
} }
function invite(values) { function invite(values) {
@ -45,34 +40,31 @@ function invite(values) {
} }
function edit(nickname, values) { function edit(nickname, values) {
return fetch('/cosmos/api/users/'+nickname, { return wrap(fetch('/cosmos/api/users/'+nickname, {
method: 'PATCH', method: 'PATCH',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(values), body: JSON.stringify(values),
}) }))
.then((res) => res.json())
} }
function get(nickname) { function get(nickname) {
return fetch('/cosmos/api/users/'+nickname, { return wrap(fetch('/cosmos/api/users/'+nickname, {
method: 'GET', method: 'GET',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
}) }))
.then((res) => res.json())
} }
function deleteUser(nickname) { function deleteUser(nickname) {
return fetch('/cosmos/api/users/'+nickname, { return wrap(fetch('/cosmos/api/users/'+nickname, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
}) }))
.then((res) => res.json())
} }
export { export {

View file

@ -7,7 +7,7 @@ export default function wrap(apicall) {
return rep; return rep;
} }
snackit(rep.message); snackit(rep.message);
throw new Error(rep); throw new Error(rep.message);
}); });
} }

View file

@ -40,4 +40,8 @@
.shake { .shake {
animation: shake 1s; animation: shake 1s;
animation-iteration-count: 1; animation-iteration-count: 1;
}
.code {
background-color: rgba(0.2,0.2,0.2,0.2);
} }

View file

@ -3,11 +3,12 @@ import * as API from './api';
import { useEffect } from 'react'; import { useEffect } from 'react';
const isLoggedIn = () => useEffect(() => { const isLoggedIn = () => useEffect(() => {
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 = '/login'; window.location.href = '/ui/login';
} }
}); });
}); }, []);
export default isLoggedIn; export default isLoggedIn;

View file

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

View file

@ -17,7 +17,7 @@ const dashboard = {
id: 'home', id: 'home',
title: 'Home', title: 'Home',
type: 'item', type: 'item',
url: '/', url: '/ui',
icon: icons.HomeOutlined, icon: icons.HomeOutlined,
breadcrumbs: false breadcrumbs: false
} }

View file

@ -19,21 +19,21 @@ const pages = {
id: 'proxy', id: 'proxy',
title: 'Proxy Routes', title: 'Proxy Routes',
type: 'item', type: 'item',
url: '/config/proxy', url: '/ui/config/proxy',
icon: icons.NodeExpandOutlined, icon: icons.NodeExpandOutlined,
}, },
{ {
id: 'users', id: 'users',
title: 'Manage Users', title: 'Manage Users',
type: 'item', type: 'item',
url: '/config/users', url: '/ui/config/users',
icon: icons.ProfileOutlined, icon: icons.ProfileOutlined,
}, },
{ {
id: 'config', id: 'config',
title: 'Configuration', title: 'Configuration',
type: 'item', type: 'item',
url: '/config/general', url: '/ui/config/general',
icon: icons.SettingOutlined, icon: icons.SettingOutlined,
} }
] ]

View file

@ -0,0 +1,36 @@
import { Link } from 'react-router-dom';
// material-ui
import { Grid, Stack, Typography } from '@mui/material';
// project import
import AuthRegister from './auth-forms/AuthRegister';
import AuthWrapper from './AuthWrapper';
import { useEffect } from 'react';
import * as API from '../../api';
// ================================|| REGISTER ||================================ //
const Logout = () => {
useEffect(() => {
API.auth.logout()
.then(() => {
setTimeout(() => {
window.location.href = '/ui/login';
}, 2000);
});
},[]);
return <AuthWrapper>
<Grid container spacing={3}>
<Grid item xs={12}>
<Typography variant="h3">
You have been logged off. Redirecting you...
</Typography>
</Grid>
</Grid>
</AuthWrapper>;
}
export default Logout;

View file

@ -4,27 +4,38 @@ import { Link } from 'react-router-dom';
import { Grid, Stack, Typography } from '@mui/material'; import { Grid, Stack, Typography } from '@mui/material';
// project import // project import
import FirebaseRegister from './auth-forms/AuthRegister'; import AuthRegister from './auth-forms/AuthRegister';
import AuthWrapper from './AuthWrapper'; import AuthWrapper from './AuthWrapper';
// ================================|| REGISTER ||================================ // // ================================|| REGISTER ||================================ //
const Register = () => ( const Register = () => {
<AuthWrapper> const urlSearchParams = new URLSearchParams(window.location.search);
const formType = urlSearchParams.get('t');
const isInviteLink = formType === '2';
const isRegister = formType === '1';
const nickname = urlSearchParams.get('nickname');
const regkey = urlSearchParams.get('key');
return <AuthWrapper>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12}> <Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}> <Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: { xs: -0.5, sm: 0.5 } }}>
<Typography variant="h3">Sign up</Typography> <Typography variant="h3">{
<Typography component={Link} to="/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary"> isInviteLink ? 'Invitation' : 'Password Reset'
Already have an account? }</Typography>
</Typography>
</Stack> </Stack>
</Grid> </Grid>
<Grid item xs={12}> <Grid item xs={12}>
<FirebaseRegister /> <AuthRegister
nickname={nickname}
isRegister={isRegister}
isInviteLink={isInviteLink}
regkey={regkey}
/>
</Grid> </Grid>
</Grid> </Grid>
</AuthWrapper> </AuthWrapper>;
); }
export default Register; export default Register;

View file

@ -0,0 +1,30 @@
import { Link } from 'react-router-dom';
// material-ui
import { Grid, Stack, Typography } from '@mui/material';
// project import
import FirebaseRegister from './auth-forms/AuthRegister';
import AuthWrapper from './AuthWrapper';
// ================================|| REGISTER ||================================ //
const Signup = () => (
<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">Sign up</Typography>
<Typography component={Link} to="/ui/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
Already have an account?
</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<FirebaseRegister />
</Grid>
</Grid>
</AuthWrapper>
);
export default Signup;

View file

@ -51,7 +51,7 @@ const AuthLogin = () => {
const urlSearchParams = new URLSearchParams(window.location.search); const urlSearchParams = new URLSearchParams(window.location.search);
const notLogged = urlSearchParams.get('notlogged') == 1; const notLogged = urlSearchParams.get('notlogged') == 1;
const invalid = urlSearchParams.get('invalid') == 1; const invalid = urlSearchParams.get('invalid') == 1;
const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/'; const redirectTo = urlSearchParams.get('redirect') ? urlSearchParams.get('redirect') : '/ui';
useEffect(() => { useEffect(() => {
API.auth.me().then((data) => { API.auth.me().then((data) => {
@ -170,7 +170,7 @@ const AuthLogin = () => {
<Grid item xs={12} sx={{ mt: -1 }}> <Grid item xs={12} sx={{ mt: -1 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}> <Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<FormControlLabel {/* <FormControlLabel
control={ control={
<Checkbox <Checkbox
checked={checked} checked={checked}
@ -184,7 +184,7 @@ const AuthLogin = () => {
/> />
<Link variant="h6" component={RouterLink} to="" color="text.primary"> <Link variant="h6" component={RouterLink} to="" color="text.primary">
Forgot Password? Forgot Password?
</Link> </Link> */}
</Stack> </Stack>
</Grid> </Grid>
{errors.submit && ( {errors.submit && (

View file

@ -15,13 +15,16 @@ import {
InputLabel, InputLabel,
OutlinedInput, OutlinedInput,
Stack, Stack,
Typography Typography,
Alert
} from '@mui/material'; } from '@mui/material';
// third party // third party
import * as Yup from 'yup'; import * as Yup from 'yup';
import { Formik } from 'formik'; import { Formik } from 'formik';
import * as API from '../../../api';
// project import // project import
import FirebaseSocial from './FirebaseSocial'; import FirebaseSocial from './FirebaseSocial';
import AnimateButton from '../../../components/@extended/AnimateButton'; import AnimateButton from '../../../components/@extended/AnimateButton';
@ -32,7 +35,7 @@ import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
// ============================|| FIREBASE - REGISTER ||============================ // // ============================|| FIREBASE - REGISTER ||============================ //
const AuthRegister = () => { const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
const [level, setLevel] = useState(); const [level, setLevel] = useState();
const [showPassword, setShowPassword] = useState(false); const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => { const handleClickShowPassword = () => {
@ -56,7 +59,7 @@ const AuthRegister = () => {
<> <>
<Formik <Formik
initialValues={{ initialValues={{
firstname: '', nickname: nickname,
lastname: '', lastname: '',
email: '', email: '',
company: '', company: '',
@ -64,112 +67,61 @@ const AuthRegister = () => {
submit: null submit: null
}} }}
validationSchema={Yup.object().shape({ validationSchema={Yup.object().shape({
firstname: Yup.string().max(255).required('First Name is required'), nickname: Yup.string().max(255).required('Nickname is required'),
lastname: Yup.string().max(255).required('Last Name is required'), password: Yup.string()
email: Yup.string().email('Must be a valid email').max(255).required('Email is required'), .max(255)
password: Yup.string().max(255).required('Password is required') .required('Password is required')
.matches(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*])(?=.{8,})/,
'Must Contain 8 Characters, One Uppercase, One Lowercase, One Number and one special case Character'
),
})} })}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try { return API.users.register({
setStatus({ success: false }); nickname: nickname,
registerKey: regkey,
password: values.password,
}).then((res) => {
setStatus({ success: true });
setSubmitting(false); setSubmitting(false);
} catch (err) { window.location.href = '/ui/login';
console.error(err); }).catch((err) => {
setStatus({ success: false }); setStatus({ success: false });
setErrors({ submit: err.message }); setErrors({ submit: err.message });
setSubmitting(false); setSubmitting(false);
} });
}} }}
> >
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => ( {({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
<form noValidate onSubmit={handleSubmit}> <form noValidate onSubmit={handleSubmit}>
<Grid container spacing={3}> <Grid container spacing={3}>
<Grid item xs={12} md={6}> {isInviteLink ? <Grid item xs={12}>
<Alert severity="info">
<strong>Invite Link</strong> - You have been invited to join this Cosmos instance. This Nickname has been provided to us by your administrator. Keep note of it, you will need it to login.
</Alert>
</Grid> : ''}
{isInviteLink ? <Grid item xs={12}>
<Stack spacing={1}> <Stack spacing={1}>
<InputLabel htmlFor="firstname-signup">First Name*</InputLabel> <InputLabel htmlFor="nickname-signup">Nickname</InputLabel>
<OutlinedInput <OutlinedInput
id="firstname-login" id="nickname-login"
type="firstname" type="nickname"
value={values.firstname} value={nickname}
name="firstname" name="nickname"
onBlur={handleBlur} onBlur={handleBlur}
onChange={handleChange} onChange={handleChange}
placeholder="John" placeholder=""
disabled={true}
fullWidth fullWidth
error={Boolean(touched.firstname && errors.firstname)} error={Boolean(touched.nickname && errors.nickname)}
/> />
{touched.firstname && errors.firstname && ( {touched.nickname && errors.nickname && (
<FormHelperText error id="helper-text-firstname-signup"> <FormHelperText error id="helper-text-nickname-signup">
{errors.firstname} {errors.nickname}
</FormHelperText> </FormHelperText>
)} )}
</Stack> </Stack>
</Grid> </Grid> : ''}
<Grid item xs={12} md={6}>
<Stack spacing={1}>
<InputLabel htmlFor="lastname-signup">Last Name*</InputLabel>
<OutlinedInput
fullWidth
error={Boolean(touched.lastname && errors.lastname)}
id="lastname-signup"
type="lastname"
value={values.lastname}
name="lastname"
onBlur={handleBlur}
onChange={handleChange}
placeholder="Doe"
inputProps={{}}
/>
{touched.lastname && errors.lastname && (
<FormHelperText error id="helper-text-lastname-signup">
{errors.lastname}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="company-signup">Company</InputLabel>
<OutlinedInput
fullWidth
error={Boolean(touched.company && errors.company)}
id="company-signup"
value={values.company}
name="company"
onBlur={handleBlur}
onChange={handleChange}
placeholder="Demo Inc."
inputProps={{}}
/>
{touched.company && errors.company && (
<FormHelperText error id="helper-text-company-signup">
{errors.company}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="email-signup">Email Address*</InputLabel>
<OutlinedInput
fullWidth
error={Boolean(touched.email && errors.email)}
id="email-login"
type="email"
value={values.email}
name="email"
onBlur={handleBlur}
onChange={handleChange}
placeholder="demo@company.com"
inputProps={{}}
/>
{touched.email && errors.email && (
<FormHelperText error id="helper-text-email-signup">
{errors.email}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}> <Grid item xs={12}>
<Stack spacing={1}> <Stack spacing={1}>
<InputLabel htmlFor="password-signup">Password</InputLabel> <InputLabel htmlFor="password-signup">Password</InputLabel>
@ -198,7 +150,7 @@ const AuthRegister = () => {
</IconButton> </IconButton>
</InputAdornment> </InputAdornment>
} }
placeholder="******" placeholder="********"
inputProps={{}} inputProps={{}}
/> />
{touched.password && errors.password && ( {touched.password && errors.password && (
@ -220,18 +172,6 @@ const AuthRegister = () => {
</Grid> </Grid>
</FormControl> </FormControl>
</Grid> </Grid>
<Grid item xs={12}>
<Typography variant="body2">
By Signing up, you agree to our &nbsp;
<Link variant="subtitle2" component={RouterLink} to="#">
Terms of Service
</Link>
&nbsp; and &nbsp;
<Link variant="subtitle2" component={RouterLink} to="#">
Privacy Policy
</Link>
</Typography>
</Grid>
{errors.submit && ( {errors.submit && (
<Grid item xs={12}> <Grid item xs={12}>
<FormHelperText error>{errors.submit}</FormHelperText> <FormHelperText error>{errors.submit}</FormHelperText>
@ -248,18 +188,12 @@ const AuthRegister = () => {
variant="contained" variant="contained"
color="primary" color="primary"
> >
Create Account {
isRegister ? 'Register' : 'Reset Password'
}
</Button> </Button>
</AnimateButton> </AnimateButton>
</Grid> </Grid>
<Grid item xs={12}>
<Divider>
<Typography variant="caption">Sign up with</Typography>
</Divider>
</Grid>
<Grid item xs={12}>
<FirebaseSocial />
</Grid>
</Grid> </Grid>
</form> </form>
)} )}

View file

@ -0,0 +1,328 @@
import * as React from 'react';
import isLoggedIn from '../../../isLoggedIn';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik, Field } from 'formik';
import * as Yup from 'yup';
import {
Alert,
Button,
Checkbox,
Divider,
FormControlLabel,
Grid,
IconButton,
InputAdornment,
InputLabel,
Link,
OutlinedInput,
Stack,
Typography,
FormHelperText,
Collapse,
TextField,
MenuItem,
} from '@mui/material';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import AnimateButton from '../../../components/@extended/AnimateButton';
import RestartModal from './restart';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
const ConfigManagement = () => {
isLoggedIn();
const [config, setConfig] = React.useState(null);
const [openModal, setOpenModal] = React.useState(false);
function refresh() {
API.config.get().then((res) => {
setConfig(res.data);
});
}
React.useEffect(() => {
refresh();
}, []);
return <div style={{maxWidth: '1000px', margin: ''}}>
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>Refresh</Button><br /><br />
{config && <>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
<Formik
initialValues={{
MongoDB: config.MongoDB,
LoggingLevel: config.LoggingLevel,
Hostname: config.HTTPConfig.Hostname,
GenerateMissingTLSCert: config.HTTPConfig.GenerateMissingTLSCert,
GenerateMissingAuthCert: config.HTTPConfig.GenerateMissingAuthCert,
HTTPPort: config.HTTPConfig.HTTPPort,
HTTPSPort: config.HTTPConfig.HTTPSPort,
}}
validationSchema={Yup.object().shape({
Hostname: Yup.string().max(255).required('Hostname is required'),
MongoDB: Yup.string().max(512),
LoggingLevel: Yup.string().max(255).required('Logging Level is required'),
})}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
try {
let toSave = {
...config,
MongoDB: values.MongoDB,
LoggingLevel: values.LoggingLevel,
HTTPConfig: {
...config.HTTPConfig,
Hostname: values.Hostname,
GenerateMissingTLSCert: values.GenerateMissingTLSCert,
GenerateMissingAuthCert: values.GenerateMissingAuthCert,
HTTPPort: values.HTTPPort,
HTTPSPort: values.HTTPSPort,
}
}
API.config.set(toSave).then((data) => {
if (data.status == 'error') {
setStatus({ success: false });
if (data.code == 'UL001') {
setErrors({ submit: 'Wrong nickname or password. Try again or try resetting your password' });
} else if (data.status == 'error') {
setErrors({ submit: 'Unexpected error. Try again later.' });
}
setSubmitting(false);
return;
} else {
setStatus({ success: true });
setSubmitting(false);
setOpenModal(true);
}
})
} catch (err) {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
<form noValidate onSubmit={handleSubmit}>
<MainCard title="General">
<Grid container spacing={3}>
<Grid item xs={12}>
<Alert severity="info">This page allow you to edit the configuration file. Any Environment Variable overwritting configuration won't appear here.</Alert>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="MongoDB-login">MongoDB connection string. It is advised to use Environment variable to store this securely instead. (Optional)</InputLabel>
<OutlinedInput
id="MongoDB-login"
type="password"
value={values.MongoDB}
name="MongoDB"
onBlur={handleBlur}
onChange={handleChange}
placeholder="MongoDB"
fullWidth
error={Boolean(touched.MongoDB && errors.MongoDB)}
/>
{touched.MongoDB && errors.MongoDB && (
<FormHelperText error id="standard-weight-helper-text-MongoDB-login">
{errors.MongoDB}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="LoggingLevel-login">Level of logging (Default: INFO)</InputLabel>
<TextField
className="px-2 my-2"
variant="outlined"
name="LoggingLevel"
id="LoggingLevel"
select
value={values.LoggingLevel}
onChange={handleChange}
error={
touched.LoggingLevel &&
Boolean(errors.LoggingLevel)
}
helperText={
touched.LoggingLevel && errors.LoggingLevel
}
>
<MenuItem key={"DEBUG"} value={"DEBUG"}>
DEBUG
</MenuItem>
<MenuItem key={"INFO"} value={"INFO"}>
INFO
</MenuItem>
<MenuItem key={"WARNING"} value={"WARNING"}>
WARNING
</MenuItem>
<MenuItem key={"ERROR"} value={"ERROR"}>
ERROR
</MenuItem>
</TextField>
</Stack>
</Grid>
</Grid>
</MainCard>
<br /><br />
<MainCard title="HTTP">
<Grid container spacing={3}>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="Hostname-login">Hostname: This will be used to restrict access to your Cosmos Server (Default: 0.0.0.0)</InputLabel>
<OutlinedInput
id="Hostname-login"
type="text"
value={values.Hostname}
name="Hostname"
onBlur={handleBlur}
onChange={handleChange}
placeholder="Hostname"
fullWidth
error={Boolean(touched.Hostname && errors.Hostname)}
/>
{touched.Hostname && errors.Hostname && (
<FormHelperText error id="standard-weight-helper-text-Hostname-login">
{errors.Hostname}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="HTTPPort-login">HTTP Port (Default: 80)</InputLabel>
<OutlinedInput
id="HTTPPort-login"
type="text"
value={values.HTTPPort}
name="HTTPPort"
onBlur={handleBlur}
onChange={handleChange}
placeholder="HTTPPort"
fullWidth
error={Boolean(touched.HTTPPort && errors.HTTPPort)}
/>
{touched.HTTPPort && errors.HTTPPort && (
<FormHelperText error id="standard-weight-helper-text-HTTPPort-login">
{errors.HTTPPort}
</FormHelperText>
)}
</Stack>
</Grid>
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="HTTPSPort-login">HTTPS Port (Default: 443)</InputLabel>
<OutlinedInput
id="HTTPSPort-login"
type="text"
value={values.HTTPSPort}
name="HTTPSPort"
onBlur={handleBlur}
onChange={handleChange}
placeholder="HTTPSPort"
fullWidth
error={Boolean(touched.HTTPSPort && errors.HTTPSPort)}
/>
{touched.HTTPSPort && errors.HTTPSPort && (
<FormHelperText error id="standard-weight-helper-text-HTTPSPort-login">
{errors.HTTPSPort}
</FormHelperText>
)}
</Stack>
</Grid>
</Grid>
</MainCard>
<br /><br />
<MainCard title="Security Certificates">
<Grid container spacing={3}>
<Grid item xs={12}>
<Alert severity="info">For security reasons, It is not possible to remotely change the Private keys of any certificates on your instance. It is advised to manually edit the config file, or better, use Environment Variables to store them.</Alert>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Field
type="checkbox"
name="GenerateMissingTLSCert"
as={FormControlLabel}
control={<Checkbox size="large" />}
label="Generate missing HTTPS Certificates automatically (Default: true)"
/>
</Stack>
</Grid>
<Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Field
type="checkbox"
name="GenerateMissingAuthCert"
as={FormControlLabel}
control={<Checkbox size="large" />}
label="Generate missing Authentication Certificates automatically (Default: true)"
/>
</Stack>
</Grid>
<Grid item xs={12}>
<h4>Authentication Public Key</h4>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<pre className='code'>
{config.HTTPConfig.AuthPublicKey}
</pre>
</Stack>
</Grid>
<Grid item xs={12}>
<h4>Root HTTPS Public Key</h4>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<pre className='code'>
{config.HTTPConfig.TLSCert}
</pre>
</Stack>
</Grid>
</Grid>
</MainCard>
<br /><br />
<MainCard>
{errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{errors.submit}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button
disableElevation
disabled={isSubmitting}
fullWidth
size="large"
type="submit"
variant="contained"
color="primary"
>
Save
</Button>
</AnimateButton>
</Grid>
</MainCard>
</form>
)}
</Formik>
</>}
</div>;
}
export default ConfigManagement;

View file

@ -0,0 +1,126 @@
import * as React from 'react';
import {
Alert,
Button,
Checkbox,
Divider,
FormControlLabel,
Grid,
IconButton,
InputAdornment,
InputLabel,
Link,
OutlinedInput,
Stack,
Typography,
FormHelperText,
Collapse,
TextField,
MenuItem,
AccordionSummary,
AccordionDetails,
Accordion,
Chip,
} from '@mui/material';
import { Formik, Field } from 'formik';
import { DownOutlined, UpOutlined } from '@ant-design/icons';
import AnimateButton from '../../../components/@extended/AnimateButton';
import RestartModal from './restart';
export const CosmosInputText = ({ name, placeholder, label, formik }) => {
return <Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor={name}>{label}</InputLabel>
<OutlinedInput
id={name}
type="text"
value={formik.values[name]}
name={name}
onBlur={formik.handleBlur}
onChange={formik.handleChange}
placeholder={placeholder}
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>
)}
</Stack>
</Grid>
}
export const CosmosSelect = ({ name, label, formik, options }) => {
return <Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor={name}>{label}</InputLabel>
<TextField
className="px-2 my-2"
variant="outlined"
name={name}
id={name}
select
value={formik.values[name]}
onChange={formik.handleChange}
error={
formik.touched[name] &&
Boolean(formik.errors[name])
}
helperText={
formik.touched[name] && formik.errors[name]
}
>
{options.map((option) => (
<MenuItem key={option[0]} value={option[0]}>
{option[1]}
</MenuItem>
))}
</TextField>
</Stack>
</Grid>;
}
export const CosmosCheckbox = ({ name, label, formik }) => {
return <Grid item xs={12}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<Field
type="checkbox"
name={name}
as={FormControlLabel}
control={<Checkbox size="large" />}
label={label}
/>
</Stack>
</Grid>
}
export const CosmosCollapse = ({ children, title }) => {
const [open, setOpen] = React.useState(false);
return <Grid item xs={12}>
<Stack spacing={1}>
<Accordion>
<AccordionSummary
expandIcon={<DownOutlined />}
aria-controls="panel1a-content"
id="panel1a-header"
>
<Typography>{title}</Typography>
</AccordionSummary>
<AccordionDetails>
{children}
</AccordionDetails>
</Accordion>
</Stack>
</Grid>
}
export function CosmosFormDivider({title}) {
return <Grid item xs={12}>
<Divider>
<Chip label={title} />
</Divider>
</Grid>
}

View file

@ -0,0 +1,169 @@
import * as React from 'react';
import isLoggedIn from '../../../isLoggedIn';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik, Field } from 'formik';
import * as Yup from 'yup';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
import {
Alert,
Button,
Checkbox,
Divider,
FormControlLabel,
Grid,
IconButton,
InputAdornment,
InputLabel,
Link,
OutlinedInput,
Stack,
Typography,
FormHelperText,
Collapse,
TextField,
MenuItem,
} from '@mui/material';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import AnimateButton from '../../../components/@extended/AnimateButton';
import RestartModal from './restart';
import RouteManagement from './routeman';
const ProxyManagement = () => {
isLoggedIn();
const [config, setConfig] = React.useState(null);
const [openModal, setOpenModal] = React.useState(false);
const [error, setError] = React.useState(null);
function updateRoutes(routes) {
let con = {
...config,
HTTPConfig: {
...config.HTTPConfig,
ProxyConfig: {
...config.HTTPConfig.ProxyConfig,
Routes: routes,
},
},
};
setConfig(con);
return con;
}
function refresh() {
API.config.get().then((res) => {
setConfig(res.data);
});
}
function up(key) {
if (key > 0) {
let tmp = routes[key];
routes[key] = routes[key-1];
routes[key-1] = tmp;
updateRoutes(routes);
}
}
function deleteRoute(key) {
routes.splice(key, 1);
updateRoutes(routes);
}
function down(key) {
if (key < routes.length - 1) {
let tmp = routes[key];
routes[key] = routes[key+1];
routes[key+1] = tmp;
updateRoutes(routes);
}
}
React.useEffect(() => {
refresh();
}, []);
let routes = config && (config.HTTPConfig.ProxyConfig.Routes || []);
return <div style={{ maxWidth: '1000px', margin: '' }}>
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
refresh();
}}>Refresh</Button>&nbsp;&nbsp;
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
routes.push({
Name: 'New Route',
Description: 'New Route',
UseHost: false,
Host: '',
UsePathPrefix: false,
PathPrefix: '',
Timeout: 30000,
ThrottlePerMinute: 100,
CORSOrigin: '',
StripPathPrefix: false,
Static: false,
SPAMode: false,
});
updateRoutes(routes);
}}>Create</Button>
<br /><br />
{config && <>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
{routes && routes.map((route,key) => (<>
<RouteManagement routeConfig={route} setRouteConfig={(newRoute) => {
routes[key] = newRoute;
}}
up={() => up(key)}
down={() => down(key)}
deleteRoute={() => deleteRoute(key)}
/>
<br /><br />
</>))}
{routes &&
<MainCard>
{error && (
<Grid item xs={12}>
<FormHelperText error>{error}</FormHelperText>
</Grid>
)}
<Grid item xs={12}>
<AnimateButton>
<Button
disableElevation
disabled={false}
fullWidth
onClick={() => {
API.config.set(updateRoutes(routes)).then(() => {
setOpenModal(true);
}).catch((err) => {
console.log(err);
setError(err.message);
});
}}
size="large"
type="submit"
variant="contained"
color="primary"
>
Save
</Button>
</AnimateButton>
</Grid>
</MainCard>
}
{!routes && <>
<Typography variant="h6" gutterBottom component="div">
No routes configured.
</Typography>
</>
}
</>}
</div>;
}
export default ProxyManagement;

View file

@ -0,0 +1,52 @@
// material-ui
import * as React from 'react';
import { Button, Typography } from '@mui/material';
import { WarningOutlined, PlusCircleOutlined, CopyOutlined, ExclamationCircleOutlined , SyncOutlined, UserOutlined, KeyOutlined } from '@ant-design/icons';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import TextField from '@mui/material/TextField';
import CircularProgress from '@mui/material/CircularProgress';
import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import isLoggedIn from '../../../isLoggedIn';
import { useEffect, useState } from 'react';
const RestartModal = ({openModal, setOpenModal}) => {
return <>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>Restart Server</DialogTitle>
<DialogContent>
<DialogContentText>
A restart is required to apply changes. Do you want to restart?
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => setOpenModal(false)}>Later</Button>
<Button onClick={() => {
API.config.restart()
.then(() => {
refresh();
setOpenModal(false);
setTimeout(() => {
window.location.reload();
}, 2000)
})
}}>Restart</Button>
</DialogActions>
</Dialog>
</>;
};
export default RestartModal;

View file

@ -0,0 +1,180 @@
import * as React from 'react';
import isLoggedIn from '../../../isLoggedIn';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik, Field } from 'formik';
import * as Yup from 'yup';
import {
Alert,
Button,
Checkbox,
Divider,
FormControlLabel,
Grid,
IconButton,
InputAdornment,
InputLabel,
Link,
OutlinedInput,
Stack,
Typography,
FormHelperText,
Collapse,
TextField,
MenuItem,
Card,
Chip,
} from '@mui/material';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import AnimateButton from '../../../components/@extended/AnimateButton';
import RestartModal from './restart';
import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, CosmosSelect } from './formShortcuts';
import { DownOutlined, UpOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons';
const RouteManagement = ({ routeConfig, setRouteConfig, up, down, deleteRoute }) => {
const [confirmDelete, setConfirmDelete] = React.useState(false);
return <div style={{ maxWidth: '1000px', margin: '' }}>
{routeConfig && <>
<Formik
initialValues={{
Name: routeConfig.Name,
Description: routeConfig.Description,
Mode: routeConfig.Mode,
Target: routeConfig.Target,
UseHost: routeConfig.UseHost,
Host: routeConfig.Host,
UsePathPrefix: routeConfig.UsePathPrefix,
PathPrefix: routeConfig.PathPrefix,
StripPathPrefix: routeConfig.StripPathPrefix,
Timeout: routeConfig.Timeout,
ThrottlePerMinute: routeConfig.ThrottlePerMinute,
CORSOrigin: routeConfig.CORSOrigin,
}}
validationSchema={Yup.object().shape({
})}
validate={(values) => {
setRouteConfig(values);
}}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
return false;
}}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<MainCard title={
<div>{routeConfig.Name} &nbsp;
<Chip label={<UpOutlined />} onClick={() => up()}/> &nbsp;
<Chip label={<DownOutlined />} onClick={() => down()}/> &nbsp;
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
{confirmDelete && (<Chip label={<CheckOutlined />} onClick={() => deleteRoute()}/>)} &nbsp;
</div>
}>
<Grid container spacing={2}>
<CosmosInputText
name="Name"
label="Name"
placeholder="Name"
formik={formik}
/>
<CosmosInputText
name="Description"
label="Description"
placeholder="Description"
formik={formik}
/>
<CosmosCollapse title="Settings">
<Grid container spacing={2}>
<CosmosFormDivider title={'Target'}/>
<CosmosSelect
name="Mode"
label="Mode"
formik={formik}
options={[
["PROXY", "Proxy"],
["STATIC", "Static Folder"],
["SPA", "Single Page Application"],
]}
/>
<CosmosInputText
name="Target"
label={formik.values.Mode == "PROXY" ? "Target URL" : "Target Folder Path"}
placeholder={formik.values.Mode == "PROXY" ? "localhost:8080" : "/path/to/my/app"}
formik={formik}
/>
<CosmosFormDivider title={'Source'}/>
<CosmosCheckbox
name="UseHost"
label="Use Host"
formik={formik}
/>
{formik.values.UseHost && <CosmosInputText
name="Host"
label="Host"
placeholder="Host"
formik={formik}
/>}
<CosmosCheckbox
name="UsePathPrefix"
label="Use Path Prefix"
formik={formik}
/>
{formik.values.UsePathPrefix && <CosmosInputText
name="PathPrefix"
label="Path Prefix"
placeholder="Path Prefix"
formik={formik}
/>}
{formik.values.UsePathPrefix && <CosmosCheckbox
name="StripPathPrefix"
label="Strip Path Prefix"
formik={formik}
/>}
<CosmosFormDivider title={'Security'}/>
<CosmosInputText
name="Timeout"
label="Timeout in milliseconds (0 for no timeout, at least 30000 or less recommended)"
placeholder="Timeout"
formik={formik}
/>
<CosmosInputText
name="ThrottlePerMinute"
label="Maximum number of requests Per Minute (0 for no limit, at least 100 or less recommended)"
placeholder="Throttle Per Minute"
formik={formik}
/>
<CosmosInputText
name="CORSOrigin"
label="Custom CORS Origin (Recommended to leave blank)"
placeholder="CORS Origin"
formik={formik}
/>
</Grid>
</CosmosCollapse>
</Grid>
</MainCard>
</form>
)}
</Formik>
</>}
</div>;
}
export default RouteManagement;

View file

@ -18,10 +18,7 @@ import TextField from '@mui/material/TextField';
import CircularProgress from '@mui/material/CircularProgress'; import CircularProgress from '@mui/material/CircularProgress';
import Chip from '@mui/material/Chip'; import Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton'; import IconButton from '@mui/material/IconButton';
import * as API from '../../../api'; import * as API from '../../../api';
// project import
import MainCard from '../../../components/MainCard'; import MainCard from '../../../components/MainCard';
import isLoggedIn from '../../../isLoggedIn'; import isLoggedIn from '../../../isLoggedIn';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -53,13 +50,13 @@ const UserManagement = () => {
refresh(); refresh();
}, []) }, [])
function sendlink(nickname) { function sendlink(nickname, formType) {
API.users.invite({ API.users.invite({
nickname nickname
}) })
.then((values) => { .then((values) => {
let sendLink = window.location.origin + '/register?nickname='+nickname+'&key=' + values.data.registerKey; let sendLink = window.location.origin + '/ui/register?t='+formType+'&nickname='+nickname+'&key=' + values.data.registerKey;
setToAction({...values.data, nickname, sendLink}); setToAction({...values.data, nickname, sendLink, formType});
setOpenInviteForm(true); setOpenInviteForm(true);
}); });
} }
@ -216,12 +213,12 @@ const UserManagement = () => {
{isRegistered ? {isRegistered ?
(<Button variant="contained" color="primary" onClick={ (<Button variant="contained" color="primary" onClick={
() => { () => {
sendlink(row.nickname); sendlink(row.nickname, 1);
} }
}>Send password reset</Button>) : }>Send password reset</Button>) :
(<Button variant="contained" className={inviteExpired ? 'shinyButton' : ''} onClick={ (<Button variant="contained" className={inviteExpired ? 'shinyButton' : ''} onClick={
() => { () => {
sendlink(row.nickname); sendlink(row.nickname, 2);
} }
} color="primary">Re-Send Invite</Button>) } color="primary">Re-Send Invite</Button>)
} }

View file

@ -1,8 +1,10 @@
import path from 'path';
import { lazy } from 'react'; import { lazy } from 'react';
// project import // project import
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';
// render - login // render - login
const AuthLogin = Loadable(lazy(() => import('../pages/authentication/Login'))); const AuthLogin = Loadable(lazy(() => import('../pages/authentication/Login')));
@ -15,12 +17,16 @@ const LoginRoutes = {
element: <MinimalLayout />, element: <MinimalLayout />,
children: [ children: [
{ {
path: 'login', path: '/ui/login',
element: <AuthLogin /> element: <AuthLogin />
}, },
{ {
path: 'register', path: '/ui/register',
element: <AuthRegister /> element: <AuthRegister />
},
{
path: '/ui/logout',
element: <Logout />
} }
] ]
}; };

View file

@ -4,6 +4,8 @@ import { lazy } from 'react';
import Loadable from '../components/Loadable'; import Loadable from '../components/Loadable';
import MainLayout from '../layout/MainLayout'; import MainLayout from '../layout/MainLayout';
import UserManagement from '../pages/config/users/usermanagement'; import UserManagement from '../pages/config/users/usermanagement';
import ConfigManagement from '../pages/config/users/configman';
import ProxyManagement from '../pages/config/users/proxyman';
// render - dashboard // render - dashboard
const DashboardDefault = Loadable(lazy(() => import('../pages/dashboard'))); const DashboardDefault = Loadable(lazy(() => import('../pages/dashboard')));
@ -24,15 +26,15 @@ const MainRoutes = {
element: <MainLayout />, element: <MainLayout />,
children: [ children: [
{ {
path: '/', path: '/ui/',
element: <DashboardDefault /> element: <DashboardDefault />
}, },
{ {
path: 'color', path: '/ui/color',
element: <Color /> element: <Color />
}, },
{ {
path: 'dashboard', path: '/ui/dashboard',
children: [ children: [
{ {
path: 'default', path: 'default',
@ -41,19 +43,27 @@ const MainRoutes = {
] ]
}, },
{ {
path: 'config/users', path: '/ui/config/users',
element: <UserManagement /> element: <UserManagement />
}, },
{ {
path: 'shadow', path: '/ui/config/general',
element: <ConfigManagement />
},
{
path: '/ui/config/proxy',
element: <ProxyManagement />
},
{
path: '/ui/shadow',
element: <Shadow /> element: <Shadow />
}, },
{ {
path: 'typography', path: '/ui/typography',
element: <Typography /> element: <Typography />
}, },
{ {
path: 'icons/ant', path: '/ui/icons/ant',
element: <AntIcons /> element: <AntIcons />
} }
] ]

View file

@ -20,8 +20,8 @@ export const strengthColor = (count) => {
// password strength indicator // password strength indicator
export const strengthIndicator = (number) => { export const strengthIndicator = (number) => {
let strengths = 0; let strengths = 0;
if (number.length > 5) strengths += 1; if (number.length > 9) strengths += 1;
if (number.length > 7) strengths += 1; if (number.length > 11) strengths += 1;
if (hasNumber(number)) strengths += 1; if (hasNumber(number)) strengths += 1;
if (hasSpecial(number)) strengths += 1; if (hasSpecial(number)) strengths += 1;
if (hasMixed(number)) strengths += 1; if (hasMixed(number)) strengths += 1;

View file

@ -86,6 +86,6 @@
}, },
"description": "Cosmos Server", "description": "Cosmos Server",
"name": "cosmos-server", "name": "cosmos-server",
"version": "0.0.4", "version": "0.0.5",
"wrapInstallFolder": "src" "wrapInstallFolder": "src"
} }

View file

@ -1,14 +1,13 @@
package user package configapi
import ( import (
"net/http" "net/http"
"encoding/json" "encoding/json"
"github.com/gorilla/mux"
"../utils" "../utils"
) )
func ConfigApiGet(w http.ResponseWriter, req *http.Request) { func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
if AdminOnly(w, req) != nil { if utils.AdminOnly(w, req) != nil {
return return
} }
@ -16,8 +15,8 @@ func ConfigApiGet(w http.ResponseWriter, req *http.Request) {
config := utils.GetBaseMainConfig() config := utils.GetBaseMainConfig()
// delete AuthPrivateKey and TLSKey // delete AuthPrivateKey and TLSKey
config.AuthPrivateKey = "" config.HTTPConfig.AuthPrivateKey = ""
config.TLSKey = "" config.HTTPConfig.TLSKey = ""
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK", "status": "OK",

View file

@ -1,23 +1,21 @@
package user package configapi
import ( import (
"net/http" "net/http"
"encoding/json" "encoding/json"
"github.com/gorilla/mux"
"../utils" "../utils"
) )
func ConfigApiGet(w http.ResponseWriter, req *http.Request) { func ConfigApiRestart(w http.ResponseWriter, req *http.Request) {
if AdminOnly(w, req) != nil { if utils.AdminOnly(w, req) != nil {
return return
} }
if(req.Method == "GET") { if(req.Method == "GET") {
utils.RestartServer()
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK" "status": "OK",
}) })
utils.RestartServer()
} else { } else {
utils.Error("Restart: Method not allowed" + req.Method, nil) utils.Error("Restart: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001") utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")

18
src/configapi/route.go Normal file
View file

@ -0,0 +1,18 @@
package configapi
import (
"net/http"
"../utils"
)
func ConfigRoute(w http.ResponseWriter, req *http.Request) {
if(req.Method == "GET") {
ConfigApiGet(w, req)
} else if (req.Method == "PUT") {
ConfigApiSet(w, req)
} else {
utils.Error("UserRoute: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")
return
}
}

View file

@ -1,14 +1,13 @@
package user package configapi
import ( import (
"net/http" "net/http"
"encoding/json" "encoding/json"
"github.com/gorilla/mux"
"../utils" "../utils"
) )
func ConfigApiSet(w http.ResponseWriter, req *http.Request) { func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
if AdminOnly(w, req) != nil { if utils.AdminOnly(w, req) != nil {
return return
} }
@ -32,20 +31,20 @@ func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
// restore AuthPrivateKey and TLSKey // restore AuthPrivateKey and TLSKey
config := utils.GetBaseMainConfig() config := utils.GetBaseMainConfig()
request.AuthPrivateKey = config.AuthPrivateKey request.HTTPConfig.AuthPrivateKey = config.HTTPConfig.AuthPrivateKey
request.TLSKey = config.TLSKey request.HTTPConfig.TLSKey = config.HTTPConfig.TLSKey
err := utils.SaveConfigTofile(request) utils.SaveConfigTofile(request)
if err != nil { // if err != nil {
utils.Error("SettingsUpdate: Error saving config to file", err) // utils.Error("SettingsUpdate: Error saving config to file", err)
utils.HTTPError(w, "Error saving config to file", // utils.HTTPError(w, "Error saving config to file",
http.StatusInternalServerError, "CS001") // http.StatusInternalServerError, "CS001")
return // return
} // }
json.NewEncoder(w).Encode(map[string]interface{}{ json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK" "status": "OK",
}) })
} else { } else {
utils.Error("SettingsUpdate: Method not allowed" + req.Method, nil) utils.Error("SettingsUpdate: Method not allowed" + req.Method, nil)

View file

@ -4,6 +4,7 @@ import (
"net/http" "net/http"
"./utils" "./utils"
"./user" "./user"
"./configapi"
"./proxy" "./proxy"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"strings" "strings"
@ -158,6 +159,10 @@ func StartServer() {
router.Use(middleware.Logger) router.Use(middleware.Logger)
router.Use(tokenMiddleware) router.Use(tokenMiddleware)
router.Use(utils.SetSecurityHeaders) router.Use(utils.SetSecurityHeaders)
router.HandleFunc("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/ui", http.StatusMovedPermanently)
}))
srapi := router.PathPrefix("/cosmos").Subrouter() srapi := router.PathPrefix("/cosmos").Subrouter()
@ -166,6 +171,8 @@ func StartServer() {
srapi.HandleFunc("/api/register", user.UserRegister) srapi.HandleFunc("/api/register", user.UserRegister)
srapi.HandleFunc("/api/invite", user.UserResendInviteLink) srapi.HandleFunc("/api/invite", user.UserResendInviteLink)
srapi.HandleFunc("/api/me", user.Me) srapi.HandleFunc("/api/me", user.Me)
srapi.HandleFunc("/api/config", configapi.ConfigRoute)
srapi.HandleFunc("/api/restart", configapi.ConfigApiRestart)
srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute) srapi.HandleFunc("/api/users/{nickname}", user.UsersIdRoute)
srapi.HandleFunc("/api/users", user.UsersRoute) srapi.HandleFunc("/api/users", user.UsersRoute)
@ -188,8 +195,9 @@ func StartServer() {
if _, err := os.Stat(pwd + "/static"); os.IsNotExist(err) { if _, err := os.Stat(pwd + "/static"); os.IsNotExist(err) {
utils.Fatal("Static folder not found at " + pwd + "/static", err) utils.Fatal("Static folder not found at " + pwd + "/static", err)
} }
fs := spa.SpaHandler(pwd + "/static", "index.html") fs := spa.SpaHandler(pwd + "/static", "index.html")
router.PathPrefix("/").Handler(fs) router.PathPrefix("/ui").Handler(http.StripPrefix("/ui", fs))
router = proxy.BuildFromConfig(router, config.ProxyConfig) router = proxy.BuildFromConfig(router, config.ProxyConfig)

View file

@ -13,11 +13,9 @@ func BuildFromConfig(router *mux.Router, config utils.ProxyConfig) *mux.Router {
w.Write([]byte("OK")) w.Write([]byte("OK"))
}) })
router.PathPrefix("/_static").Handler(http.StripPrefix("/_static", http.FileServer(http.Dir("static"))))
for i := len(config.Routes)-1; i >= 0; i-- { for i := len(config.Routes)-1; i >= 0; i-- {
routeConfig := config.Routes[i] routeConfig := config.Routes[i]
RouterGen(routeConfig.Routing, router, RouteTo(routeConfig.Target)) RouterGen(routeConfig, router, RouteTo(routeConfig.Target))
} }
return router return router

View file

@ -9,7 +9,7 @@ import (
"github.com/go-chi/httprate" "github.com/go-chi/httprate"
) )
func RouterGen(route utils.Route, router *mux.Router, destination *httputil.ReverseProxy) *mux.Route { func RouterGen(route utils.ProxyRouteConfig, router *mux.Router, destination *httputil.ReverseProxy) *mux.Route {
var realDestination http.Handler var realDestination http.Handler
realDestination = destination realDestination = destination

View file

@ -18,7 +18,7 @@ type CreateRequestJSON struct {
} }
func UserCreate(w http.ResponseWriter, req *http.Request) { func UserCreate(w http.ResponseWriter, req *http.Request) {
if AdminOnly(w, req) != nil { if utils.AdminOnly(w, req) != nil {
return return
} }

View file

@ -12,7 +12,7 @@ func UserDelete(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req) vars := mux.Vars(req)
nickname := vars["nickname"] nickname := vars["nickname"]
if AdminOrItselfOnly(w, req, nickname) != nil { if utils.AdminOrItselfOnly(w, req, nickname) != nil {
return return
} }

View file

@ -15,7 +15,7 @@ func UserEdit(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req) vars := mux.Vars(req)
nickname := vars["nickname"] nickname := vars["nickname"]
if AdminOrItselfOnly(w, req, nickname) != nil { if utils.AdminOrItselfOnly(w, req, nickname) != nil {
return return
} }
@ -43,7 +43,7 @@ func UserEdit(w http.ResponseWriter, req *http.Request) {
toSet := map[string]interface{}{} toSet := map[string]interface{}{}
if request.Email != "" { if request.Email != "" {
if AdminOnly(w, req) != nil { if utils.AdminOnly(w, req) != nil {
return return
} }

View file

@ -15,7 +15,7 @@ func UserGet(w http.ResponseWriter, req *http.Request) {
nickname = req.Header.Get("x-cosmos-user") nickname = req.Header.Get("x-cosmos-user")
} }
if AdminOrItselfOnly(w, req, nickname) != nil { if utils.AdminOrItselfOnly(w, req, nickname) != nil {
return return
} }

View file

@ -12,7 +12,7 @@ import (
var maxLimit = 100 var maxLimit = 100
func UserList(w http.ResponseWriter, req *http.Request) { func UserList(w http.ResponseWriter, req *http.Request) {
if AdminOnly(w, req) != nil { if utils.AdminOnly(w, req) != nil {
return return
} }

View file

@ -25,7 +25,7 @@ func UserResendInviteLink(w http.ResponseWriter, req *http.Request) {
nickname := utils.Sanitize(request.Nickname) nickname := utils.Sanitize(request.Nickname)
if AdminOrItselfOnly(w, req, nickname) != nil { if utils.AdminOrItselfOnly(w, req, nickname) != nil {
return return
} }

View file

@ -7,7 +7,6 @@ import (
"errors" "errors"
"strings" "strings"
"time" "time"
"strconv"
) )
func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, error) { func RefreshUserToken(w http.ResponseWriter, req *http.Request) (utils.User, error) {
@ -106,7 +105,7 @@ func logOutUser(w http.ResponseWriter) {
} }
func redirectToReLogin(w http.ResponseWriter, req *http.Request) { func redirectToReLogin(w http.ResponseWriter, req *http.Request) {
http.Redirect(w, req, "/login?invalid=1&redirect=" + req.URL.Path, http.StatusFound) http.Redirect(w, req, "/ui/login?invalid=1&redirect=" + req.URL.Path, http.StatusFound)
} }
func SendUserToken(w http.ResponseWriter, user utils.User) { func SendUserToken(w http.ResponseWriter, user utils.User) {
@ -156,62 +155,4 @@ func SendUserToken(w http.ResponseWriter, user utils.User) {
http.SetCookie(w, &cookie) http.SetCookie(w, &cookie)
// http.SetCookie(w, &cookie2) // http.SetCookie(w, &cookie2)
}
func loggedInOnly(w http.ResponseWriter, req *http.Request) error {
userNickname := req.Header.Get("x-cosmos-user")
role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role"))
isUserLoggedIn := role > 0
if !isUserLoggedIn || userNickname == "" {
utils.Error("LoggedInOnly: User is not logged in", nil)
//http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound)
utils.HTTPError(w, "User not logged in", http.StatusUnauthorized, "HTTP004")
return errors.New("User not logged in")
}
return nil
}
func AdminOnly(w http.ResponseWriter, req *http.Request) error {
userNickname := req.Header.Get("x-cosmos-user")
role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role"))
isUserLoggedIn := role > 0
isUserAdmin := role > 1
if !isUserLoggedIn || userNickname == "" {
utils.Error("AdminOnly: User is not logged in", nil)
//http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound)
utils.HTTPError(w, "User not logged in", http.StatusUnauthorized, "HTTP004")
return errors.New("User not logged in")
}
if isUserLoggedIn && !isUserAdmin {
utils.Error("AdminOnly: User is not admin", nil)
utils.HTTPError(w, "User unauthorized", http.StatusUnauthorized, "HTTP005")
return errors.New("User not Admin")
}
return nil
}
func AdminOrItselfOnly(w http.ResponseWriter, req *http.Request, nickname string) error {
userNickname := req.Header.Get("x-cosmos-user")
role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role"))
isUserLoggedIn := role > 0
isUserAdmin := role > 1
if !isUserLoggedIn || userNickname == "" {
utils.Error("AdminOrItselfOnly: User is not logged in", nil)
utils.HTTPError(w, "User not logged in", http.StatusUnauthorized, "HTTP004")
return errors.New("User not logged in")
}
if nickname != userNickname && !isUserAdmin {
utils.Error("AdminOrItselfOnly: User is not admin", nil)
utils.HTTPError(w, "User unauthorized", http.StatusUnauthorized, "HTTP005")
return errors.New("User not Admin")
}
return nil
} }

View file

@ -7,6 +7,7 @@ import (
) )
type Role int type Role int
type ProxyMode string
type LoggingLevel string type LoggingLevel string
const ( const (
@ -29,6 +30,12 @@ var LoggingLevelLabels = map[LoggingLevel]int{
"ERROR": ERROR, "ERROR": ERROR,
} }
var ProxyModeList = map[string]string{
"PROXY": "PROXY",
"SPA": "SPA",
"STATIC": "STATIC",
}
type FileStats struct { type FileStats struct {
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
@ -78,19 +85,16 @@ type ProxyConfig struct {
} }
type ProxyRouteConfig struct { type ProxyRouteConfig struct {
Routing Route `validate:"required"` Name string
Target string `validate:"required"` Description string
}
type Route struct {
UseHost bool UseHost bool
Host string Host string
UsePathPrefix bool UsePathPrefix bool
PathPrefix string PathPrefix string
Timeout time.Duration Timeout time.Duration
ThrottlePerMinute int ThrottlePerMinute int
SPAMode bool
CORSOrigin string CORSOrigin string
StripPathPrefix bool StripPathPrefix bool
Static bool Target string `validate:"required"`
} Mode ProxyMode
}

View file

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"math/rand" "math/rand"
"errors"
) )
var BaseMainConfig Config var BaseMainConfig Config
@ -175,7 +176,7 @@ func SaveConfigTofile(config Config) {
configFile := GetConfigFileName() configFile := GetConfigFileName()
CreateDefaultConfigFileIfNecessary() CreateDefaultConfigFileIfNecessary()
file, err := os.OpenFile(configFile, os.O_WRONLY, os.ModePerm) file, err := os.OpenFile(configFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, os.ModePerm)
if err != nil { if err != nil {
Fatal("Opening Config File", err) Fatal("Opening Config File", err)
} }
@ -194,4 +195,63 @@ func SaveConfigTofile(config Config) {
func RestartServer() { func RestartServer() {
Log("Restarting server...") Log("Restarting server...")
os.Exit(0) os.Exit(0)
}
func loggedInOnly(w http.ResponseWriter, req *http.Request) error {
userNickname := req.Header.Get("x-cosmos-user")
role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role"))
isUserLoggedIn := role > 0
if !isUserLoggedIn || userNickname == "" {
Error("LoggedInOnly: User is not logged in", nil)
//http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound)
HTTPError(w, "User not logged in", http.StatusUnauthorized, "HTTP004")
return errors.New("User not logged in")
}
return nil
}
func AdminOnly(w http.ResponseWriter, req *http.Request) error {
userNickname := req.Header.Get("x-cosmos-user")
role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role"))
isUserLoggedIn := role > 0
isUserAdmin := role > 1
if !isUserLoggedIn || userNickname == "" {
Error("AdminOnly: User is not logged in", nil)
//http.Redirect(w, req, "/login?notlogged=1&redirect=" + req.URL.Path, http.StatusFound)
HTTPError(w, "User not logged in", http.StatusUnauthorized, "HTTP004")
return errors.New("User not logged in")
}
if isUserLoggedIn && !isUserAdmin {
Error("AdminOnly: User is not admin", nil)
HTTPError(w, "User unauthorized", http.StatusUnauthorized, "HTTP005")
return errors.New("User not Admin")
}
return nil
}
func AdminOrItselfOnly(w http.ResponseWriter, req *http.Request, nickname string) error {
userNickname := req.Header.Get("x-cosmos-user")
role, _ := strconv.Atoi(req.Header.Get("x-cosmos-role"))
isUserLoggedIn := role > 0
isUserAdmin := role > 1
if !isUserLoggedIn || userNickname == "" {
Error("AdminOrItselfOnly: User is not logged in", nil)
HTTPError(w, "User not logged in", http.StatusUnauthorized, "HTTP004")
return errors.New("User not logged in")
}
if nickname != userNickname && !isUserAdmin {
Error("AdminOrItselfOnly: User is not admin", nil)
HTTPError(w, "User unauthorized", http.StatusUnauthorized, "HTTP005")
return errors.New("User not Admin")
}
return nil
} }

View file

@ -8,6 +8,7 @@ export default defineConfig({
build: { build: {
outDir: '../static', outDir: '../static',
}, },
// base: '/ui',
server: { server: {
proxy: { proxy: {
'/cosmos/api': { '/cosmos/api': {