From 9aa2bc48eaf0170de1dd99eac3a7247f67c61f6a Mon Sep 17 00:00:00 2001 From: Yann Stepienik Date: Sat, 29 Apr 2023 12:11:03 +0100 Subject: [PATCH] v0.2.0-unstable4 --- client/src/api/config.ts | 7 +- client/src/components/hostChip.jsx | 2 +- client/src/components/routeComponents.jsx | 11 +- .../components/tableView/prettyTableView.jsx | 30 +-- client/src/pages/config/routeConfigPage.jsx | 9 +- client/src/pages/config/routes/newRoute.jsx | 94 +++++++++ .../src/pages/config/routes/routeSecurity.jsx | 18 +- client/src/pages/config/routes/routeman.jsx | 89 +++++---- .../src/pages/config/routes/routeoverview.jsx | 20 +- client/src/pages/config/users/proxyman.jsx | 69 ++----- client/src/pages/config/users/restart.jsx | 2 +- .../src/pages/config/users/usermanagement.jsx | 181 +++++++++--------- client/src/pages/servapps/servapps.jsx | 28 ++- client/src/utils/routes.jsx | 57 +++++- package.json | 2 +- src/configapi/patch.go | 75 +++++--- 16 files changed, 448 insertions(+), 246 deletions(-) create mode 100644 client/src/pages/config/routes/newRoute.jsx diff --git a/client/src/api/config.ts b/client/src/api/config.ts index 178f5c4..5010406 100644 --- a/client/src/api/config.ts +++ b/client/src/api/config.ts @@ -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 { async function deleteRoute(routeName: string): Promise { return rawUpdateRoute(routeName, 'delete'); } +async function addRoute(newRoute: Route): Promise { + return rawUpdateRoute("", 'add', newRoute); +} + export { get, set, @@ -77,4 +81,5 @@ export { moveRouteUp, moveRouteDown, deleteRoute, + addRoute, }; \ No newline at end of file diff --git a/client/src/components/hostChip.jsx b/client/src/components/hostChip.jsx index 4f60702..6b6ad43 100644 --- a/client/src/components/hostChip.jsx +++ b/client/src/components/hostChip.jsx @@ -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 ? : null} /> diff --git a/client/src/components/routeComponents.jsx b/client/src/components/routeComponents.jsx index f686bc0..1e1afea 100644 --- a/client/src/components/routeComponents.jsx +++ b/client/src/components/routeComponents.jsx @@ -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
+ +
+ {route.SmartShield && route.SmartShield.Enabled ? + : + + } +
+
+  
{route.AuthEnabled ? diff --git a/client/src/components/tableView/prettyTableView.jsx b/client/src/components/tableView/prettyTableView.jsx index 6c7dc0a..ccbbd2b 100644 --- a/client/src/components/tableView/prettyTableView.jsx +++ b/client/src/components/tableView/prettyTableView.jsx @@ -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 ( @@ -34,11 +40,11 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => { /> - +
{columns.map((column) => ( - {column.title} + (!column.screenMin || screenMin[column.screenMin]) && {column.title} ))} @@ -73,15 +79,15 @@ const PrettyTableView = ({ getKey, data, columns, onRowClick, linkTo }) => { > {columns.map((column) => ( - - {column.field(row, key)} + (!column.screenMin || screenMin[column.screenMin]) && + {column.field(row, key)} ))} diff --git a/client/src/pages/config/routeConfigPage.jsx b/client/src/pages/config/routeConfigPage.jsx index 79595fd..26d1b03 100644 --- a/client/src/pages/config/routeConfigPage.jsx +++ b/client/src/pages/config/routeConfigPage.jsx @@ -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 = () => {
{routeName}
- {config && + Route not found + } + + {config && currentRoute && @@ -46,6 +50,7 @@ const RouteConfigPage = () => { title="Setup" submitButton routeConfig={currentRoute} + routeNames={config.HTTPConfig.ProxyConfig.Routes.map((r) => r.Name)} /> }, { diff --git a/client/src/pages/config/routes/newRoute.jsx b/client/src/pages/config/routes/newRoute.jsx new file mode 100644 index 0000000..da12e3e --- /dev/null +++ b/client/src/pages/config/routes/newRoute.jsx @@ -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 <> + + setOpenNewModal(false)}> + New URL + {openNewModal && <> + + + +
+ r.Name)} + setRouteConfig={(_newRoute) => { + setNewRoute(sanitizeRoute(_newRoute)); + }} + up={() => {}} + down={() => {}} + deleteRoute={() => {}} + noControls + /> +
+
+
+
+ + {submitErrors && submitErrors.length > 0 && + {submitErrors.map((err) => { + return
{err}
+ })}
+
} + + +
+ } +
+ ; +} + +export default NewRouteCreate; \ No newline at end of file diff --git a/client/src/pages/config/routes/routeSecurity.jsx b/client/src/pages/config/routes/routeSecurity.jsx index 112209e..7fd3660 100644 --- a/client/src/pages/config/routes/routeSecurity.jsx +++ b/client/src/pages/config/routes/routeSecurity.jsx @@ -23,20 +23,24 @@ const RouteSecurity = ({ routeConfig }) => { { 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} /> + + { return h ?
@@ -47,12 +22,21 @@ const Hide = ({ children, h }) => {
: <>{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
- + {routeConfig && <> { 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' }} />} + + + + + + {submitButton &&    - - -

- + +    + + + {config && <> + {routes && {
{r.Description}
}, - { title: 'Origin', clickable:true, search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => }, - { title: 'Target', search: (r) => r.Target, field: (r) => <> }, - { title: 'Security', field: (r) => , + { title: 'Origin', screenMin: 'md', clickable:true, search: (r) => r.Host + ' ' + r.PathPrefix, field: (r) => }, + { title: 'Target', screenMin: 'md', search: (r) => r.Target, field: (r) => <> }, + { title: 'Security', screenMin: 'lg', field: (r) => , style: {minWidth: '70px'} }, { title: '', clickable:true, field: (r, k) => {
} - {/* {routes && routes.map((route,key) => (<> - { - routes[key] = sanitizeRoute(newRoute); - setNeedSave(true); - }} - up={() => up(key)} - down={() => down(key)} - deleteRoute={() => deleteRoute(key)} - /> -

- ))} */} - {routes && needSave && <>




@@ -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}`; diff --git a/client/src/pages/config/users/restart.jsx b/client/src/pages/config/users/restart.jsx index 5332202..1da8e78 100644 --- a/client/src/pages/config/users/restart.jsx +++ b/client/src/pages/config/users/restart.jsx @@ -64,7 +64,7 @@ const RestartModal = ({openModal, setOpenModal}) => { }, 1500) setTimeout(() => { setWarn(true); - }, 8000) + }, 20000) }}>Restart } diff --git a/client/src/pages/config/users/usermanagement.jsx b/client/src/pages/config/users/usermanagement.jsx index aecc9c9..f6b7813 100644 --- a/client/src/pages/config/users/usermanagement.jsx +++ b/client/src/pages/config/users/usermanagement.jsx @@ -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 = () => { - -    -

- {isLoading ?

- : -
- - - Nickname - Status - Created At - Last Login - Actions - - - - {rows.map((row) => { - const isRegistered = new Date(row.registeredAt).getTime() > 0; - const inviteExpired = new Date(row.registerKeyExp).getTime() < new Date().getTime(); + }}>Refresh   +

- const hasLastLogin = new Date(row.lastLogin).getTime() > 0; - return ( - - -   {row.nickname} - - - {isRegistered ? (row.role > 1 ? } - label="Admin" - variant="outlined" - /> : } - label="User" - variant="outlined" - />) : ( - inviteExpired ? } - label="Invite Expired" - color="error" - /> : } - label="Invite Pending" - color="warning" - /> - )} - - - {new Date(row.createdAt).toLocaleDateString()} -  - {new Date(row.createdAt).toLocaleTimeString()} - - - {hasLastLogin ? - {new Date(row.lastLogin).toLocaleDateString()} -  - {new Date(row.lastLogin).toLocaleTimeString()} - : '-'} - - - {isRegistered ? - () : - () - } -    - - ) - })} -
-
-
} - + {isLoading &&

} + + {!isLoading && rows && ( r.nickname} + columns={[ + { + title: 'User', + // underline: true, + field: (r) => {r.nickname}, + }, + { + 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 ? } + label="Admin" + /> : } + label="User" + />) : ( + inviteExpired ? } + label="Invite Expired" + color="error" + /> : } + 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 ? + () : + () + } +    + } + }, + ]} + />)} ; }; diff --git a/client/src/pages/servapps/servapps.jsx b/client/src/pages/servapps/servapps.jsx index ae3398b..e2b2974 100644 --- a/client/src/pages/servapps/servapps.jsx +++ b/client/src/pages/servapps/servapps.jsx @@ -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 = () => {
}