v0.2.0-unstable
new UI
This commit is contained in:
parent
963a1c7699
commit
1ad6edf50a
|
@ -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>
|
||||
|
|
|
@ -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
80
client/src/api/config.ts
Normal 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,
|
||||
};
|
|
@ -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
|
||||
};
|
|
@ -22,3 +22,7 @@ export default function wrap(apicall) {
|
|||
export function setSnackit(snack) {
|
||||
snackit = snack;
|
||||
}
|
||||
|
||||
export {
|
||||
snackit
|
||||
};
|
16
client/src/components/back.jsx
Normal file
16
client/src/components/back.jsx
Normal 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;
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
83
client/src/components/tabbedView/tabbedView.jsx
Normal file
83
client/src/components/tabbedView/tabbedView.jsx
Normal 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;
|
|
@ -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>
|
||||
))}
|
||||
|
|
|
@ -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: '';
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
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>
|
||||
)}
|
||||
let currentRoute = null;
|
||||
if (config) {
|
||||
currentRoute = config.HTTPConfig.ProxyConfig.Routes.find((r) => r.Name === routeName);
|
||||
}
|
||||
|
||||
<CosmosInputText
|
||||
name="Name"
|
||||
label="Name"
|
||||
placeholder="Name"
|
||||
formik={formik}
|
||||
/>
|
||||
const refreshConfig = () => {
|
||||
API.config.get().then((res) => {
|
||||
setConfig(res.data);
|
||||
});
|
||||
};
|
||||
|
||||
<CosmosInputText
|
||||
name="Description"
|
||||
label="Description"
|
||||
placeholder="Description"
|
||||
formik={formik}
|
||||
/>
|
||||
useEffect(() => {
|
||||
refreshConfig();
|
||||
}, []);
|
||||
|
||||
<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'}/>
|
||||
return <div>
|
||||
<h2>
|
||||
<Stack spacing={1}>
|
||||
<Stack direction="row" spacing={1} alignItems="center">
|
||||
<Back />
|
||||
<div>{routeName}</div>
|
||||
</Stack>
|
||||
|
||||
{config && <PrettyTabbedView tabs={[
|
||||
{
|
||||
(formik.values.Mode === "SERVAPP")?
|
||||
<CosmosContainerPicker
|
||||
formik={formik}
|
||||
lockTarget={lockTarget}
|
||||
TargetContainer={TargetContainer}
|
||||
onTargetChange={() => {
|
||||
setRouteConfig(formik.values);
|
||||
}}
|
||||
title: 'Overview',
|
||||
children: <RouteOverview routeConfig={currentRoute} />
|
||||
},
|
||||
{
|
||||
title: 'Setup',
|
||||
children: <RouteManagement
|
||||
title="Setup"
|
||||
submitButton
|
||||
routeConfig={currentRoute}
|
||||
/>
|
||||
: <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}
|
||||
},
|
||||
{
|
||||
title: 'Security',
|
||||
children: <RouteSecurity
|
||||
routeConfig={currentRoute}
|
||||
/>
|
||||
}
|
||||
},
|
||||
{
|
||||
title: 'Permissions',
|
||||
children: <div>WIP</div>
|
||||
},
|
||||
]}/>}
|
||||
|
||||
<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;
|
119
client/src/pages/config/routes/routeSecurity.jsx
Normal file
119
client/src/pages/config/routes/routeSecurity.jsx
Normal 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;
|
221
client/src/pages/config/routes/routeman.jsx
Normal file
221
client/src/pages/config/routes/routeman.jsx
Normal 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;
|
38
client/src/pages/config/routes/routeoverview.jsx
Normal file
38
client/src/pages/config/routes/routeoverview.jsx
Normal 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;
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
checkIsOnline();
|
||||
}, 1500)
|
||||
setTimeout(() => {
|
||||
setWarn(true);
|
||||
}, 8000)
|
||||
}}>Restart</Button>
|
||||
</DialogActions>
|
||||
</DialogActions>}
|
||||
</Dialog>
|
||||
</>;
|
||||
};
|
||||
|
|
|
@ -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}
|
||||
<Chip label={<UpOutlined />} onClick={() => up()}/>
|
||||
<Chip label={<DownOutlined />} onClick={() => down()}/>
|
||||
{!confirmDelete && (<Chip label={<DeleteOutlined />} onClick={() => setConfirmDelete(true)}/>)}
|
||||
{confirmDelete && (<Chip label={<CheckOutlined />} onClick={() => deleteRoute()}/>)}
|
||||
</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;
|
|
@ -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,7 +246,9 @@ const ServeApps = () => {
|
|||
})[app.State]
|
||||
}
|
||||
</Typography>
|
||||
<Stack direction="column" spacing={0} alignItems="flex-start">
|
||||
<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('/', '')}
|
||||
</Typography>
|
||||
|
@ -244,6 +257,7 @@ const ServeApps = () => {
|
|||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Stack margin={1} direction="column" spacing={1} alignItems="flex-start">
|
||||
<Typography variant="h6" color="text.secondary">
|
||||
Ports
|
||||
|
|
|
@ -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
1
go.mod
|
@ -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
6
go.sum
|
@ -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
83
src/configapi/patch.go
Normal 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",
|
||||
})
|
||||
}
|
|
@ -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")
|
||||
|
|
|
@ -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",
|
||||
})
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Reference in a new issue