From 28a44da3ad85246a808553d697eb26935a137cf6 Mon Sep 17 00:00:00 2001 From: Yann Stepienik Date: Thu, 16 Mar 2023 18:56:36 +0000 Subject: [PATCH] Proxy settings + register page --- .circleci/config.yml | 2 +- client/src/api/authentication.jsx | 16 +- client/src/api/config.jsx | 35 ++ client/src/api/index.jsx | 4 +- client/src/api/users.jsx | 32 +- client/src/api/wrap.js | 2 +- client/src/index.css | 4 + client/src/isLoggedIn.jsx | 5 +- .../MainLayout/Header/HeaderContent/index.jsx | 11 +- client/src/menu-items/dashboard.jsx | 2 +- client/src/menu-items/pages.jsx | 6 +- client/src/pages/authentication/Logoff.jsx | 36 ++ client/src/pages/authentication/Register.jsx | 31 +- client/src/pages/authentication/Signup.jsx | 30 ++ .../authentication/auth-forms/AuthLogin.jsx | 6 +- .../auth-forms/AuthRegister.jsx | 156 +++------ client/src/pages/config/users/configman.jsx | 328 ++++++++++++++++++ .../src/pages/config/users/formShortcuts.jsx | 126 +++++++ client/src/pages/config/users/proxyman.jsx | 169 +++++++++ client/src/pages/config/users/restart.jsx | 52 +++ client/src/pages/config/users/routeman.jsx | 180 ++++++++++ .../src/pages/config/users/usermanagement.jsx | 13 +- client/src/routes/LoginRoutes.jsx | 10 +- client/src/routes/MainRoutes.jsx | 24 +- client/src/utils/password-strength.jsx | 4 +- gupm.json | 2 +- src/configapi/get.go | 9 +- src/configapi/restart.go | 14 +- src/configapi/route.go | 18 + src/configapi/set.go | 25 +- src/httpServer.go | 10 +- src/proxy/buildFromConfig.go | 4 +- src/proxy/routerGen.go | 2 +- src/user/create.go | 2 +- src/user/delete.go | 2 +- src/user/edit.go | 4 +- src/user/get.go | 2 +- src/user/list.go | 2 +- src/user/resend.go | 2 +- src/user/token.go | 61 +--- src/utils/types.go | 20 +- src/utils/utils.go | 62 +++- vite.config.js | 1 + 43 files changed, 1239 insertions(+), 287 deletions(-) create mode 100644 client/src/api/config.jsx create mode 100644 client/src/pages/authentication/Logoff.jsx create mode 100644 client/src/pages/authentication/Signup.jsx create mode 100644 client/src/pages/config/users/configman.jsx create mode 100644 client/src/pages/config/users/formShortcuts.jsx create mode 100644 client/src/pages/config/users/proxyman.jsx create mode 100644 client/src/pages/config/users/restart.jsx create mode 100644 client/src/pages/config/users/routeman.jsx create mode 100644 src/configapi/route.go diff --git a/.circleci/config.yml b/.circleci/config.yml index 0f4f481..101533d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -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) diff --git a/client/src/api/authentication.jsx b/client/src/api/authentication.jsx index 821f5f3..36a1494 100644 --- a/client/src/api/authentication.jsx +++ b/client/src/api/authentication.jsx @@ -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 }; \ No newline at end of file diff --git a/client/src/api/config.jsx b/client/src/api/config.jsx new file mode 100644 index 0000000..3df6992 --- /dev/null +++ b/client/src/api/config.jsx @@ -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 +}; \ No newline at end of file diff --git a/client/src/api/index.jsx b/client/src/api/index.jsx index f5f85c4..fb5f0fb 100644 --- a/client/src/api/index.jsx +++ b/client/src/api/index.jsx @@ -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 }; \ No newline at end of file diff --git a/client/src/api/users.jsx b/client/src/api/users.jsx index 701f0ff..e7b80df 100644 --- a/client/src/api/users.jsx +++ b/client/src/api/users.jsx @@ -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), + 'Content-Type': 'application/json' }, - }) - .then((res) => res.json()) + body: JSON.stringify(values), + })) } 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 { diff --git a/client/src/api/wrap.js b/client/src/api/wrap.js index d0e0c2b..4a1ccac 100644 --- a/client/src/api/wrap.js +++ b/client/src/api/wrap.js @@ -7,7 +7,7 @@ export default function wrap(apicall) { return rep; } snackit(rep.message); - throw new Error(rep); + throw new Error(rep.message); }); } diff --git a/client/src/index.css b/client/src/index.css index daac495..4c66f7d 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -40,4 +40,8 @@ .shake { animation: shake 1s; animation-iteration-count: 1; +} + +.code { + background-color: rgba(0.2,0.2,0.2,0.2); } \ No newline at end of file diff --git a/client/src/isLoggedIn.jsx b/client/src/isLoggedIn.jsx index 4f90d15..04821b9 100644 --- a/client/src/isLoggedIn.jsx +++ b/client/src/isLoggedIn.jsx @@ -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; \ No newline at end of file diff --git a/client/src/layout/MainLayout/Header/HeaderContent/index.jsx b/client/src/layout/MainLayout/Header/HeaderContent/index.jsx index 9c65b12..5fd04ca 100644 --- a/client/src/layout/MainLayout/Header/HeaderContent/index.jsx +++ b/client/src/layout/MainLayout/Header/HeaderContent/index.jsx @@ -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 && } {matchesXs && } - - {!matchesXs && } - {matchesXs && } + + + + {/* */} + {/* {!matchesXs && } + {matchesXs && } */} ); }; diff --git a/client/src/menu-items/dashboard.jsx b/client/src/menu-items/dashboard.jsx index ba3f25d..954df02 100644 --- a/client/src/menu-items/dashboard.jsx +++ b/client/src/menu-items/dashboard.jsx @@ -17,7 +17,7 @@ const dashboard = { id: 'home', title: 'Home', type: 'item', - url: '/', + url: '/ui', icon: icons.HomeOutlined, breadcrumbs: false } diff --git a/client/src/menu-items/pages.jsx b/client/src/menu-items/pages.jsx index ca4f92b..9fc88a3 100644 --- a/client/src/menu-items/pages.jsx +++ b/client/src/menu-items/pages.jsx @@ -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, } ] diff --git a/client/src/pages/authentication/Logoff.jsx b/client/src/pages/authentication/Logoff.jsx new file mode 100644 index 0000000..98413e6 --- /dev/null +++ b/client/src/pages/authentication/Logoff.jsx @@ -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 + + + + You have been logged off. Redirecting you... + + + + ; +} + +export default Logout; diff --git a/client/src/pages/authentication/Register.jsx b/client/src/pages/authentication/Register.jsx index a22df0a..71a7d18 100644 --- a/client/src/pages/authentication/Register.jsx +++ b/client/src/pages/authentication/Register.jsx @@ -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 = () => ( - +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 - Sign up - - Already have an account? - + { + isInviteLink ? 'Invitation' : 'Password Reset' + } - + - -); + ; +} export default Register; diff --git a/client/src/pages/authentication/Signup.jsx b/client/src/pages/authentication/Signup.jsx new file mode 100644 index 0000000..eb02839 --- /dev/null +++ b/client/src/pages/authentication/Signup.jsx @@ -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 = () => ( + + + + + Sign up + + Already have an account? + + + + + + + + +); + +export default Signup; diff --git a/client/src/pages/authentication/auth-forms/AuthLogin.jsx b/client/src/pages/authentication/auth-forms/AuthLogin.jsx index 424dc14..52203de 100644 --- a/client/src/pages/authentication/auth-forms/AuthLogin.jsx +++ b/client/src/pages/authentication/auth-forms/AuthLogin.jsx @@ -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 = () => { - { /> Forgot Password? - + */} {errors.submit && ( diff --git a/client/src/pages/authentication/auth-forms/AuthRegister.jsx b/client/src/pages/authentication/auth-forms/AuthRegister.jsx index d905875..9a8d6a4 100644 --- a/client/src/pages/authentication/auth-forms/AuthRegister.jsx +++ b/client/src/pages/authentication/auth-forms/AuthRegister.jsx @@ -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 = () => { <> { 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 }) => (
- + {isInviteLink ? + + Invite Link - 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. + + : ''} + {isInviteLink ? - First Name* + Nickname - {touched.firstname && errors.firstname && ( - - {errors.firstname} + {touched.nickname && errors.nickname && ( + + {errors.nickname} )} - - - - Last Name* - - {touched.lastname && errors.lastname && ( - - {errors.lastname} - - )} - - - - - Company - - {touched.company && errors.company && ( - - {errors.company} - - )} - - - - - Email Address* - - {touched.email && errors.email && ( - - {errors.email} - - )} - - + : ''} Password @@ -198,7 +150,7 @@ const AuthRegister = () => { } - placeholder="******" + placeholder="********" inputProps={{}} /> {touched.password && errors.password && ( @@ -220,18 +172,6 @@ const AuthRegister = () => { - - - By Signing up, you agree to our   - - Terms of Service - -   and   - - Privacy Policy - - - {errors.submit && ( {errors.submit} @@ -248,18 +188,12 @@ const AuthRegister = () => { variant="contained" color="primary" > - Create Account + { + isRegister ? 'Register' : 'Reset Password' + } - - - Sign up with - - - - -
)} diff --git a/client/src/pages/config/users/configman.jsx b/client/src/pages/config/users/configman.jsx new file mode 100644 index 0000000..848d6ae --- /dev/null +++ b/client/src/pages/config/users/configman.jsx @@ -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
+

+ {config && <> + + { + 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 }) => ( +
+ + + + This page allow you to edit the configuration file. Any Environment Variable overwritting configuration won't appear here. + + + + MongoDB connection string. It is advised to use Environment variable to store this securely instead. (Optional) + + {touched.MongoDB && errors.MongoDB && ( + + {errors.MongoDB} + + )} + + + + + + Level of logging (Default: INFO) + + + DEBUG + + + INFO + + + WARNING + + + ERROR + + + + + + + +

+ + + + + + Hostname: This will be used to restrict access to your Cosmos Server (Default: 0.0.0.0) + + {touched.Hostname && errors.Hostname && ( + + {errors.Hostname} + + )} + + + + + + HTTP Port (Default: 80) + + {touched.HTTPPort && errors.HTTPPort && ( + + {errors.HTTPPort} + + )} + + + + + + HTTPS Port (Default: 443) + + {touched.HTTPSPort && errors.HTTPSPort && ( + + {errors.HTTPSPort} + + )} + + + + +

+ + + + 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. + + + + } + label="Generate missing HTTPS Certificates automatically (Default: true)" + /> + + + + + + } + label="Generate missing Authentication Certificates automatically (Default: true)" + /> + + + + +

Authentication Public Key

+ +
+                      {config.HTTPConfig.AuthPublicKey}
+                    
+
+
+ + +

Root HTTPS Public Key

+ +
+                      {config.HTTPConfig.TLSCert}
+                    
+
+
+ +
+
+ +

+ + + {errors.submit && ( + + {errors.submit} + + )} + + + + + + +
+ )} +
+ } +
; +} + +export default ConfigManagement; \ No newline at end of file diff --git a/client/src/pages/config/users/formShortcuts.jsx b/client/src/pages/config/users/formShortcuts.jsx new file mode 100644 index 0000000..807ecf0 --- /dev/null +++ b/client/src/pages/config/users/formShortcuts.jsx @@ -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 + + {label} + + {formik.touched[name] && formik.errors[name] && ( + + {formik.errors[name]} + + )} + + +} + +export const CosmosSelect = ({ name, label, formik, options }) => { + return + + {label} + + {options.map((option) => ( + + {option[1]} + + ))} + + + ; +} + +export const CosmosCheckbox = ({ name, label, formik }) => { + return + + } + label={label} + /> + + +} + +export const CosmosCollapse = ({ children, title }) => { + const [open, setOpen] = React.useState(false); + return + + + } + aria-controls="panel1a-content" + id="panel1a-header" + > + {title} + + + {children} + + + + +} + +export function CosmosFormDivider({title}) { + return + + + + +} \ No newline at end of file diff --git a/client/src/pages/config/users/proxyman.jsx b/client/src/pages/config/users/proxyman.jsx new file mode 100644 index 0000000..b7a2033 --- /dev/null +++ b/client/src/pages/config/users/proxyman.jsx @@ -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
+    + + +

+ + {config && <> + + {routes && routes.map((route,key) => (<> + { + routes[key] = newRoute; + }} + up={() => up(key)} + down={() => down(key)} + deleteRoute={() => deleteRoute(key)} + /> +

+ ))} + + {routes && + + {error && ( + + {error} + + )} + + + + + + + } + {!routes && <> + + No routes configured. + + + } + } +
; +} + +export default ProxyManagement; \ No newline at end of file diff --git a/client/src/pages/config/users/restart.jsx b/client/src/pages/config/users/restart.jsx new file mode 100644 index 0000000..f2395e0 --- /dev/null +++ b/client/src/pages/config/users/restart.jsx @@ -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 <> + setOpenModal(false)}> + Restart Server + + + A restart is required to apply changes. Do you want to restart? + + + + + + + + ; +}; + +export default RestartModal; diff --git a/client/src/pages/config/users/routeman.jsx b/client/src/pages/config/users/routeman.jsx new file mode 100644 index 0000000..0e5a90a --- /dev/null +++ b/client/src/pages/config/users/routeman.jsx @@ -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
+ {routeConfig && <> + { + setRouteConfig(values); + }} + onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { + return false; + }} + > + {(formik) => ( +
+ {routeConfig.Name}   + } onClick={() => up()}/>   + } onClick={() => down()}/>   + {!confirmDelete && (} onClick={() => setConfirmDelete(true)}/>)} + {confirmDelete && (} onClick={() => deleteRoute()}/>)}   +
+ }> + + + + + + + + + + + + + + + + + + + {formik.values.UseHost && } + + + + {formik.values.UsePathPrefix && } + + {formik.values.UsePathPrefix && } + + + + + + + + + + + + + + )} +
+ } + ; +} + +export default RouteManagement; \ No newline at end of file diff --git a/client/src/pages/config/users/usermanagement.jsx b/client/src/pages/config/users/usermanagement.jsx index 62e1555..a275161 100644 --- a/client/src/pages/config/users/usermanagement.jsx +++ b/client/src/pages/config/users/usermanagement.jsx @@ -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 ? () : () } diff --git a/client/src/routes/LoginRoutes.jsx b/client/src/routes/LoginRoutes.jsx index ffd0031..63d2195 100644 --- a/client/src/routes/LoginRoutes.jsx +++ b/client/src/routes/LoginRoutes.jsx @@ -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: , children: [ { - path: 'login', + path: '/ui/login', element: }, { - path: 'register', + path: '/ui/register', element: + }, + { + path: '/ui/logout', + element: } ] }; diff --git a/client/src/routes/MainRoutes.jsx b/client/src/routes/MainRoutes.jsx index 6b567bd..7e0b84d 100644 --- a/client/src/routes/MainRoutes.jsx +++ b/client/src/routes/MainRoutes.jsx @@ -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: , children: [ { - path: '/', + path: '/ui/', element: }, { - path: 'color', + path: '/ui/color', element: }, { - path: 'dashboard', + path: '/ui/dashboard', children: [ { path: 'default', @@ -41,19 +43,27 @@ const MainRoutes = { ] }, { - path: 'config/users', + path: '/ui/config/users', element: }, { - path: 'shadow', + path: '/ui/config/general', + element: + }, + { + path: '/ui/config/proxy', + element: + }, + { + path: '/ui/shadow', element: }, { - path: 'typography', + path: '/ui/typography', element: }, { - path: 'icons/ant', + path: '/ui/icons/ant', element: } ] diff --git a/client/src/utils/password-strength.jsx b/client/src/utils/password-strength.jsx index 51e812d..2d1927e 100644 --- a/client/src/utils/password-strength.jsx +++ b/client/src/utils/password-strength.jsx @@ -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; diff --git a/gupm.json b/gupm.json index 89e7ac9..30c6762 100644 --- a/gupm.json +++ b/gupm.json @@ -86,6 +86,6 @@ }, "description": "Cosmos Server", "name": "cosmos-server", - "version": "0.0.4", + "version": "0.0.5", "wrapInstallFolder": "src" } \ No newline at end of file diff --git a/src/configapi/get.go b/src/configapi/get.go index ce66b71..de94e25 100644 --- a/src/configapi/get.go +++ b/src/configapi/get.go @@ -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", diff --git a/src/configapi/restart.go b/src/configapi/restart.go index b15f716..e9a585f 100644 --- a/src/configapi/restart.go +++ b/src/configapi/restart.go @@ -1,23 +1,21 @@ -package user +package configapi import ( "net/http" - "encoding/json" - "github.com/gorilla/mux" + "encoding/json" "../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") diff --git a/src/configapi/route.go b/src/configapi/route.go new file mode 100644 index 0000000..7d7e724 --- /dev/null +++ b/src/configapi/route.go @@ -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 + } +} \ No newline at end of file diff --git a/src/configapi/set.go b/src/configapi/set.go index 60053fe..f6fd41c 100644 --- a/src/configapi/set.go +++ b/src/configapi/set.go @@ -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) diff --git a/src/httpServer.go b/src/httpServer.go index 8e11a82..fb664e1 100644 --- a/src/httpServer.go +++ b/src/httpServer.go @@ -4,6 +4,7 @@ import ( "net/http" "./utils" "./user" + "./configapi" "./proxy" "github.com/gorilla/mux" "strings" @@ -158,6 +159,10 @@ func StartServer() { router.Use(middleware.Logger) 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() @@ -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) diff --git a/src/proxy/buildFromConfig.go b/src/proxy/buildFromConfig.go index 8c9659d..ad6208b 100644 --- a/src/proxy/buildFromConfig.go +++ b/src/proxy/buildFromConfig.go @@ -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 diff --git a/src/proxy/routerGen.go b/src/proxy/routerGen.go index 57677f8..8772bf5 100644 --- a/src/proxy/routerGen.go +++ b/src/proxy/routerGen.go @@ -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 diff --git a/src/user/create.go b/src/user/create.go index 79b5a3f..ff92b9c 100644 --- a/src/user/create.go +++ b/src/user/create.go @@ -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 } diff --git a/src/user/delete.go b/src/user/delete.go index 2607ad5..2f36323 100644 --- a/src/user/delete.go +++ b/src/user/delete.go @@ -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 } diff --git a/src/user/edit.go b/src/user/edit.go index 85403a5..0cc39e6 100644 --- a/src/user/edit.go +++ b/src/user/edit.go @@ -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 } diff --git a/src/user/get.go b/src/user/get.go index eef542a..56edef5 100644 --- a/src/user/get.go +++ b/src/user/get.go @@ -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 } diff --git a/src/user/list.go b/src/user/list.go index 7ab3410..f446893 100644 --- a/src/user/list.go +++ b/src/user/list.go @@ -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 } diff --git a/src/user/resend.go b/src/user/resend.go index b5d317d..69160ef 100644 --- a/src/user/resend.go +++ b/src/user/resend.go @@ -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 } diff --git a/src/user/token.go b/src/user/token.go index b144212..c07d20b 100644 --- a/src/user/token.go +++ b/src/user/token.go @@ -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) { @@ -156,62 +155,4 @@ 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 } \ No newline at end of file diff --git a/src/utils/types.go b/src/utils/types.go index 4fcdc38..798d666 100644 --- a/src/utils/types.go +++ b/src/utils/types.go @@ -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 -} \ No newline at end of file + Target string `validate:"required"` + Mode ProxyMode +} diff --git a/src/utils/utils.go b/src/utils/utils.go index 255c439..f7f92a4 100644 --- a/src/utils/utils.go +++ b/src/utils/utils.go @@ -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) } @@ -194,4 +195,63 @@ func SaveConfigTofile(config Config) { 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 } \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 12c14e3..90b4275 100644 --- a/vite.config.js +++ b/vite.config.js @@ -8,6 +8,7 @@ export default defineConfig({ build: { outDir: '../static', }, + // base: '/ui', server: { proxy: { '/cosmos/api': {