v0.2.0-unstable4

This commit is contained in:
Yann Stepienik 2023-04-29 12:11:03 +01:00
parent fd568de0d0
commit 9aa2bc48ea
16 changed files with 448 additions and 246 deletions

View file

@ -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,
};

View file

@ -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}
/>

View file

@ -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>
&nbsp;
<Tooltip title={route.AuthEnabled ? "Authentication is enabled" : "Authentication is disabled"}>
<div style={{display: 'inline-block'}}>
{route.AuthEnabled ?

View file

@ -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>

View file

@ -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)}
/>
},
{

View 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;

View file

@ -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)"

View file

@ -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

View file

@ -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} &nbsp;&nbsp;
{!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>

View file

@ -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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;
<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}`;

View file

@ -64,7 +64,7 @@ const RestartModal = ({openModal, setOpenModal}) => {
}, 1500)
setTimeout(() => {
setWarn(true);
}, 8000)
}, 20000)
}}>Restart</Button>
</DialogActions>}
</Dialog>

View file

@ -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>&nbsp;&nbsp;
<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>&nbsp;&nbsp;
<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">
&nbsp;&nbsp;<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()} -&nbsp;
{new Date(row.createdAt).toLocaleTimeString()}
</TableCell>
<TableCell>
{hasLastLogin ? <span>
{new Date(row.lastLogin).toLocaleDateString()} -&nbsp;
{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>)
}
&nbsp;&nbsp;<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>)
}
&nbsp;&nbsp;<Button variant="contained" color="error" onClick={
() => {
setToAction(r.nickname);
setOpenDeleteForm(true);
}
}>Delete</Button></>
}
},
]}
/>)}
</>;
};

View file

@ -10,8 +10,8 @@ import { styled } from '@mui/material/styles';
import * as API from '../../api';
import IsLoggedIn from '../../isLoggedIn';
import RestartModal from '../config/users/restart';
import RouteManagement, { ValidateRoute } from '../config/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/>
})}

View file

@ -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 [];
}

View file

@ -1,6 +1,6 @@
{
"name": "cosmos-server",
"version": "0.2.0-unstable3",
"version": "0.2.0-unstable4",
"description": "",
"main": "test-server.js",
"bugs": {

View file

@ -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)