v0.2.0-unstable

new UI
This commit is contained in:
Yann Stepienik 2023-04-28 19:28:01 +01:00
parent 963a1c7699
commit 1ad6edf50a
27 changed files with 879 additions and 536 deletions

View file

@ -13,9 +13,11 @@ import { setSnackit } from './api/wrap';
const App = () => {
const [open, setOpen] = React.useState(false);
const [message, setMessage] = React.useState('');
setSnackit((message) => {
const [severity, setSeverity] = React.useState('error');
setSnackit((message, severity='error') => {
setMessage(message);
setOpen(true);
setSeverity(severity);
})
return (
<ThemeCustomization>
@ -25,7 +27,7 @@ const App = () => {
onClose={() => {setOpen(false)}}
anchorOrigin={{ vertical: 'top', horizontal: 'center' }}
>
<Alert className={open ? 'shake' : ''} severity="error" sx={{ width: '100%' }}>
<Alert className={(open && severity == "error") ? 'shake' : ''} severity={severity} sx={{ width: '100%' }}>
{message}
</Alert>
</Snackbar>

View file

@ -1,35 +0,0 @@
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
};

80
client/src/api/config.ts Normal file
View file

@ -0,0 +1,80 @@
import wrap from './wrap';
interface Route {
Name: string;
}
type Operation = 'replace' | 'move_up' | 'move_down' | 'delete';
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 fetch('/cosmos/api/restart', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
},
})
}
async function rawUpdateRoute(routeName: string, operation: Operation, newRoute?: Route): Promise<void> {
const payload = {
routeName,
operation,
newRoute,
};
if (operation === 'replace') {
if (!newRoute) throw new Error('newRoute must be provided for replace operation');
}
return wrap(fetch('/cosmos/api/config', {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
}));
}
async function replaceRoute(routeName: string, newRoute: Route): Promise<void> {
return rawUpdateRoute(routeName, 'replace', newRoute);
}
async function moveRouteUp(routeName: string): Promise<void> {
return rawUpdateRoute(routeName, 'move_up');
}
async function moveRouteDown(routeName: string): Promise<void> {
return rawUpdateRoute(routeName, 'move_down');
}
async function deleteRoute(routeName: string): Promise<void> {
return rawUpdateRoute(routeName, 'delete');
}
export {
get,
set,
restart,
rawUpdateRoute,
replaceRoute,
moveRouteUp,
moveRouteDown,
deleteRoute,
};

View file

@ -1,7 +1,7 @@
import * as auth from './authentication.jsx';
import * as users from './users.jsx';
import * as config from './config.jsx';
import * as docker from './docker.jsx';
import * as auth from './authentication';
import * as users from './users';
import * as config from './config';
import * as docker from './docker';
import wrap from './wrap';
@ -14,6 +14,28 @@ const getStatus = () => {
}))
}
const isOnline = () => {
return fetch('/cosmos/api/status', {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
}).then(async (response) => {
let rep;
try {
rep = await response.json();
} catch {
throw new Error('Server error');
}
if (response.status == 200) {
return rep;
}
const e = new Error(rep.message);
e.status = response.status;
throw e;
});
}
const newInstall = (req) => {
return wrap(fetch('/cosmos/api/newInstall', {
method: 'POST',
@ -31,4 +53,5 @@ export {
docker,
getStatus,
newInstall,
isOnline
};

View file

@ -21,4 +21,8 @@ export default function wrap(apicall) {
export function setSnackit(snack) {
snackit = snack;
}
}
export {
snackit
};

View file

@ -0,0 +1,16 @@
import { LeftOutlined } from "@ant-design/icons";
import { IconButton } from "@mui/material";
import { useNavigate } from "react-router";
function Back() {
const navigate = useNavigate();
const goBack = () => {
navigate(-1);
}
return <IconButton onClick={goBack}>
<LeftOutlined />
</IconButton>
;
}
export default Back;

View file

@ -24,7 +24,10 @@ const HostChip = ({route, settings}) => {
return <Chip
label={((isOnline == null) ? "⚪" : (isOnline ? "🟢 " : "🔴 ")) + url}
color="secondary"
style={{paddingRight: '4px'}}
style={{
paddingRight: '4px',
textDecoration: isOnline ? 'none' : 'underline wavy red',
}}
onClick={() => {
if(route.UseHost)
window.open(window.location.origin.split("://")[0] + "://" + route.Host + route.PathPrefix, '_blank');

View file

@ -118,13 +118,13 @@ export const RouteActions = ({route, routeKey, up, down, deleteRoute}) => {
return <>
<Stack direction={'row'} spacing={2} alignItems={'center'} justifyContent={'right'}>
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
{confirmDelete && (<Chip label={<CheckOutlined />} color="error" onClick={() => deleteRoute()}/>)}
{confirmDelete && (<Chip label={<CheckOutlined />} color="error" onClick={(event) => deleteRoute(event)}/>)}
<Tooltip title='Routes with the lowest priority are matched first'>
<Stack direction={'column'} spacing={0}>
<Card sx={{...miniChip, borderBottom: 'none'}} onClick={() => up()}><UpOutlined /></Card>
<Card sx={{...miniChip, borderBottom: 'none'}} onClick={(event) => up(event)}><UpOutlined /></Card>
<Card sx={{...miniChip, cursor: 'auto'}}>{routeKey}</Card>
<Card sx={{...miniChip, borderTop: 'none'}} onClick={() => down()}><DownOutlined /></Card>
<Card sx={{...miniChip, borderTop: 'none'}} onClick={(event) => down(event)}><DownOutlined /></Card>
</Stack>
</Tooltip>
</Stack>

View file

@ -0,0 +1,83 @@
import React, { useState } from 'react';
import { Box, Tab, Tabs, Typography, MenuItem, Select, useMediaQuery } from '@mui/material';
import { styled } from '@mui/system';
const StyledTabs = styled(Tabs)`
border-right: 1px solid ${({ theme }) => theme.palette.divider};
`;
const TabPanel = (props) => {
const { children, value, index, ...other } = props;
return (
<div
role="tabpanel"
style={{
width: '100%',
}}
hidden={value !== index}
id={`vertical-tabpanel-${index}`}
aria-labelledby={`vertical-tab-${index}`}
{...other}
>
{value === index && (
<Box p={3}>
<Typography>{children}</Typography>
</Box>
)}
</div>
);
};
const a11yProps = (index) => {
return {
id: `vertical-tab-${index}`,
'aria-controls': `vertical-tabpanel-${index}`,
};
};
const PrettyTabbedView = ({ tabs }) => {
const [value, setValue] = useState(0);
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
const handleChange = (event, newValue) => {
setValue(newValue);
};
const handleSelectChange = (event) => {
setValue(event.target.value);
};
return (
<Box display="flex" height="100%" flexDirection={isMobile ? 'column' : 'row'}>
{isMobile ? (
<Select value={value} onChange={handleSelectChange} sx={{ minWidth: 120 }}>
{tabs.map((tab, index) => (
<MenuItem key={index} value={index}>
{tab.title}
</MenuItem>
))}
</Select>
) : (
<StyledTabs
orientation="vertical"
variant="scrollable"
value={value}
onChange={handleChange}
aria-label="Vertical tabs"
>
{tabs.map((tab, index) => (
<Tab key={index} label={tab.title} {...a11yProps(index)} />
))}
</StyledTabs>
)}
{tabs.map((tab, index) => (
<TabPanel key={index} value={value} index={index}>
{tab.children}
</TabPanel>
))}
</Box>
);
};
export default PrettyTabbedView;

View file

@ -9,8 +9,9 @@ import Paper from '@mui/material/Paper';
import { Input, InputAdornment, Stack, TextField } from '@mui/material';
import { SearchOutlined } from '@ant-design/icons';
import { useTheme } from '@mui/material/styles';
import { Link } from 'react-router-dom';
const PrettyTableView = ({ getKey, data, columns, onRowClick }) => {
const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => {
const [search, setSearch] = React.useState('');
const theme = useTheme();
const isDark = theme.palette.mode === 'dark';
@ -59,18 +60,28 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick }) => {
key={getKey(row)}
sx={{
cursor: 'pointer',
borderLeft: 'transparent solid 5px',
borderLeft: 'transparent solid 2px',
'&:last-child td, &:last-child th': { border: 0 },
'&:hover': {
backgroundColor: 'rgba(0, 0, 0, 0.04)',
backgroundColor: 'rgba(0, 0, 0, 0.06)',
borderColor: 'gray',
textDecoration: 'underline',
},
}}
>
{columns.map((column) => (
<TableCell
style={column.style}
>{column.field(row, key)}</TableCell>
<TableCell sx={column.style}>
{!column.clickable ? <Link
to={linkTo && linkTo(row, key)}
style={{
color: 'inherit',
textDecoration: 'inherit',
}}
>
{column.field(row, key)}
</Link> : column.field(row, key)}
</TableCell>
))}
</TableRow>
))}

View file

@ -25,6 +25,16 @@
overflow: hidden;
}
.stickyButton {
position: fixed;
bottom: 20px;
width: 100%;
left: 0;
right: 0;
box-shadow: 0px 0px 10px 0px rgba(0,0,0,0.50);
z-index: 10;
}
.shinyButton:before {
position: absolute;
content: '';
@ -44,4 +54,4 @@
.code {
background-color: rgba(0.2,0.2,0.2,0.2);
}
}

View file

@ -50,7 +50,7 @@ const MainLayout = () => {
<Drawer open={open} handleDrawerToggle={handleDrawerToggle} />
<Box component="main" sx={{ width: '100%', flexGrow: 1, p: { xs: 2, sm: 3 } }}>
<Toolbar />
<Breadcrumbs navigation={navigation} title divider={false} />
<Breadcrumbs navigation={navigation} divider={false} />
<Outlet />
</Box>
</Box>

View file

@ -1,194 +1,71 @@
import * as React from 'react';
import MainCard from '../../../components/MainCard';
import { Formik } from 'formik';
import * as Yup from 'yup';
import {
Alert,
Grid,
FormHelperText,
Chip,
import { useParams } from "react-router";
import Back from "../../components/back";
import { CircularProgress, Stack } from "@mui/material";
import PrettyTabbedView from "../../components/tabbedView/tabbedView";
import RouteManagement from "./routes/routeman";
import { useEffect, useState } from "react";
import * as API from "../../api";
import RouteSecurity from "./routes/routeSecurity";
import RouteOverview from "./routes/routeoverview";
} from '@mui/material';
import { CosmosCheckbox, CosmosCollapse, CosmosFormDivider, CosmosInputText, CosmosSelect } from './formShortcuts';
import { DownOutlined, UpOutlined, CheckOutlined, DeleteOutlined } from '@ant-design/icons';
import { CosmosContainerPicker } from './containerPicker';
import { ValidateRoute } from './users/routeman';
const RouteConfigPage = () => {
const { routeName } = useParams();
const [config, setConfig] = useState(null);
let currentRoute = null;
if (config) {
currentRoute = config.HTTPConfig.ProxyConfig.Routes.find((r) => r.Name === routeName);
}
const RouteConfig = ({route, key, lockTarget, TargetContainer, setRouteConfig}) => {
return (<div style={{ maxWidth: '1000px', margin: '' }}>
{route && <>
<Formik
initialValues={{
Name: route.Name,
Description: route.Description,
Mode: route.Mode || "SERVAPP",
Target: route.Target,
UseHost: route.UseHost,
AuthEnabled: route.AuthEnabled,
Host: route.Host,
UsePathPrefix: route.UsePathPrefix,
PathPrefix: route.PathPrefix,
StripPathPrefix: route.StripPathPrefix,
Timeout: route.Timeout,
ThrottlePerMinute: route.ThrottlePerMinute,
CORSOrigin: route.CORSOrigin,
}}
validationSchema={ValidateRoute}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
return false;
}}
// validate={(values) => {
// setRouteConfig(values);
// }}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<MainCard name={route.Name} title={route.Name}>
<Grid container spacing={2}>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
const refreshConfig = () => {
API.config.get().then((res) => {
setConfig(res.data);
});
};
<CosmosInputText
name="Name"
label="Name"
placeholder="Name"
formik={formik}
/>
useEffect(() => {
refreshConfig();
}, []);
<CosmosInputText
name="Description"
label="Description"
placeholder="Description"
formik={formik}
/>
return <div>
<h2>
<Stack spacing={1}>
<Stack direction="row" spacing={1} alignItems="center">
<Back />
<div>{routeName}</div>
</Stack>
<CosmosCollapse title="Settings">
<Grid container spacing={2}>
<CosmosFormDivider title={'Target Type'}/>
<Grid item xs={12}>
<Alert color='info'>What are you trying to access with this route?</Alert>
</Grid>
{config && <PrettyTabbedView tabs={[
{
title: 'Overview',
children: <RouteOverview routeConfig={currentRoute} />
},
{
title: 'Setup',
children: <RouteManagement
title="Setup"
submitButton
routeConfig={currentRoute}
/>
},
{
title: 'Security',
children: <RouteSecurity
routeConfig={currentRoute}
/>
},
{
title: 'Permissions',
children: <div>WIP</div>
},
]}/>}
<CosmosSelect
name="Mode"
label="Mode"
formik={formik}
disabled={lockTarget}
options={[
["SERVAPP", "ServApp - Docker Container"],
["PROXY", "Proxy"],
["STATIC", "Static Folder"],
["SPA", "Single Page Application"],
["REDIRECT", "Redirection"]
]}
/>
<CosmosFormDivider title={'Target Settings'}/>
{
(formik.values.Mode === "SERVAPP")?
<CosmosContainerPicker
formik={formik}
lockTarget={lockTarget}
TargetContainer={TargetContainer}
onTargetChange={() => {
setRouteConfig(formik.values);
}}
/>
: <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'}/>
<Grid item xs={12}>
<Alert color='info'>What URL do you want to access your target from?</Alert>
</Grid>
<CosmosCheckbox
name="UseHost"
label="Use Host"
formik={formik}
/>
{formik.values.UseHost && <CosmosInputText
name="Host"
label="Host"
placeholder="Host"
formik={formik}
style={{paddingLeft: '20px'}}
/>}
<CosmosCheckbox
name="UsePathPrefix"
label="Use Path Prefix"
formik={formik}
/>
{formik.values.UsePathPrefix && <CosmosInputText
name="PathPrefix"
label="Path Prefix"
placeholder="Path Prefix"
formik={formik}
style={{paddingLeft: '20px'}}
/>}
{formik.values.UsePathPrefix && <CosmosCheckbox
name="StripPathPrefix"
label="Strip Path Prefix"
formik={formik}
style={{paddingLeft: '20px'}}
/>}
<CosmosFormDivider title={'Security'}/>
<Grid item xs={12}>
<Alert color='info'>Additional security settings. MFA and Captcha are not yet implemented.</Alert>
</Grid>
<CosmosCheckbox
name="AuthEnabled"
label="Authentication Required"
formik={formik}
/>
<CosmosInputText
name="Timeout"
label="Timeout in milliseconds (0 for no timeout, at least 30000 or less recommended)"
placeholder="Timeout"
type="number"
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"
type="number"
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>)
{!config && <div style={{textAlign: 'center'}}>
<CircularProgress />
</div>}
</Stack>
</h2>
</div>
}
export default RouteConfig;
export default RouteConfigPage;

View file

@ -0,0 +1,119 @@
import * as React from 'react';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik } from 'formik';
import {
Alert,
Button,
Grid,
Stack,
} from '@mui/material';
import RestartModal from '../users/restart';
import { CosmosCheckbox, CosmosInputText } from '../users/formShortcuts';
import { snackit } from '../../../api/wrap';
const RouteSecurity = ({ routeConfig }) => {
const [openModal, setOpenModal] = React.useState(false);
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
{routeConfig && <>
<Formik
initialValues={{
AuthEnabled: routeConfig.AuthEnabled,
Host: routeConfig.Host,
UsePathPrefix: routeConfig.UsePathPrefix,
PathPrefix: routeConfig.PathPrefix,
StripPathPrefix: routeConfig.StripPathPrefix,
Timeout: routeConfig.Timeout,
ThrottlePerMinute: routeConfig.ThrottlePerMinute,
CORSOrigin: routeConfig.CORSOrigin,
MaxBandwith: routeConfig.MaxBandwith,
}}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
const fullValues = {
...routeConfig,
...values,
}
API.config.replaceRoute(routeConfig.Name, fullValues).then((res) => {
if (res.status == "OK") {
setStatus({ success: true });
snackit('Route updated successfully', 'success');
setSubmitting(false);
setOpenModal(true);
} else {
setStatus({ success: false });
setErrors({ submit: res.status });
setSubmitting(false);
}
});
}}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<Stack spacing={2}>
<MainCard name={routeConfig.Name} title={'Security'}>
<Grid container spacing={2}>
<Grid item xs={12}>
<Alert color='info'>Additional security settings. MFA and Captcha are not yet implemented.</Alert>
</Grid>
<CosmosCheckbox
name="AuthEnabled"
label="Authentication Required"
formik={formik}
/>
<CosmosInputText
name="Timeout"
label="Timeout in milliseconds (0 for no timeout, at least 30000 or less recommended)"
placeholder="Timeout"
type="number"
formik={formik}
/>
<CosmosInputText
name="MaxBandwith"
label="Maximum Bandwith limit per user in bytes per seconds (0 for no limit)"
placeholder="Maximum Bandwith"
type="number"
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"
type="number"
formik={formik}
/>
<CosmosInputText
name="CORSOrigin"
label="Custom CORS Origin (Recommended to leave blank)"
placeholder="CORS Origin"
formik={formik}
/>
</Grid>
</MainCard>
<MainCard ><Button
fullWidth
disableElevation
size="large"
type="submit"
variant="contained"
color="primary"
>
Save
</Button></MainCard>
</Stack>
</form>
)}
</Formik>
</>}
</div>;
}
export default RouteSecurity;

View file

@ -0,0 +1,221 @@
import * as React from 'react';
import * as API from '../../../api';
import MainCard from '../../../components/MainCard';
import { Formik } from 'formik';
import * as Yup from 'yup';
import {
Alert,
Button,
Grid,
Stack,
FormHelperText,
} from '@mui/material';
import RestartModal from '../users/restart';
import { CosmosCheckbox, CosmosFormDivider, CosmosInputText, CosmosSelect } from '../users/formShortcuts';
import { CosmosContainerPicker } from '../users/containerPicker';
import { snackit } from '../../../api/wrap';
export const ValidateRoute = Yup.object().shape({
Name: Yup.string().required('Name is required'),
Mode: Yup.string().required('Mode is required'),
Target: Yup.string().required('Target is required').when('Mode', {
is: 'SERVAPP',
then: Yup.string().matches(/:[0-9]+$/, 'Invalid Target, must have a port'),
}),
Host: Yup.string().when('UseHost', {
is: true,
then: Yup.string().required('Host is required')
.matches(/[\.|\:]/, 'Host must be full domain ([sub.]domain.com) or an IP')
}),
PathPrefix: Yup.string().when('UsePathPrefix', {
is: true,
then: Yup.string().required('Path Prefix is required').matches(/^\//, 'Path Prefix must start with / (e.g. /api). Do not include a domain/subdomain in it, use the Host for this.')
}),
UseHost: Yup.boolean().when('UsePathPrefix',
{
is: false,
then: Yup.boolean().oneOf([true], 'Source must at least be either Host or Path Prefix')
}),
})
const Hide = ({ children, h }) => {
return h ? <div style={{ display: 'none' }}>
{children}
</div> : <>{children}</>
}
const RouteManagement = ({ routeConfig, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false }) => {
const [openModal, setOpenModal] = React.useState(false);
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
{routeConfig && <>
<Formik
initialValues={{
Name: routeConfig.Name,
Description: routeConfig.Description,
Mode: routeConfig.Mode || "SERVAPP",
Target: routeConfig.Target,
UseHost: routeConfig.UseHost,
Host: routeConfig.Host,
}}
validationSchema={ValidateRoute}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
if(!submitButton) {
return false;
} else {
const fullValues = {
...routeConfig,
...values,
}
API.config.replaceRoute(routeConfig.Name, fullValues).then((res) => {
if (res.status == "OK") {
setStatus({ success: true });
snackit('Route updated successfully', 'success')
setSubmitting(false);
setOpenModal(true);
} else {
setStatus({ success: false });
setErrors({ submit: res.status });
setSubmitting(false);
}
});
}
}}
validate={(values) => {
setRouteConfig && setRouteConfig(values);
}}
>
{(formik) => (
<form noValidate onSubmit={formik.handleSubmit}>
<Stack spacing={2}>
<MainCard name={routeConfig.Name} title={
noControls ? 'New URL' :
<div>{title || routeConfig.Name}</div>
}>
<Grid container spacing={2}>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<CosmosInputText
name="Name"
label="Name"
placeholder="Name"
formik={formik}
/>
<CosmosInputText
name="Description"
label="Description"
placeholder="Description"
formik={formik}
/>
<Hide h={lockTarget}>
<CosmosFormDivider title={'Target Type'} />
<Grid item xs={12}>
<Alert color='info'>What are you trying to access with this route?</Alert>
</Grid>
<CosmosSelect
name="Mode"
label="Mode"
formik={formik}
disabled={lockTarget}
options={[
["SERVAPP", "ServApp - Docker Container"],
["PROXY", "Proxy"],
["STATIC", "Static Folder"],
["SPA", "Single Page Application"],
["REDIRECT", "Redirection"]
]}
/>
</Hide>
<CosmosFormDivider title={'Target Settings'} />
{
(formik.values.Mode === "SERVAPP") ?
<CosmosContainerPicker
formik={formik}
lockTarget={lockTarget}
TargetContainer={TargetContainer}
onTargetChange={() => {
setRouteConfig && setRouteConfig(formik.values);
}}
/>
: <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'} />
<Grid item xs={12}>
<Alert color='info'>What URL do you want to access your target from?</Alert>
</Grid>
<CosmosCheckbox
name="UseHost"
label="Use Host"
formik={formik}
/>
{formik.values.UseHost && <CosmosInputText
name="Host"
label="Host"
placeholder="Host"
formik={formik}
style={{ paddingLeft: '20px' }}
/>}
<CosmosCheckbox
name="UsePathPrefix"
label="Use Path Prefix"
formik={formik}
/>
{formik.values.UsePathPrefix && <CosmosInputText
name="PathPrefix"
label="Path Prefix"
placeholder="Path Prefix"
formik={formik}
style={{ paddingLeft: '20px' }}
/>}
{formik.values.UsePathPrefix && <CosmosCheckbox
name="StripPathPrefix"
label="Strip Path Prefix"
formik={formik}
style={{ paddingLeft: '20px' }}
/>}
</Grid>
</MainCard>
{submitButton && <MainCard ><Button
fullWidth
disableElevation
size="large"
type="submit"
variant="contained"
color="primary"
>
Save
</Button></MainCard>}
</Stack>
</form>
)}
</Formik>
</>}
</div>;
}
export default RouteManagement;

View file

@ -0,0 +1,38 @@
import * as React from 'react';
import MainCard from '../../../components/MainCard';
import RestartModal from '../users/restart';
import { Chip, Stack, useMediaQuery } from '@mui/material';
import HostChip from '../../../components/HostChip';
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
import { getFaviconURL } from '../../../utils/routes';
const RouteOverview = ({ routeConfig }) => {
const [openModal, setOpenModal] = React.useState(false);
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
return <div style={{ maxWidth: '1000px', width: '100%'}}>
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
{routeConfig && <>
<MainCard name={routeConfig.Name} title={routeConfig.Name}>
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
<div>
<img src={getFaviconURL(routeConfig)} width="128px" />
</div>
<Stack spacing={2}>
<strong>Description</strong>
<div>{routeConfig.Description}</div>
<strong>URL</strong>
<div><HostChip route={routeConfig} /></div>
<strong>Target</strong>
<div><RouteMode route={routeConfig} /> <Chip label={routeConfig.Target} /></div>
<strong>Security</strong>
<div><RouteSecurity route={routeConfig} /></div>
</Stack>
</Stack>
</MainCard>
</>}
</div>;
}
export default RouteOverview;

View file

@ -25,17 +25,19 @@ import {
TextField,
MenuItem,
Chip,
CircularProgress,
} from '@mui/material';
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
import AnimateButton from '../../../components/@extended/AnimateButton';
import RestartModal from './restart';
import RouteManagement, {ValidateRoute} from './routeman';
import RouteManagement, {ValidateRoute} from '../routes/routeman';
import { map } from 'lodash';
import { getFaviconURL, sanitizeRoute } from '../../../utils/routes';
import PrettyTableView from '../../../components/tableView/prettyTableView';
import HostChip from '../../../components/hostChip';
import {RouteActions, RouteMode, RouteSecurity} from '../../../components/routeComponents';
import { useNavigate } from 'react-router';
const stickyButton = {
position: 'fixed',
@ -60,6 +62,7 @@ const ProxyManagement = () => {
const [error, setError] = React.useState(null);
const [submitErrors, setSubmitErrors] = React.useState([]);
const [needSave, setNeedSave] = React.useState(false);
const navigate = useNavigate();
function updateRoutes(routes) {
let con = {
@ -90,7 +93,8 @@ const ProxyManagement = () => {
});
}
function up(key) {
function up(event, key) {
event.stopPropagation();
if (key > 0) {
let tmp = routes[key];
routes[key] = routes[key-1];
@ -98,15 +102,19 @@ const ProxyManagement = () => {
updateRoutes(routes);
setNeedSave(true);
}
return false;
}
function deleteRoute(key) {
function deleteRoute(event, key) {
event.stopPropagation();
routes.splice(key, 1);
updateRoutes(routes);
setNeedSave(true);
return false;
}
function down(key) {
function down(event, key) {
event.stopPropagation();
if (key < routes.length - 1) {
let tmp = routes[key];
routes[key] = routes[key+1];
@ -114,6 +122,7 @@ const ProxyManagement = () => {
updateRoutes(routes);
setNeedSave(true);
}
return false;
}
React.useEffect(() => {
@ -161,6 +170,7 @@ const ProxyManagement = () => {
{routes && <PrettyTableView
data={routes}
getKey={(r) => r.Name + r.Target + r.Mode}
onRowClick={(r) => {navigate('/ui/config-url/' + r.Name)}}
columns={[
{
title: '',
@ -171,9 +181,12 @@ const ProxyManagement = () => {
},
{ title: 'URL',
search: (r) => r.Name + ' ' + r.Description,
style: {
textDecoration: 'inherit',
},
field: (r) => <>
<div style={{display:'inline-block', fontSize:'125%', color: isDark ? theme.palette.primary.light : theme.palette.primary.dark}}>{r.Name}</div><br/>
<div style={{display:'inline-block', fontSize: '90%', opacity: '90%'}}>{r.Description}</div>
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize:'125%', color: isDark ? theme.palette.primary.light : theme.palette.primary.dark}}>{r.Name}</div><br/>
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize: '90%', opacity: '90%'}}>{r.Description}</div>
</>
},
// { title: 'Description', field: (r) => shorten(r.Description), style:{fontSize: '90%', opacity: '90%'} },
@ -182,12 +195,12 @@ const ProxyManagement = () => {
{ title: 'Target', search: (r) => r.Target, field: (r) => <><RouteMode route={r} /> <Chip label={r.Target} /></> },
{ title: 'Security', field: (r) => <RouteSecurity route={r} />,
style: {minWidth: '70px'} },
{ title: '', field: (r, k) => <RouteActions
{ title: '', clickable:true, field: (r, k) => <RouteActions
route={r}
routeKey={k}
up={() => up(k)}
down={() => down(k)}
deleteRoute={() => deleteRoute(k)}
up={(event) => up(event, k)}
down={(event) => down(event, k)}
deleteRoute={(event) => deleteRoute(event, k)}
/>,
style: {
textAlign: 'right',
@ -195,6 +208,11 @@ const ProxyManagement = () => {
},
]}
/>}
{
!routes && <div style={{textAlign: 'center'}}>
<CircularProgress />
</div>
}
{/* {routes && routes.map((route,key) => (<>
<RouteManagement key={route.Name} routeConfig={route}

View file

@ -1,6 +1,6 @@
// material-ui
import * as React from 'react';
import { Button, Typography } from '@mui/material';
import { Alert, Button, Stack, 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';
@ -23,28 +23,50 @@ import MainCard from '../../../components/MainCard';
import IsLoggedIn from '../../../IsLoggedIn';
import { useEffect, useState } from 'react';
function checkIsOnline() {
API.isOnline().then((res) => {
window.location.reload();
}).catch((err) => {
setTimeout(() => {
checkIsOnline();
}, 1000);
});
}
const RestartModal = ({openModal, setOpenModal}) => {
const [isRestarting, setIsRestarting] = useState(false);
const [warn, setWarn] = useState(false);
return <>
<Dialog open={openModal} onClose={() => setOpenModal(false)}>
<DialogTitle>Restart Server</DialogTitle>
<DialogTitle>{!isRestarting ? 'Restart Server?' : 'Restarting Server...'}</DialogTitle>
<DialogContent>
<DialogContentText>
A restart is required to apply changes. Do you want to restart?
{warn && <div>
<Alert severity="warning" icon={<WarningOutlined />}>
The server is taking longer than expected to restart.<br />Consider troubleshouting the logs.
</Alert>
</div>}
{isRestarting ?
<div style={{textAlign: 'center', padding: '20px'}}>
<CircularProgress />
</div>
: 'A restart is required to apply changes. Do you want to restart?'}
</DialogContentText>
</DialogContent>
<DialogActions>
{!isRestarting && <DialogActions>
<Button onClick={() => setOpenModal(false)}>Later</Button>
<Button onClick={() => {
setIsRestarting(true);
API.config.restart()
.then(() => {
refresh();
setOpenModal(false);
setTimeout(() => {
window.location.reload();
}, 2000)
})
setTimeout(() => {
checkIsOnline();
}, 1500)
setTimeout(() => {
setWarn(true);
}, 8000)
}}>Restart</Button>
</DialogActions>
</DialogActions>}
</Dialog>
</>;
};

View file

@ -1,257 +0,0 @@
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';
import { CosmosContainerPicker } from './containerPicker';
export const ValidateRoute = Yup.object().shape({
Name: Yup.string().required('Name is required'),
Mode: Yup.string().required('Mode is required'),
Target: Yup.string().required('Target is required').when('Mode', {
is: 'SERVAPP',
then: Yup.string().matches(/:[0-9]+$/, 'Invalid Target, must have a port'),
}),
Host: Yup.string().when('UseHost', {
is: true,
then: Yup.string().required('Host is required')
.matches(/[\.|\:]/, 'Host must be full domain ([sub.]domain.com) or an IP')
}),
PathPrefix: Yup.string().when('UsePathPrefix', {
is: true,
then: Yup.string().required('Path Prefix is required').matches(/^\//, 'Path Prefix must start with / (e.g. /api). Do not include a domain/subdomain in it, use the Host for this.')
}),
UseHost: Yup.boolean().when('UsePathPrefix',
{
is: false,
then: Yup.boolean().oneOf([true], 'Source must at least be either Host or Path Prefix')
}),
})
const RouteManagement = ({ routeConfig, TargetContainer, noControls=false, lockTarget=false, setRouteConfig, up, down, deleteRoute }) => {
const [confirmDelete, setConfirmDelete] = React.useState(false);
const myRef = React.useRef(null)
const currRef = myRef.current;
React.useEffect(() => {
if(currRef && window.location.hash === '#' + routeConfig.Name) {
currRef.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, [currRef])
return <div style={{ maxWidth: '1000px', margin: '' }}>
{routeConfig && <>
<Formik
initialValues={{
Name: routeConfig.Name,
Description: routeConfig.Description,
Mode: routeConfig.Mode || "SERVAPP",
Target: routeConfig.Target,
UseHost: routeConfig.UseHost,
AuthEnabled: routeConfig.AuthEnabled,
Host: routeConfig.Host,
UsePathPrefix: routeConfig.UsePathPrefix,
PathPrefix: routeConfig.PathPrefix,
StripPathPrefix: routeConfig.StripPathPrefix,
Timeout: routeConfig.Timeout,
ThrottlePerMinute: routeConfig.ThrottlePerMinute,
CORSOrigin: routeConfig.CORSOrigin,
}}
validateOnChange={false}
validationSchema={ValidateRoute}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
return false;
}}
validate={(values) => {
setRouteConfig(values);
}}
>
{(formik) => (
<form ref={myRef} noValidate onSubmit={formik.handleSubmit}>
<MainCard name={routeConfig.Name} title={
noControls ? 'New URL' :
<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}>
{formik.errors.submit && (
<Grid item xs={12}>
<FormHelperText error>{formik.errors.submit}</FormHelperText>
</Grid>
)}
<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 Type'}/>
<Grid item xs={12}>
<Alert color='info'>What are you trying to access with this route?</Alert>
</Grid>
<CosmosSelect
name="Mode"
label="Mode"
formik={formik}
disabled={lockTarget}
options={[
["SERVAPP", "ServApp - Docker Container"],
["PROXY", "Proxy"],
["STATIC", "Static Folder"],
["SPA", "Single Page Application"],
["REDIRECT", "Redirection"]
]}
/>
<CosmosFormDivider title={'Target Settings'}/>
{
(formik.values.Mode === "SERVAPP")?
<CosmosContainerPicker
formik={formik}
lockTarget={lockTarget}
TargetContainer={TargetContainer}
onTargetChange={() => {
setRouteConfig(formik.values);
}}
/>
: <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'}/>
<Grid item xs={12}>
<Alert color='info'>What URL do you want to access your target from?</Alert>
</Grid>
<CosmosCheckbox
name="UseHost"
label="Use Host"
formik={formik}
/>
{formik.values.UseHost && <CosmosInputText
name="Host"
label="Host"
placeholder="Host"
formik={formik}
style={{paddingLeft: '20px'}}
/>}
<CosmosCheckbox
name="UsePathPrefix"
label="Use Path Prefix"
formik={formik}
/>
{formik.values.UsePathPrefix && <CosmosInputText
name="PathPrefix"
label="Path Prefix"
placeholder="Path Prefix"
formik={formik}
style={{paddingLeft: '20px'}}
/>}
{formik.values.UsePathPrefix && <CosmosCheckbox
name="StripPathPrefix"
label="Strip Path Prefix"
formik={formik}
style={{paddingLeft: '20px'}}
/>}
<CosmosFormDivider title={'Security'}/>
<Grid item xs={12}>
<Alert color='info'>Additional security settings. MFA and Captcha are not yet implemented.</Alert>
</Grid>
<CosmosCheckbox
name="AuthEnabled"
label="Authentication Required"
formik={formik}
/>
<CosmosInputText
name="Timeout"
label="Timeout in milliseconds (0 for no timeout, at least 30000 or less recommended)"
placeholder="Timeout"
type="number"
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"
type="number"
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

@ -10,8 +10,8 @@ import { styled } from '@mui/material/styles';
import * as API from '../../api';
import IsLoggedIn from '../../IsLoggedIn';
import RestartModal from '../config/users/restart';
import RouteManagement, { ValidateRoute } from '../config/users/routeman';
import { sanitizeRoute } from '../../utils/routes';
import RouteManagement, { ValidateRoute } from '../config/routes/routeman';
import { getFaviconURL, sanitizeRoute } from '../../utils/routes';
import HostChip from '../../components/hostChip';
const Item = styled(Paper)(({ theme }) => ({
@ -123,6 +123,17 @@ const ServeApps = () => {
return name.replace('/', '').replace(/_/g, '-').replace(/[^a-zA-Z0-9-]/g, '').toLowerCase().replace(/\s/g, '-') + '.' + window.location.origin.split('://')[1]
}
const getFirstRouteFavIcon = (app) => {
let routes = getContainersRoutes(app.Names[0].replace('/', ''));
console.log(routes)
if(routes.length > 0) {
let url = getFaviconURL(routes[0]);
return url;
} else {
return getFaviconURL('');
}
}
return <div>
<IsLoggedIn />
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
@ -235,13 +246,16 @@ const ServeApps = () => {
})[app.State]
}
</Typography>
<Stack direction="column" spacing={0} alignItems="flex-start">
<Typography variant="h5" color="text.secondary">
{app.Names[0].replace('/', '')}&nbsp;
</Typography>
<Typography style={{ fontSize: '80%' }} color="text.secondary">
{app.Image}
</Typography>
<Stack direction="row" spacing={2} alignItems="center">
<img src={getFirstRouteFavIcon(app)} width="40px" />
<Stack direction="column" spacing={0} alignItems="flex-start" style={{height: '40px'}}>
<Typography variant="h5" color="text.secondary">
{app.Names[0].replace('/', '')}&nbsp;
</Typography>
<Typography style={{ fontSize: '80%' }} color="text.secondary">
{app.Image}
</Typography>
</Stack>
</Stack>
</Stack>
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">

View file

@ -8,6 +8,7 @@ import ConfigManagement from '../pages/config/users/configman';
import ProxyManagement from '../pages/config/users/proxyman';
import ServeApps from '../pages/servapps/servapps';
import { Navigate } from 'react-router';
import RouteConfigPage from '../pages/config/RouteConfig';
// render - dashboard
const DashboardDefault = Loadable(lazy(() => import('../pages/dashboard')));
@ -52,6 +53,10 @@ const MainRoutes = {
path: '/ui/config-url',
element: <ProxyManagement />
},
{
path: '/ui/config-url/:routeName',
element: <RouteConfigPage />,
},
]
};

1
go.mod
View file

@ -35,6 +35,7 @@ require (
github.com/docker/docker v23.0.1+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
github.com/evanphx/json-patch v0.5.2 // indirect
github.com/exoscale/egoscale v0.40.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/foomo/simplecert v1.8.4 // indirect

6
go.sum
View file

@ -154,6 +154,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k=
github.com/evanphx/json-patch v0.5.2/go.mod h1:ZWS5hhDbVDyob71nXKNL0+PWn6ToqBHMikGIFbs31qQ=
github.com/exoscale/egoscale v0.23.0/go.mod h1:hRo78jkjkCDKpivQdRBEpNYF5+cVpCJCPDg2/r45KaY=
github.com/exoscale/egoscale v0.40.0 h1:fvVKszvqAXNP1ryhC0rwsKHPenyMaV0fGf14oUMNZFw=
github.com/exoscale/egoscale v0.40.0/go.mod h1:BFi2GNsnsrALev3+gFO/HIQADBQhqJ41S0QrNEB2GJw=
@ -189,6 +191,7 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-openapi/errors v0.19.2/go.mod h1:qX0BLWsyaKfvhluLejVpVNwNRdXZhEbTA4kxxpKBC94=
github.com/go-openapi/strfmt v0.19.8/go.mod h1:qBBipho+3EoIqn6YDI+4RnQEtj6jT/IdKm+PAlXxSUc=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@ -343,6 +346,7 @@ github.com/jarcoal/httpmock v1.0.7/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT
github.com/jasonlvhit/gocron v0.0.1 h1:qTt5qF3b3srDjeOIR4Le1LfeyvoYzJlYpqvG7tJX5YU=
github.com/jasonlvhit/gocron v0.0.1/go.mod h1:k9a3TV8VcU73XZxfVHCHWMWF9SOqgoku0/QlY2yvlA4=
github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
@ -391,6 +395,7 @@ github.com/linode/linodego v0.24.2 h1:wj7DO4/8zGEJm6HtGp03L1HLATzmffkwEoifiKIFi0
github.com/linode/linodego v0.24.2/go.mod h1:GSBKPpjoQfxEfryoCRcgkuUOCuVtGHWhzI8OMdycNTE=
github.com/liquidweb/liquidweb-go v1.6.1 h1:O51RbJo3ZEWFkZFfP32zIF6MCoZzwuuybuXsvZvVEEI=
github.com/liquidweb/liquidweb-go v1.6.1/go.mod h1:UDcVnAMDkZxpw4Y7NOHkqoeiGacVLEIG/i5J9cyixzQ=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
@ -567,6 +572,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.deanishe.net/favicon v0.1.0 h1:Afy941gjRik+DjUUcYHUxcztFEeFse2ITBkMMOlgefM=
go.deanishe.net/favicon v0.1.0/go.mod h1:vIKVI+lUh8k3UAzaN4gjC+cpyatLQWmx0hVX4vLE8jU=
go.mongodb.org/mongo-driver v1.4.2/go.mod h1:WcMNYLx/IlOxLe6JRJiv2uXuCz6zBLndR4SoGjYphSc=

83
src/configapi/patch.go Normal file
View file

@ -0,0 +1,83 @@
package configapi
import (
"encoding/json"
"net/http"
"sync"
"github.com/azukaar/cosmos-server/src/utils"
)
type UpdateRouteRequest struct {
RouteName string `json:"routeName"`
Operation string `json:"operation"`
NewRoute *utils.ProxyRouteConfig `json:"newRoute,omitempty"`
}
var configLock sync.Mutex
func ConfigApiPatch(w http.ResponseWriter, req *http.Request) {
if utils.AdminOnly(w, req) != nil {
return
}
configLock.Lock()
defer configLock.Unlock()
var updateReq UpdateRouteRequest
err := json.NewDecoder(req.Body).Decode(&updateReq)
if err != nil {
utils.Error("SettingsUpdate: Invalid Update Request", err)
utils.HTTPError(w, "Invalid Update Request", http.StatusBadRequest, "UR001")
return
}
config := utils.ReadConfigFromFile()
routes := config.HTTPConfig.ProxyConfig.Routes
routeIndex := -1
for i, route := range routes {
if route.Name == updateReq.RouteName {
routeIndex = i
break
}
}
if routeIndex == -1 {
utils.Error("SettingsUpdate: Route not found: "+updateReq.RouteName, nil)
utils.HTTPError(w, "Route not found", http.StatusNotFound, "UR002")
return
}
switch updateReq.Operation {
case "replace":
if updateReq.NewRoute == nil {
utils.Error("SettingsUpdate: NewRoute must be provided for replace operation", nil)
utils.HTTPError(w, "NewRoute must be provided for replace operation", http.StatusBadRequest, "UR003")
return
}
routes[routeIndex] = *updateReq.NewRoute
case "move_up":
if routeIndex > 0 {
routes[routeIndex-1], routes[routeIndex] = routes[routeIndex], routes[routeIndex-1]
}
case "move_down":
if routeIndex < len(routes)-1 {
routes[routeIndex+1], routes[routeIndex] = routes[routeIndex], routes[routeIndex+1]
}
case "delete":
routes = append(routes[:routeIndex], routes[routeIndex+1:]...)
default:
utils.Error("SettingsUpdate: Unsupported operation: "+updateReq.Operation, nil)
utils.HTTPError(w, "Unsupported operation", http.StatusBadRequest, "UR004")
return
}
config.HTTPConfig.ProxyConfig.Routes = routes
utils.SaveConfigTofile(config)
utils.NeedsRestart = true
json.NewEncoder(w).Encode(map[string]interface{}{
"status": "OK",
})
}

View file

@ -10,6 +10,8 @@ func ConfigRoute(w http.ResponseWriter, req *http.Request) {
ConfigApiGet(w, req)
} else if (req.Method == "PUT") {
ConfigApiSet(w, req)
} else if (req.Method == "PATCH") {
ConfigApiPatch(w, req)
} else {
utils.Error("UserRoute: Method not allowed" + req.Method, nil)
utils.HTTPError(w, "Method not allowed", http.StatusMethodNotAllowed, "HTTP001")

View file

@ -38,13 +38,6 @@ func ConfigApiSet(w http.ResponseWriter, req *http.Request) {
utils.SaveConfigTofile(request)
utils.NeedsRestart = true
// 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",
})

View file

@ -198,7 +198,11 @@ func StartServer() {
srapi.HandleFunc("/api/servapps", docker.ContainersRoute)
srapi.Use(tokenMiddleware)
srapi.Use(utils.CORSHeader(utils.GetMainConfig().HTTPConfig.Hostname))
srapi.Use(proxy.SmartShieldMiddleware(
utils.SmartShieldPolicy{
Enabled: true,
},
))
srapi.Use(utils.MiddlewareTimeout(20 * time.Second))
srapi.Use(httprate.Limit(60, 1*time.Minute,
httprate.WithKeyFuncs(httprate.KeyByIP),