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:
name: Build UI
command: node .bin/vite build
command: node .bin/vite build --base=/ui/
- run:
name: Build Linux (ARM)

View file

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

View file

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

View file

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

View file

@ -41,3 +41,7 @@
animation: shake 1s;
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';
const isLoggedIn = () => useEffect(() => {
console.log("CHECK LOGIN")
API.auth.me().then((data) => {
if(data.status != 'OK') {
window.location.href = '/login';
window.location.href = '/ui/login';
}
});
});
}, []);
export default isLoggedIn;

View file

@ -1,5 +1,5 @@
// 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';
// project import
@ -18,9 +18,12 @@ const HeaderContent = () => {
{!matchesXs && <Search />}
{matchesXs && <Box sx={{ width: '100%', ml: 1 }} />}
<Notification />
{!matchesXs && <Profile />}
{matchesXs && <MobileSection />}
<Link href="/ui/logout" underline="none">
<Chip label="Logout" />
</Link>
{/* <Notification /> */}
{/* {!matchesXs && <Profile />}
{matchesXs && <MobileSection />} */}
</>
);
};

View file

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

View file

@ -19,21 +19,21 @@ const pages = {
id: 'proxy',
title: 'Proxy Routes',
type: 'item',
url: '/config/proxy',
url: '/ui/config/proxy',
icon: icons.NodeExpandOutlined,
},
{
id: 'users',
title: 'Manage Users',
type: 'item',
url: '/config/users',
url: '/ui/config/users',
icon: icons.ProfileOutlined,
},
{
id: 'config',
title: 'Configuration',
type: 'item',
url: '/config/general',
url: '/ui/config/general',
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';
// project import
import FirebaseRegister from './auth-forms/AuthRegister';
import AuthRegister from './auth-forms/AuthRegister';
import AuthWrapper from './AuthWrapper';
// ================================|| REGISTER ||================================ //
const Register = () => (
<AuthWrapper>
const Register = () => {
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 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="/login" variant="body1" sx={{ textDecoration: 'none' }} color="primary">
Already have an account?
</Typography>
<Typography variant="h3">{
isInviteLink ? 'Invitation' : 'Password Reset'
}</Typography>
</Stack>
</Grid>
<Grid item xs={12}>
<FirebaseRegister />
<AuthRegister
nickname={nickname}
isRegister={isRegister}
isInviteLink={isInviteLink}
regkey={regkey}
/>
</Grid>
</Grid>
</AuthWrapper>
);
</AuthWrapper>;
}
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 notLogged = urlSearchParams.get('notlogged') == 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(() => {
API.auth.me().then((data) => {
@ -170,7 +170,7 @@ const AuthLogin = () => {
<Grid item xs={12} sx={{ mt: -1 }}>
<Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2}>
<FormControlLabel
{/* <FormControlLabel
control={
<Checkbox
checked={checked}
@ -184,7 +184,7 @@ const AuthLogin = () => {
/>
<Link variant="h6" component={RouterLink} to="" color="text.primary">
Forgot Password?
</Link>
</Link> */}
</Stack>
</Grid>
{errors.submit && (

View file

@ -15,13 +15,16 @@ import {
InputLabel,
OutlinedInput,
Stack,
Typography
Typography,
Alert
} from '@mui/material';
// third party
import * as Yup from 'yup';
import { Formik } from 'formik';
import * as API from '../../../api';
// project import
import FirebaseSocial from './FirebaseSocial';
import AnimateButton from '../../../components/@extended/AnimateButton';
@ -32,7 +35,7 @@ import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
// ============================|| FIREBASE - REGISTER ||============================ //
const AuthRegister = () => {
const AuthRegister = ({nickname, isRegister, isInviteLink, regkey}) => {
const [level, setLevel] = useState();
const [showPassword, setShowPassword] = useState(false);
const handleClickShowPassword = () => {
@ -56,7 +59,7 @@ const AuthRegister = () => {
<>
<Formik
initialValues={{
firstname: '',
nickname: nickname,
lastname: '',
email: '',
company: '',
@ -64,112 +67,61 @@ const AuthRegister = () => {
submit: null
}}
validationSchema={Yup.object().shape({
firstname: Yup.string().max(255).required('First Name is required'),
lastname: Yup.string().max(255).required('Last Name is required'),
email: Yup.string().email('Must be a valid email').max(255).required('Email is required'),
password: Yup.string().max(255).required('Password is required')
nickname: Yup.string().max(255).required('Nickname is required'),
password: Yup.string()
.max(255)
.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 }) => {
try {
setStatus({ success: false });
return API.users.register({
nickname: nickname,
registerKey: regkey,
password: values.password,
}).then((res) => {
setStatus({ success: true });
setSubmitting(false);
} catch (err) {
console.error(err);
window.location.href = '/ui/login';
}).catch((err) => {
setStatus({ success: false });
setErrors({ submit: err.message });
setSubmitting(false);
}
});
}}
>
{({ errors, handleBlur, handleChange, handleSubmit, isSubmitting, touched, values }) => (
<form noValidate onSubmit={handleSubmit}>
<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}>
<InputLabel htmlFor="firstname-signup">First Name*</InputLabel>
<InputLabel htmlFor="nickname-signup">Nickname</InputLabel>
<OutlinedInput
id="firstname-login"
type="firstname"
value={values.firstname}
name="firstname"
id="nickname-login"
type="nickname"
value={nickname}
name="nickname"
onBlur={handleBlur}
onChange={handleChange}
placeholder="John"
placeholder=""
disabled={true}
fullWidth
error={Boolean(touched.firstname && errors.firstname)}
error={Boolean(touched.nickname && errors.nickname)}
/>
{touched.firstname && errors.firstname && (
<FormHelperText error id="helper-text-firstname-signup">
{errors.firstname}
{touched.nickname && errors.nickname && (
<FormHelperText error id="helper-text-nickname-signup">
{errors.nickname}
</FormHelperText>
)}
</Stack>
</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> : ''}
<Grid item xs={12}>
<Stack spacing={1}>
<InputLabel htmlFor="password-signup">Password</InputLabel>
@ -198,7 +150,7 @@ const AuthRegister = () => {
</IconButton>
</InputAdornment>
}
placeholder="******"
placeholder="********"
inputProps={{}}
/>
{touched.password && errors.password && (
@ -220,18 +172,6 @@ const AuthRegister = () => {
</Grid>
</FormControl>
</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 && (
<Grid item xs={12}>
<FormHelperText error>{errors.submit}</FormHelperText>
@ -248,18 +188,12 @@ const AuthRegister = () => {
variant="contained"
color="primary"
>
Create Account
{
isRegister ? 'Register' : 'Reset Password'
}
</Button>
</AnimateButton>
</Grid>
<Grid item xs={12}>
<Divider>
<Typography variant="caption">Sign up with</Typography>
</Divider>
</Grid>
<Grid item xs={12}>
<FirebaseSocial />
</Grid>
</Grid>
</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 Chip from '@mui/material/Chip';
import IconButton from '@mui/material/IconButton';
import * as API from '../../../api';
// project import
import MainCard from '../../../components/MainCard';
import isLoggedIn from '../../../isLoggedIn';
import { useEffect, useState } from 'react';
@ -53,13 +50,13 @@ const UserManagement = () => {
refresh();
}, [])
function sendlink(nickname) {
function sendlink(nickname, formType) {
API.users.invite({
nickname
})
.then((values) => {
let sendLink = window.location.origin + '/register?nickname='+nickname+'&key=' + values.data.registerKey;
setToAction({...values.data, nickname, sendLink});
let sendLink = window.location.origin + '/ui/register?t='+formType+'&nickname='+nickname+'&key=' + values.data.registerKey;
setToAction({...values.data, nickname, sendLink, formType});
setOpenInviteForm(true);
});
}
@ -216,12 +213,12 @@ const UserManagement = () => {
{isRegistered ?
(<Button variant="contained" color="primary" onClick={
() => {
sendlink(row.nickname);
sendlink(row.nickname, 1);
}
}>Send password reset</Button>) :
(<Button variant="contained" className={inviteExpired ? 'shinyButton' : ''} onClick={
() => {
sendlink(row.nickname);
sendlink(row.nickname, 2);
}
} color="primary">Re-Send Invite</Button>)
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -4,6 +4,7 @@ import (
"net/http"
"./utils"
"./user"
"./configapi"
"./proxy"
"github.com/gorilla/mux"
"strings"
@ -159,6 +160,10 @@ func StartServer() {
router.Use(tokenMiddleware)
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.HandleFunc("/api/login", user.UserLogin)
@ -166,6 +171,8 @@ func StartServer() {
srapi.HandleFunc("/api/register", user.UserRegister)
srapi.HandleFunc("/api/invite", user.UserResendInviteLink)
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", user.UsersRoute)
@ -188,8 +195,9 @@ func StartServer() {
if _, err := os.Stat(pwd + "/static"); os.IsNotExist(err) {
utils.Fatal("Static folder not found at " + pwd + "/static", err)
}
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)

View file

@ -13,11 +13,9 @@ func BuildFromConfig(router *mux.Router, config utils.ProxyConfig) *mux.Router {
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-- {
routeConfig := config.Routes[i]
RouterGen(routeConfig.Routing, router, RouteTo(routeConfig.Target))
RouterGen(routeConfig, router, RouteTo(routeConfig.Target))
}
return router

View file

@ -9,7 +9,7 @@ import (
"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
realDestination = destination

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,6 @@ import (
"errors"
"strings"
"time"
"strconv"
)
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) {
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) {
@ -157,61 +156,3 @@ func SendUserToken(w http.ResponseWriter, user utils.User) {
http.SetCookie(w, &cookie)
// 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 ProxyMode string
type LoggingLevel string
const (
@ -29,6 +30,12 @@ var LoggingLevelLabels = map[LoggingLevel]int{
"ERROR": ERROR,
}
var ProxyModeList = map[string]string{
"PROXY": "PROXY",
"SPA": "SPA",
"STATIC": "STATIC",
}
type FileStats struct {
Name string `json:"name"`
Path string `json:"path"`
@ -78,19 +85,16 @@ type ProxyConfig struct {
}
type ProxyRouteConfig struct {
Routing Route `validate:"required"`
Target string `validate:"required"`
}
type Route struct {
Name string
Description string
UseHost bool
Host string
UsePathPrefix bool
PathPrefix string
Timeout time.Duration
ThrottlePerMinute int
SPAMode bool
CORSOrigin string
StripPathPrefix bool
Static bool
Target string `validate:"required"`
Mode ProxyMode
}

View file

@ -7,6 +7,7 @@ import (
"strconv"
"strings"
"math/rand"
"errors"
)
var BaseMainConfig Config
@ -175,7 +176,7 @@ func SaveConfigTofile(config Config) {
configFile := GetConfigFileName()
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 {
Fatal("Opening Config File", err)
}
@ -195,3 +196,62 @@ func RestartServer() {
Log("Restarting server...")
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: {
outDir: '../static',
},
// base: '/ui',
server: {
proxy: {
'/cosmos/api': {