v0.2.0-unstable4
This commit is contained in:
parent
fd568de0d0
commit
9aa2bc48ea
|
@ -3,7 +3,7 @@ interface Route {
|
|||
Name: string;
|
||||
}
|
||||
|
||||
type Operation = 'replace' | 'move_up' | 'move_down' | 'delete';
|
||||
type Operation = 'replace' | 'move_up' | 'move_down' | 'delete' | 'add';
|
||||
|
||||
function get() {
|
||||
return wrap(fetch('/cosmos/api/config', {
|
||||
|
@ -68,6 +68,10 @@ async function moveRouteDown(routeName: string): Promise<void> {
|
|||
async function deleteRoute(routeName: string): Promise<void> {
|
||||
return rawUpdateRoute(routeName, 'delete');
|
||||
}
|
||||
async function addRoute(newRoute: Route): Promise<void> {
|
||||
return rawUpdateRoute("", 'add', newRoute);
|
||||
}
|
||||
|
||||
export {
|
||||
get,
|
||||
set,
|
||||
|
@ -77,4 +81,5 @@ export {
|
|||
moveRouteUp,
|
||||
moveRouteDown,
|
||||
deleteRoute,
|
||||
addRoute,
|
||||
};
|
|
@ -35,7 +35,7 @@ const HostChip = ({route, settings}) => {
|
|||
window.open(window.location.origin + route.PathPrefix, '_blank');
|
||||
}}
|
||||
onDelete={settings ? () => {
|
||||
window.open('/ui/config-url#'+route.Name, '_blank');
|
||||
window.open('/ui/config-url/'+route.Name, '_blank');
|
||||
} : null}
|
||||
deleteIcon={settings ? <SettingOutlined /> : null}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, SafetyOutlined, UpOutlined } from "@ant-design/icons";
|
||||
import { Card, Chip, Stack, Tooltip } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
|
@ -61,6 +61,15 @@ export const RouteMode = ({route}) => {
|
|||
|
||||
export const RouteSecurity = ({route}) => {
|
||||
return <div style={{fontWeight: 'bold', fontSize: '110%'}}>
|
||||
<Tooltip title={route.SmartShield && route.SmartShield.Enabled ? "Smart Shield is enabled" : "Smart Shield is disabled"}>
|
||||
<div style={{display: 'inline-block'}}>
|
||||
{route.SmartShield && route.SmartShield.Enabled ?
|
||||
<SafetyOutlined style={{color: 'green'}} /> :
|
||||
<SafetyOutlined style={{color: 'red'}} />
|
||||
}
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip title={route.AuthEnabled ? "Authentication is enabled" : "Authentication is disabled"}>
|
||||
<div style={{display: 'inline-block'}}>
|
||||
{route.AuthEnabled ?
|
||||
|
|
|
@ -6,7 +6,7 @@ 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 { Input, InputAdornment, Stack, TextField } from '@mui/material';
|
||||
import { Input, InputAdornment, Stack, TextField, useMediaQuery } from '@mui/material';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
@ -15,6 +15,12 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => {
|
|||
const [search, setSearch] = React.useState('');
|
||||
const theme = useTheme();
|
||||
const isDark = theme.palette.mode === 'dark';
|
||||
const screenMin = {
|
||||
xs: useMediaQuery((theme) => theme.breakpoints.up('xs')),
|
||||
sm: useMediaQuery((theme) => theme.breakpoints.up('sm')),
|
||||
md: useMediaQuery((theme) => theme.breakpoints.up('md')),
|
||||
lg: useMediaQuery((theme) => theme.breakpoints.up('lg')),
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack direction="column" spacing={2}>
|
||||
|
@ -34,11 +40,11 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => {
|
|||
/>
|
||||
|
||||
<TableContainer style={{background: isDark ? '#252b32' : '', borderTop: '3px solid ' + theme.palette.primary.main}} component={Paper}>
|
||||
<Table sx={{ minWidth: 650 }} aria-label="simple table">
|
||||
<Table aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableCell>{column.title}</TableCell>
|
||||
(!column.screenMin || screenMin[column.screenMin]) && <TableCell>{column.title}</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
|
@ -73,15 +79,15 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => {
|
|||
>
|
||||
{columns.map((column) => (
|
||||
|
||||
<TableCell
|
||||
component={(linkTo && !column.clickable) ? Link : 'td'}
|
||||
to={linkTo(row, key)}
|
||||
className={column.underline ? 'emphasis' : ''}
|
||||
sx={{
|
||||
textDecoration: 'none',
|
||||
...column.style,
|
||||
}}>
|
||||
{column.field(row, key)}
|
||||
(!column.screenMin || screenMin[column.screenMin]) && <TableCell
|
||||
component={(linkTo && !column.clickable) ? Link : 'td'}
|
||||
to={linkTo && linkTo(row, key)}
|
||||
className={column.underline ? 'emphasis' : ''}
|
||||
sx={{
|
||||
textDecoration: 'none',
|
||||
...column.style,
|
||||
}}>
|
||||
{column.field(row, key)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { useParams } from "react-router";
|
||||
import Back from "../../components/back";
|
||||
import { CircularProgress, Stack } from "@mui/material";
|
||||
import { Alert, CircularProgress, Stack } from "@mui/material";
|
||||
import PrettyTabbedView from "../../components/tabbedView/tabbedView";
|
||||
import RouteManagement from "./routes/routeman";
|
||||
import { useEffect, useState } from "react";
|
||||
|
@ -35,7 +35,11 @@ const RouteConfigPage = () => {
|
|||
<div>{routeName}</div>
|
||||
</Stack>
|
||||
|
||||
{config && <PrettyTabbedView tabs={[
|
||||
{config && !currentRoute && <div>
|
||||
<Alert severity="error">Route not found</Alert>
|
||||
</div>}
|
||||
|
||||
{config && currentRoute && <PrettyTabbedView tabs={[
|
||||
{
|
||||
title: 'Overview',
|
||||
children: <RouteOverview routeConfig={currentRoute} />
|
||||
|
@ -46,6 +50,7 @@ const RouteConfigPage = () => {
|
|||
title="Setup"
|
||||
submitButton
|
||||
routeConfig={currentRoute}
|
||||
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
||||
/>
|
||||
},
|
||||
{
|
||||
|
|
94
client/src/pages/config/routes/newRoute.jsx
Normal file
94
client/src/pages/config/routes/newRoute.jsx
Normal file
|
@ -0,0 +1,94 @@
|
|||
// material-ui
|
||||
import { AppstoreAddOutlined, PlusCircleOutlined, ReloadOutlined, SearchOutlined, SettingOutlined } from '@ant-design/icons';
|
||||
import { Alert, Badge, Button, Card, Checkbox, Chip, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Divider, Input, InputAdornment, TextField, Tooltip, Typography } from '@mui/material';
|
||||
import Grid2 from '@mui/material/Unstable_Grid2/Grid2';
|
||||
import { Stack } from '@mui/system';
|
||||
import { useEffect, useState } from 'react';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { styled } from '@mui/material/styles';
|
||||
|
||||
import * as API from '../../../api';
|
||||
import IsLoggedIn from '../../../isLoggedIn';
|
||||
import RestartModal from '../../config/users/restart';
|
||||
import RouteManagement from '../../config/routes/routeman';
|
||||
import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../../utils/routes';
|
||||
import HostChip from '../../../components/hostChip';
|
||||
|
||||
|
||||
const NewRouteCreate = ({ openNewModal, setOpenNewModal, config }) => {
|
||||
const [openRestartModal, setOpenRestartModal] = useState(false);
|
||||
const [submitErrors, setSubmitErrors] = useState([]);
|
||||
const [newRoute, setNewRoute] = useState(null);
|
||||
|
||||
function addRoute() {
|
||||
return API.config.addRoute(newRoute).then((res) => {
|
||||
setOpenNewModal(false);
|
||||
setOpenRestartModal(true);
|
||||
});
|
||||
}
|
||||
|
||||
return <>
|
||||
<RestartModal openModal={openRestartModal} setOpenModal={setOpenRestartModal} />
|
||||
<Dialog open={openNewModal} onClose={() => setOpenNewModal(false)}>
|
||||
<DialogTitle>New URL</DialogTitle>
|
||||
{openNewModal && <>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
<Stack spacing={2}>
|
||||
<div>
|
||||
<RouteManagement
|
||||
routeConfig={{
|
||||
Target: "",
|
||||
Mode: "SERVAPP",
|
||||
Name: "New Route",
|
||||
Description: "New Route",
|
||||
UseHost: false,
|
||||
Host: "",
|
||||
UsePathPrefix: false,
|
||||
PathPrefix: '',
|
||||
CORSOrigin: '',
|
||||
StripPathPrefix: false,
|
||||
AuthEnabled: false,
|
||||
Timeout: 14400000,
|
||||
ThrottlePerMinute: 10000,
|
||||
SmartShield: {
|
||||
Enabled: true,
|
||||
}
|
||||
}}
|
||||
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
||||
setRouteConfig={(_newRoute) => {
|
||||
setNewRoute(sanitizeRoute(_newRoute));
|
||||
}}
|
||||
up={() => {}}
|
||||
down={() => {}}
|
||||
deleteRoute={() => {}}
|
||||
noControls
|
||||
/>
|
||||
</div>
|
||||
</Stack>
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
{submitErrors && submitErrors.length > 0 && <Stack spacing={2} direction={"column"}>
|
||||
<Alert severity="error">{submitErrors.map((err) => {
|
||||
return <div>{err}</div>
|
||||
})}</Alert>
|
||||
</Stack>}
|
||||
<Button onClick={() => setOpenNewModal(false)}>Cancel</Button>
|
||||
<Button onClick={() => {
|
||||
let errors = ValidateRoute(newRoute, config);
|
||||
if (errors && errors.length > 0) {
|
||||
setSubmitErrors(errors);
|
||||
} else {
|
||||
setSubmitErrors([]);
|
||||
addRoute();
|
||||
}
|
||||
|
||||
}}>Confirm</Button>
|
||||
</DialogActions>
|
||||
</>}
|
||||
</Dialog>
|
||||
</>;
|
||||
}
|
||||
|
||||
export default NewRouteCreate;
|
|
@ -23,20 +23,24 @@ const RouteSecurity = ({ 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,
|
||||
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
|
||||
}}
|
||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
const fullValues = {
|
||||
...routeConfig,
|
||||
...values,
|
||||
}
|
||||
|
||||
if(!fullValues.SmartShield) {
|
||||
fullValues.SmartShield = {};
|
||||
}
|
||||
fullValues.SmartShield.Enabled = values._SmartShield_Enabled;
|
||||
delete fullValues._SmartShield_Enabled;
|
||||
|
||||
API.config.replaceRoute(routeConfig.Name, fullValues).then((res) => {
|
||||
if (res.status == "OK") {
|
||||
setStatus({ success: true });
|
||||
|
@ -66,6 +70,12 @@ const RouteSecurity = ({ routeConfig }) => {
|
|||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosCheckbox
|
||||
name="_SmartShield_Enabled"
|
||||
label="Smart Shield Protection"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosInputText
|
||||
name="Timeout"
|
||||
label="Timeout in milliseconds (0 for no timeout, at least 30000 or less recommended)"
|
||||
|
|
|
@ -14,32 +14,7 @@ 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')
|
||||
}),
|
||||
})
|
||||
import { ValidateRouteSchema, sanitizeRoute } from '../../../utils/routes';
|
||||
|
||||
const Hide = ({ children, h }) => {
|
||||
return h ? <div style={{ display: 'none' }}>
|
||||
|
@ -47,12 +22,21 @@ const Hide = ({ children, h }) => {
|
|||
</div> : <>{children}</>
|
||||
}
|
||||
|
||||
const RouteManagement = ({ routeConfig, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false }) => {
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
const debounce = (func, wait) => {
|
||||
let timeout;
|
||||
return function (...args) {
|
||||
const context = this;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||
};
|
||||
};
|
||||
|
||||
const RouteManagement = ({ routeConfig, routeNames, TargetContainer, noControls = false, lockTarget = false, title, setRouteConfig, submitButton = false, newRoute }) => {
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
|
||||
return <div style={{ maxWidth: '1000px', width: '100%', margin: '', position: 'relative' }}>
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||
|
||||
|
||||
{routeConfig && <>
|
||||
<Formik
|
||||
initialValues={{
|
||||
|
@ -62,17 +46,32 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls = false, loc
|
|||
Target: routeConfig.Target,
|
||||
UseHost: routeConfig.UseHost,
|
||||
Host: routeConfig.Host,
|
||||
UsePathPrefix: routeConfig.UsePathPrefix,
|
||||
PathPrefix: routeConfig.PathPrefix,
|
||||
StripPathPrefix: routeConfig.StripPathPrefix,
|
||||
AuthEnabled: routeConfig.AuthEnabled,
|
||||
_SmartShield_Enabled: (routeConfig.SmartShield ? routeConfig.SmartShield.Enabled : false),
|
||||
}}
|
||||
validationSchema={ValidateRoute}
|
||||
validationSchema={ValidateRouteSchema}
|
||||
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
if(!submitButton) {
|
||||
return false;
|
||||
} else {
|
||||
const fullValues = {
|
||||
let fullValues = {
|
||||
...routeConfig,
|
||||
...values,
|
||||
}
|
||||
API.config.replaceRoute(routeConfig.Name, fullValues).then((res) => {
|
||||
|
||||
fullValues = sanitizeRoute(fullValues);
|
||||
|
||||
let op;
|
||||
if(newRoute) {
|
||||
op = API.config.newRoute(routeConfig.Name, fullValues)
|
||||
} else {
|
||||
op = API.config.replaceRoute(routeConfig.Name, fullValues)
|
||||
}
|
||||
|
||||
op.then((res) => {
|
||||
if (res.status == "OK") {
|
||||
setStatus({ success: true });
|
||||
snackit('Route updated successfully', 'success')
|
||||
|
@ -87,7 +86,17 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls = false, loc
|
|||
}
|
||||
}}
|
||||
validate={(values) => {
|
||||
setRouteConfig && setRouteConfig(values);
|
||||
let fullValues = {
|
||||
...routeConfig,
|
||||
...values,
|
||||
}
|
||||
|
||||
// check name is unique
|
||||
if (newRoute && routeNames.includes(fullValues.Name)) {
|
||||
return { Name: 'Name must be unique' }
|
||||
}
|
||||
|
||||
setRouteConfig && debounce(() => setRouteConfig(fullValues), 500)();
|
||||
}}
|
||||
>
|
||||
{(formik) => (
|
||||
|
@ -198,6 +207,20 @@ const RouteManagement = ({ routeConfig, TargetContainer, noControls = false, loc
|
|||
formik={formik}
|
||||
style={{ paddingLeft: '20px' }}
|
||||
/>}
|
||||
|
||||
<CosmosFormDivider title={'Basic Security'} />
|
||||
|
||||
<CosmosCheckbox
|
||||
name="AuthEnabled"
|
||||
label="Authentication Required"
|
||||
formik={formik}
|
||||
/>
|
||||
|
||||
<CosmosCheckbox
|
||||
name="_SmartShield_Enabled"
|
||||
label="Smart Shield Protection"
|
||||
formik={formik}
|
||||
/>
|
||||
</Grid>
|
||||
</MainCard>
|
||||
{submitButton && <MainCard ><Button
|
||||
|
|
|
@ -1,25 +1,39 @@
|
|||
import * as React from 'react';
|
||||
import MainCard from '../../../components/MainCard';
|
||||
import RestartModal from '../users/restart';
|
||||
import { Chip, Stack, useMediaQuery } from '@mui/material';
|
||||
import { Chip, Divider, Stack, useMediaQuery } from '@mui/material';
|
||||
import HostChip from '../../../components/hostChip';
|
||||
import { RouteMode, RouteSecurity } from '../../../components/routeComponents';
|
||||
import { getFaviconURL } from '../../../utils/routes';
|
||||
import * as API from '../../../api';
|
||||
import { CheckOutlined, ClockCircleOutlined, DashboardOutlined, DeleteOutlined, DownOutlined, LockOutlined, UpOutlined } from "@ant-design/icons";
|
||||
|
||||
const RouteOverview = ({ routeConfig }) => {
|
||||
const [openModal, setOpenModal] = React.useState(false);
|
||||
const isMobile = useMediaQuery((theme) => theme.breakpoints.down('sm'));
|
||||
const [confirmDelete, setConfirmDelete] = React.useState(false);
|
||||
|
||||
function deleteRoute(event) {
|
||||
event.stopPropagation();
|
||||
API.config.deleteRoute(routeConfig.Name).then(() => {
|
||||
setOpenModal(true);
|
||||
});
|
||||
}
|
||||
|
||||
return <div style={{ maxWidth: '1000px', width: '100%'}}>
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||
|
||||
{routeConfig && <>
|
||||
<MainCard name={routeConfig.Name} title={routeConfig.Name}>
|
||||
<MainCard name={routeConfig.Name} title={<div>
|
||||
{routeConfig.Name}
|
||||
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
|
||||
{confirmDelete && (<Chip label={<CheckOutlined />} color="error" onClick={(event) => deleteRoute(event)}/>)}
|
||||
</div>}>
|
||||
<Stack spacing={2} direction={isMobile ? 'column' : 'row'} alignItems={isMobile ? 'center' : 'flex-start'}>
|
||||
<div>
|
||||
<img src={getFaviconURL(routeConfig)} width="128px" />
|
||||
</div>
|
||||
<Stack spacing={2}>
|
||||
<Stack spacing={2} >
|
||||
<strong>Description</strong>
|
||||
<div>{routeConfig.Description}</div>
|
||||
<strong>URL</strong>
|
||||
|
|
|
@ -31,13 +31,14 @@ import {
|
|||
import { EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
|
||||
import AnimateButton from '../../../components/@extended/AnimateButton';
|
||||
import RestartModal from './restart';
|
||||
import RouteManagement, {ValidateRoute} from '../routes/routeman';
|
||||
import RouteManagement from '../routes/routeman';
|
||||
import { map } from 'lodash';
|
||||
import { getFaviconURL, sanitizeRoute } from '../../../utils/routes';
|
||||
import { getFaviconURL, sanitizeRoute, ValidateRoute } 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';
|
||||
import NewRouteCreate from '../routes/newRoute';
|
||||
|
||||
const stickyButton = {
|
||||
position: 'fixed',
|
||||
|
@ -62,7 +63,7 @@ const ProxyManagement = () => {
|
|||
const [error, setError] = React.useState(null);
|
||||
const [submitErrors, setSubmitErrors] = React.useState([]);
|
||||
const [needSave, setNeedSave] = React.useState(false);
|
||||
const navigate = useNavigate();
|
||||
const [openNewModal, setOpenNewModal] = React.useState(false);
|
||||
|
||||
function updateRoutes(routes) {
|
||||
let con = {
|
||||
|
@ -129,43 +130,22 @@ const ProxyManagement = () => {
|
|||
refresh();
|
||||
}, []);
|
||||
|
||||
const testRoute = (route) => {
|
||||
try {
|
||||
ValidateRoute.validateSync(route);
|
||||
} catch (e) {
|
||||
return e.errors;
|
||||
}
|
||||
}
|
||||
let routes = config && (config.HTTPConfig.ProxyConfig.Routes || []);
|
||||
|
||||
return <div style={{ }}>
|
||||
<IsLoggedIn />
|
||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
||||
refresh();
|
||||
}}>Refresh</Button>
|
||||
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
|
||||
routes.unshift({
|
||||
Name: 'New URL',
|
||||
Description: 'New URL',
|
||||
Mode: "SERVAPP",
|
||||
UseHost: false,
|
||||
Host: '',
|
||||
UsePathPrefix: false,
|
||||
PathPrefix: '',
|
||||
Timeout: 30000,
|
||||
ThrottlePerMinute: 0,
|
||||
CORSOrigin: '',
|
||||
StripPathPrefix: false,
|
||||
AuthEnabled: false,
|
||||
});
|
||||
updateRoutes(routes);
|
||||
setNeedSave(true);
|
||||
}}>Create</Button>
|
||||
|
||||
<br /><br />
|
||||
|
||||
<Stack direction="row" spacing={1} style={{ marginBottom: '20px' }}>
|
||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
||||
refresh();
|
||||
}}>Refresh</Button>
|
||||
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
|
||||
setOpenNewModal(true);
|
||||
}}>Create</Button>
|
||||
</Stack>
|
||||
|
||||
{config && <>
|
||||
<RestartModal openModal={openModal} setOpenModal={setOpenModal} />
|
||||
<NewRouteCreate openNewModal={openNewModal} setOpenNewModal={setOpenNewModal} config={config}/>
|
||||
|
||||
{routes && <PrettyTableView
|
||||
data={routes}
|
||||
|
@ -190,9 +170,9 @@ const ProxyManagement = () => {
|
|||
<div style={{display:'inline-block', textDecoration: 'inherit', fontSize: '90%', opacity: '90%'}}>{r.Description}</div>
|
||||
</>
|
||||
},
|
||||
{ title: 'Origin', clickable:true, search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => <HostChip route={r} /> },
|
||||
{ title: 'Target', search: (r) => r.Target, field: (r) => <><RouteMode route={r} /> <Chip label={r.Target} /></> },
|
||||
{ title: 'Security', field: (r) => <RouteSecurity route={r} />,
|
||||
{ title: 'Origin', screenMin: 'md', clickable:true, search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => <HostChip route={r} /> },
|
||||
{ title: 'Target', screenMin: 'md', search: (r) => r.Target, field: (r) => <><RouteMode route={r} /> <Chip label={r.Target} /></> },
|
||||
{ title: 'Security', screenMin: 'lg', field: (r) => <RouteSecurity route={r} />,
|
||||
style: {minWidth: '70px'} },
|
||||
{ title: '', clickable:true, field: (r, k) => <RouteActions
|
||||
route={r}
|
||||
|
@ -213,19 +193,6 @@ const ProxyManagement = () => {
|
|||
</div>
|
||||
}
|
||||
|
||||
{/* {routes && routes.map((route,key) => (<>
|
||||
<RouteManagement key={route.Name} routeConfig={route}
|
||||
setRouteConfig={(newRoute) => {
|
||||
routes[key] = sanitizeRoute(newRoute);
|
||||
setNeedSave(true);
|
||||
}}
|
||||
up={() => up(key)}
|
||||
down={() => down(key)}
|
||||
deleteRoute={() => deleteRoute(key)}
|
||||
/>
|
||||
<br /><br />
|
||||
</>))} */}
|
||||
|
||||
{routes && needSave && <>
|
||||
<div>
|
||||
<br /><br /><br /><br />
|
||||
|
@ -249,7 +216,7 @@ const ProxyManagement = () => {
|
|||
fullWidth
|
||||
onClick={() => {
|
||||
if(routes.some((route, key) => {
|
||||
let errors = testRoute(route);
|
||||
let errors = ValidateRoute(route, config);
|
||||
if (errors && errors.length > 0) {
|
||||
errors = errors.map((err) => {
|
||||
return `${route.Name}: ${err}`;
|
||||
|
|
|
@ -64,7 +64,7 @@ const RestartModal = ({openModal, setOpenModal}) => {
|
|||
}, 1500)
|
||||
setTimeout(() => {
|
||||
setWarn(true);
|
||||
}, 8000)
|
||||
}, 20000)
|
||||
}}>Restart</Button>
|
||||
</DialogActions>}
|
||||
</Dialog>
|
||||
|
|
|
@ -22,6 +22,7 @@ import * as API from '../../../api';
|
|||
import MainCard from '../../../components/MainCard';
|
||||
import IsLoggedIn from '../../../isLoggedIn';
|
||||
import { useEffect, useState } from 'react';
|
||||
import PrettyTableView from '../../../components/tableView/prettyTableView';
|
||||
|
||||
const UserManagement = () => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
@ -32,7 +33,7 @@ const UserManagement = () => {
|
|||
|
||||
const roles = ['Guest', 'User', 'Admin']
|
||||
|
||||
const [rows, setRows] = useState([]);
|
||||
const [rows, setRows] = useState(null);
|
||||
|
||||
function refresh() {
|
||||
setIsLoading(true);
|
||||
|
@ -143,97 +144,99 @@ const UserManagement = () => {
|
|||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<MainCard title="Users">
|
||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
||||
|
||||
<Button variant="contained" color="primary" startIcon={<SyncOutlined />} onClick={() => {
|
||||
refresh();
|
||||
}}>Refresh</Button>
|
||||
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
|
||||
setOpenCreateForm(true)
|
||||
}}>Create</Button><br /><br />
|
||||
{isLoading ? <center><br /><CircularProgress color="inherit" /></center>
|
||||
: <TableContainer component={Paper}>
|
||||
<Table aria-label="simple table">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Nickname</TableCell>
|
||||
<TableCell>Status</TableCell>
|
||||
<TableCell>Created At</TableCell>
|
||||
<TableCell>Last Login</TableCell>
|
||||
<TableCell>Actions</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row) => {
|
||||
const isRegistered = new Date(row.registeredAt).getTime() > 0;
|
||||
const inviteExpired = new Date(row.registerKeyExp).getTime() < new Date().getTime();
|
||||
}}>Refresh</Button>
|
||||
<Button variant="contained" color="primary" startIcon={<PlusCircleOutlined />} onClick={() => {
|
||||
setOpenCreateForm(true)
|
||||
}}>Create</Button><br /><br />
|
||||
|
||||
const hasLastLogin = new Date(row.lastLogin).getTime() > 0;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={row.nickname}
|
||||
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
|
||||
>
|
||||
<TableCell component="th" scope="row">
|
||||
<strong>{row.nickname}</strong>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isRegistered ? (row.role > 1 ? <Chip
|
||||
icon={<KeyOutlined />}
|
||||
label="Admin"
|
||||
variant="outlined"
|
||||
/> : <Chip
|
||||
icon={<UserOutlined />}
|
||||
label="User"
|
||||
variant="outlined"
|
||||
/>) : (
|
||||
inviteExpired ? <Chip
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
label="Invite Expired"
|
||||
color="error"
|
||||
/> : <Chip
|
||||
icon={<WarningOutlined />}
|
||||
label="Invite Pending"
|
||||
color="warning"
|
||||
/>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{new Date(row.createdAt).toLocaleDateString()} -
|
||||
{new Date(row.createdAt).toLocaleTimeString()}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{hasLastLogin ? <span>
|
||||
{new Date(row.lastLogin).toLocaleDateString()} -
|
||||
{new Date(row.lastLogin).toLocaleTimeString()}
|
||||
</span> : '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{isRegistered ?
|
||||
(<Button variant="contained" color="primary" onClick={
|
||||
() => {
|
||||
sendlink(row.nickname, 1);
|
||||
}
|
||||
}>Send password reset</Button>) :
|
||||
(<Button variant="contained" className={inviteExpired ? 'shinyButton' : ''} onClick={
|
||||
() => {
|
||||
sendlink(row.nickname, 2);
|
||||
}
|
||||
} color="primary">Re-Send Invite</Button>)
|
||||
}
|
||||
<Button variant="contained" color="error" onClick={
|
||||
() => {
|
||||
setToAction(row.nickname);
|
||||
setOpenDeleteForm(true);
|
||||
}
|
||||
}>Delete</Button></TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>}
|
||||
</MainCard>
|
||||
{isLoading && <center><br /><CircularProgress /></center>}
|
||||
|
||||
{!isLoading && rows && (<PrettyTableView
|
||||
data={rows}
|
||||
getKey={(r) => r.nickname}
|
||||
columns={[
|
||||
{
|
||||
title: 'User',
|
||||
// underline: true,
|
||||
field: (r) => <strong>{r.nickname}</strong>,
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
screenMin: 'sm',
|
||||
field: (r) => {
|
||||
const isRegistered = new Date(r.registeredAt).getTime() > 0;
|
||||
const inviteExpired = new Date(r.registerKeyExp).getTime() < new Date().getTime();
|
||||
|
||||
return <>{isRegistered ? (r.role > 1 ? <Chip
|
||||
icon={<KeyOutlined />}
|
||||
label="Admin"
|
||||
/> : <Chip
|
||||
icon={<UserOutlined />}
|
||||
label="User"
|
||||
/>) : (
|
||||
inviteExpired ? <Chip
|
||||
icon={<ExclamationCircleOutlined />}
|
||||
label="Invite Expired"
|
||||
color="error"
|
||||
/> : <Chip
|
||||
icon={<WarningOutlined />}
|
||||
label="Invite Pending"
|
||||
color="warning"
|
||||
/>
|
||||
)}</>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Email',
|
||||
screenMin: 'md',
|
||||
field: (r) => r.email,
|
||||
},
|
||||
{
|
||||
title: 'Created At',
|
||||
screenMin: 'lg',
|
||||
field: (r) => new Date(r.createdAt).toLocaleString(),
|
||||
},
|
||||
{
|
||||
title: 'Last Login',
|
||||
screenMin: 'lg',
|
||||
field: (r) => {
|
||||
const hasLastLogin = new Date(r.lastLogin).getTime() > 0;
|
||||
return <>{hasLastLogin ? new Date(r.lastLogin).toLocaleString() : 'Never'}</>
|
||||
},
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
clickable: true,
|
||||
field: (r) => {
|
||||
const isRegistered = new Date(r.registeredAt).getTime() > 0;
|
||||
const inviteExpired = new Date(r.registerKeyExp).getTime() < new Date().getTime();
|
||||
|
||||
return <>{isRegistered ?
|
||||
(<Button variant="contained" color="primary" onClick={
|
||||
() => {
|
||||
sendlink(r.nickname, 1);
|
||||
}
|
||||
}>Send password reset</Button>) :
|
||||
(<Button variant="contained" className={inviteExpired ? 'shinyButton' : ''} onClick={
|
||||
() => {
|
||||
sendlink(r.nickname, 2);
|
||||
}
|
||||
} color="primary">Re-Send Invite</Button>)
|
||||
}
|
||||
<Button variant="contained" color="error" onClick={
|
||||
() => {
|
||||
setToAction(r.nickname);
|
||||
setOpenDeleteForm(true);
|
||||
}
|
||||
}>Delete</Button></>
|
||||
}
|
||||
},
|
||||
]}
|
||||
/>)}
|
||||
</>;
|
||||
};
|
||||
|
||||
|
|
|
@ -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/routes/routeman';
|
||||
import { getFaviconURL, sanitizeRoute } from '../../utils/routes';
|
||||
import RouteManagement from '../config/routes/routeman';
|
||||
import { ValidateRoute, getFaviconURL, sanitizeRoute } from '../../utils/routes';
|
||||
import HostChip from '../../components/hostChip';
|
||||
|
||||
const Item = styled(Paper)(({ theme }) => ({
|
||||
|
@ -49,14 +49,6 @@ const ServeApps = () => {
|
|||
})
|
||||
}
|
||||
|
||||
const testRoute = (route) => {
|
||||
try {
|
||||
ValidateRoute.validateSync(route);
|
||||
} catch (e) {
|
||||
return e.errors;
|
||||
}
|
||||
}
|
||||
|
||||
const refreshServeApps = () => {
|
||||
API.docker.list().then((res) => {
|
||||
setServeApps(res.data);
|
||||
|
@ -97,8 +89,8 @@ const ServeApps = () => {
|
|||
ProxyConfig: {
|
||||
...config.HTTPConfig.ProxyConfig,
|
||||
Routes: [
|
||||
...config.HTTPConfig.ProxyConfig.Routes,
|
||||
newRoute,
|
||||
...config.HTTPConfig.ProxyConfig.Routes,
|
||||
]
|
||||
},
|
||||
},
|
||||
|
@ -109,6 +101,7 @@ const ServeApps = () => {
|
|||
setOpenRestartModal(true);
|
||||
});
|
||||
}
|
||||
|
||||
const gridAnim = {
|
||||
transition: 'all 0.2s ease',
|
||||
opacity: 1,
|
||||
|
@ -125,7 +118,6 @@ const ServeApps = () => {
|
|||
|
||||
const getFirstRouteFavIcon = (app) => {
|
||||
let routes = getContainersRoutes(app.Names[0].replace('/', ''));
|
||||
console.log(routes)
|
||||
if(routes.length > 0) {
|
||||
let url = getFaviconURL(routes[0]);
|
||||
return url;
|
||||
|
@ -160,12 +152,16 @@ const ServeApps = () => {
|
|||
Host: getHostnameFromName(openModal.Names[0]),
|
||||
UsePathPrefix: false,
|
||||
PathPrefix: '',
|
||||
Timeout: 30000,
|
||||
ThrottlePerMinute: 0,
|
||||
CORSOrigin: '',
|
||||
StripPathPrefix: false,
|
||||
AuthEnabled: false,
|
||||
Timeout: 14400000,
|
||||
ThrottlePerMinute: 10000,
|
||||
SmartShield: {
|
||||
Enabled: true,
|
||||
}
|
||||
}}
|
||||
routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)}
|
||||
setRouteConfig={(_newRoute) => {
|
||||
setNewRoute(sanitizeRoute(_newRoute));
|
||||
}}
|
||||
|
@ -187,7 +183,7 @@ const ServeApps = () => {
|
|||
</Stack>}
|
||||
<Button onClick={() => setOpenModal(false)}>Cancel</Button>
|
||||
<Button onClick={() => {
|
||||
let errors = testRoute(newRoute);
|
||||
let errors = ValidateRoute(newRoute, config);
|
||||
if (errors && errors.length > 0) {
|
||||
errors = errors.map((err) => {
|
||||
return `${err}`;
|
||||
|
@ -305,7 +301,7 @@ const ServeApps = () => {
|
|||
<Typography variant="h6" color="text.secondary">
|
||||
URLs
|
||||
</Typography>
|
||||
<Stack spacing={2} direction="row">
|
||||
<Stack style={noOver} spacing={2} direction="row">
|
||||
{getContainersRoutes(app.Names[0].replace('/', '')).map((route) => {
|
||||
return <HostChip route={route} settings/>
|
||||
})}
|
||||
|
|
|
@ -1,12 +1,27 @@
|
|||
import Folder from '../assets/images/icons/folder(1).svg';
|
||||
import * as Yup from 'yup';
|
||||
|
||||
export const sanitizeRoute = (_route) => {
|
||||
let route = {..._route};
|
||||
|
||||
export const sanitizeRoute = (route) => {
|
||||
if (!route.UseHost) {
|
||||
route.Host = "";
|
||||
}
|
||||
if (!route.UsePathPrefix) {
|
||||
route.PathPrefix = "";
|
||||
}
|
||||
|
||||
route.Name = route.Name.trim();
|
||||
|
||||
if(!route.SmartShield) {
|
||||
route.SmartShield = {};
|
||||
}
|
||||
|
||||
if(typeof route._SmartShield_Enabled !== "undefined") {
|
||||
route.SmartShield.Enabled = route._SmartShield_Enabled;
|
||||
delete route._SmartShield_Enabled;
|
||||
}
|
||||
|
||||
return route;
|
||||
}
|
||||
|
||||
|
@ -36,4 +51,44 @@ export const getFaviconURL = (route) => {
|
|||
} else {
|
||||
return addRemote(addProtocol(getOrigin(route)));
|
||||
}
|
||||
}
|
||||
|
||||
export const ValidateRouteSchema = 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')
|
||||
}),
|
||||
})
|
||||
|
||||
export const ValidateRoute = (routeConfig, config) => {
|
||||
let routeNames= config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name);
|
||||
|
||||
try {
|
||||
ValidateRouteSchema.validateSync(routeConfig);
|
||||
} catch (e) {
|
||||
return e.errors;
|
||||
}
|
||||
if (routeNames.includes(routeConfig.Name)) {
|
||||
return ['Route Name already exists. Name must be unique.'];
|
||||
}
|
||||
return [];
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "cosmos-server",
|
||||
"version": "0.2.0-unstable3",
|
||||
"version": "0.2.0-unstable4",
|
||||
"description": "",
|
||||
"main": "test-server.js",
|
||||
"bugs": {
|
||||
|
|
|
@ -36,42 +36,57 @@ func ConfigApiPatch(w http.ResponseWriter, req *http.Request) {
|
|||
routes := config.HTTPConfig.ProxyConfig.Routes
|
||||
routeIndex := -1
|
||||
|
||||
for i, route := range routes {
|
||||
if route.Name == updateReq.RouteName {
|
||||
routeIndex = i
|
||||
break
|
||||
if updateReq.Operation != "add" {
|
||||
if updateReq.RouteName == "" {
|
||||
utils.Error("SettingsUpdate: RouteName must be provided", nil)
|
||||
utils.HTTPError(w, "RouteName must be provided", http.StatusBadRequest, "UR002")
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
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:]...)
|
||||
case "add":
|
||||
if updateReq.NewRoute == nil {
|
||||
utils.Error("SettingsUpdate: NewRoute must be provided for add operation", nil)
|
||||
utils.HTTPError(w, "NewRoute must be provided for add operation", http.StatusBadRequest, "UR003")
|
||||
return
|
||||
}
|
||||
routes = append([]utils.ProxyRouteConfig{*updateReq.NewRoute}, routes...)
|
||||
default:
|
||||
utils.Error("SettingsUpdate: Unsupported operation: "+updateReq.Operation, nil)
|
||||
utils.HTTPError(w, "Unsupported operation", http.StatusBadRequest, "UR004")
|
||||
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)
|
||||
|
|
Loading…
Reference in a new issue