mirror of
https://github.com/dstotijn/hetty.git
synced 2025-07-01 18:47:29 -04:00
Add scope support
This commit is contained in:
@ -6,7 +6,7 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"export": "next build && next export -o dist"
|
||||
"export": "rm -rf .next && next build && next export -o dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"@apollo/client": "^3.2.0",
|
||||
|
@ -22,6 +22,7 @@ import HomeIcon from "@material-ui/icons/Home";
|
||||
import SettingsEthernetIcon from "@material-ui/icons/SettingsEthernet";
|
||||
import SendIcon from "@material-ui/icons/Send";
|
||||
import FolderIcon from "@material-ui/icons/Folder";
|
||||
import LocationSearchingIcon from "@material-ui/icons/LocationSearching";
|
||||
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
|
||||
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
|
||||
import clsx from "clsx";
|
||||
@ -33,6 +34,7 @@ export enum Page {
|
||||
ProxySetup,
|
||||
ProxyLogs,
|
||||
Sender,
|
||||
Scope,
|
||||
}
|
||||
|
||||
const drawerWidth = 240;
|
||||
@ -236,6 +238,22 @@ export function Layout({ title, page, children }: Props): JSX.Element {
|
||||
<ListItemText primary="Sender" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link href="/scope" passHref>
|
||||
<ListItem
|
||||
button
|
||||
component="a"
|
||||
key="scope"
|
||||
selected={page === Page.Scope}
|
||||
className={classes.listItem}
|
||||
>
|
||||
<Tooltip title="Scope">
|
||||
<ListItemIcon className={classes.listItemIcon}>
|
||||
<LocationSearchingIcon />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary="Scope" />
|
||||
</ListItem>
|
||||
</Link>
|
||||
<Link href="/projects" passHref>
|
||||
<ListItem
|
||||
button
|
||||
|
@ -120,6 +120,9 @@ function ProjectList(): JSX.Element {
|
||||
});
|
||||
return DELETE;
|
||||
},
|
||||
httpRequestLogFilter(_, { DELETE }) {
|
||||
return DELETE;
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
@ -136,6 +139,9 @@ function ProjectList(): JSX.Element {
|
||||
projects(_, { DELETE }) {
|
||||
return DELETE;
|
||||
},
|
||||
httpRequestLogFilter(_, { DELETE }) {
|
||||
return DELETE;
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
|
201
admin/src/components/reqlog/Search.tsx
Normal file
201
admin/src/components/reqlog/Search.tsx
Normal file
@ -0,0 +1,201 @@
|
||||
import {
|
||||
Box,
|
||||
Checkbox,
|
||||
CircularProgress,
|
||||
ClickAwayListener,
|
||||
createStyles,
|
||||
FormControlLabel,
|
||||
InputBase,
|
||||
makeStyles,
|
||||
Paper,
|
||||
Popper,
|
||||
Theme,
|
||||
Tooltip,
|
||||
useTheme,
|
||||
} from "@material-ui/core";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import FilterListIcon from "@material-ui/icons/FilterList";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { gql, useApolloClient, useMutation, useQuery } from "@apollo/client";
|
||||
import { withoutTypename } from "../../lib/omitTypename";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
|
||||
const FILTER = gql`
|
||||
query HttpRequestLogFilter {
|
||||
httpRequestLogFilter {
|
||||
onlyInScope
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const SET_FILTER = gql`
|
||||
mutation SetHttpRequestLogFilter($filter: HttpRequestLogFilterInput) {
|
||||
setHttpRequestLogFilter(filter: $filter) {
|
||||
onlyInScope
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
padding: "2px 4px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
width: 400,
|
||||
},
|
||||
input: {
|
||||
marginLeft: theme.spacing(1),
|
||||
flex: 1,
|
||||
},
|
||||
iconButton: {
|
||||
padding: 10,
|
||||
},
|
||||
filterPopper: {
|
||||
width: 400,
|
||||
marginTop: 6,
|
||||
zIndex: 99,
|
||||
},
|
||||
filterOptions: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
filterLoading: {
|
||||
marginRight: 1,
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export interface SearchFilter {
|
||||
onlyInScope: boolean;
|
||||
}
|
||||
|
||||
function Search(): JSX.Element {
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
const { loading: filterLoading, error: filterErr, data: filter } = useQuery(
|
||||
FILTER
|
||||
);
|
||||
|
||||
const client = useApolloClient();
|
||||
const [
|
||||
setFilterMutate,
|
||||
{ error: setFilterErr, loading: setFilterLoading },
|
||||
] = useMutation<{
|
||||
setHttpRequestLogFilter: SearchFilter | null;
|
||||
}>(SET_FILTER, {
|
||||
update(_, { data: { setHttpRequestLogFilter } }) {
|
||||
client.writeQuery({
|
||||
query: FILTER,
|
||||
data: {
|
||||
httpRequestLogFilter: setHttpRequestLogFilter,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const filterRef = useRef<HTMLElement | null>();
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
|
||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleClickAway = (event: React.MouseEvent<EventTarget>) => {
|
||||
if (filterRef.current.contains(event.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
setFilterOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<ClickAwayListener onClickAway={handleClickAway}>
|
||||
<Box style={{ display: "inline-block" }}>
|
||||
{filterErr && (
|
||||
<Box mb={4}>
|
||||
<Alert severity="error">
|
||||
Error fetching filter: {filterErr.message}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
{setFilterErr && (
|
||||
<Box mb={4}>
|
||||
<Alert severity="error">
|
||||
Error setting filter: {setFilterErr.message}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
<Paper
|
||||
component="form"
|
||||
onSubmit={handleSubmit}
|
||||
ref={filterRef}
|
||||
className={classes.root}
|
||||
>
|
||||
<Tooltip title="Toggle filter options">
|
||||
<IconButton
|
||||
className={classes.iconButton}
|
||||
onClick={() => setFilterOpen(!filterOpen)}
|
||||
style={{
|
||||
color:
|
||||
filter?.httpRequestLogFilter !== null
|
||||
? theme.palette.secondary.main
|
||||
: "inherit",
|
||||
}}
|
||||
>
|
||||
{filterLoading || setFilterLoading ? (
|
||||
<CircularProgress className={classes.filterLoading} size={23} />
|
||||
) : (
|
||||
<FilterListIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<InputBase
|
||||
className={classes.input}
|
||||
placeholder="Search proxy logs…"
|
||||
onFocus={() => setFilterOpen(true)}
|
||||
/>
|
||||
<Tooltip title="Search">
|
||||
<IconButton type="submit" className={classes.iconButton}>
|
||||
<SearchIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</Paper>
|
||||
|
||||
<Popper
|
||||
className={classes.filterPopper}
|
||||
open={filterOpen}
|
||||
anchorEl={filterRef.current}
|
||||
placement="bottom-start"
|
||||
>
|
||||
<Paper className={classes.filterOptions}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Checkbox
|
||||
checked={
|
||||
filter?.httpRequestLogFilter?.onlyInScope ? true : false
|
||||
}
|
||||
disabled={filterLoading || setFilterLoading}
|
||||
onChange={(e) =>
|
||||
setFilterMutate({
|
||||
variables: {
|
||||
filter: {
|
||||
...withoutTypename(filter?.httpRequestLogFilter),
|
||||
onlyInScope: e.target.checked,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Only show in-scope requests"
|
||||
/>
|
||||
</Paper>
|
||||
</Popper>
|
||||
</Box>
|
||||
</ClickAwayListener>
|
||||
);
|
||||
}
|
||||
|
||||
export default Search;
|
140
admin/src/components/scope/AddRule.tsx
Normal file
140
admin/src/components/scope/AddRule.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import { gql, useApolloClient, useMutation } from "@apollo/client";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
CircularProgress,
|
||||
createStyles,
|
||||
FormControl,
|
||||
FormControlLabel,
|
||||
FormLabel,
|
||||
makeStyles,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
TextField,
|
||||
Theme,
|
||||
} from "@material-ui/core";
|
||||
import AddIcon from "@material-ui/icons/Add";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import React from "react";
|
||||
import { SCOPE } from "./Rules";
|
||||
|
||||
const SET_SCOPE = gql`
|
||||
mutation SetScope($scope: [ScopeRuleInput!]!) {
|
||||
setScope(scope: $scope) {
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
ruleExpression: {
|
||||
fontFamily: "'JetBrains Mono', monospace",
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
function AddRule(): JSX.Element {
|
||||
const classes = useStyles();
|
||||
|
||||
const [ruleType, setRuleType] = React.useState("url");
|
||||
const [expression, setExpression] = React.useState(null);
|
||||
|
||||
const client = useApolloClient();
|
||||
const [setScope, { error, loading }] = useMutation(SET_SCOPE, {
|
||||
onError() {},
|
||||
onCompleted() {
|
||||
expression.value = "";
|
||||
},
|
||||
update(_, { data: { setScope } }) {
|
||||
client.writeQuery({
|
||||
query: SCOPE,
|
||||
data: { scope: setScope },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleTypeChange = (e: React.ChangeEvent, value: string) => {
|
||||
setRuleType(value);
|
||||
};
|
||||
const handleSubmit = (e: React.SyntheticEvent) => {
|
||||
e.preventDefault();
|
||||
let scope = [];
|
||||
|
||||
try {
|
||||
const data = client.readQuery({
|
||||
query: SCOPE,
|
||||
});
|
||||
scope = data.scope;
|
||||
} catch (e) {}
|
||||
|
||||
setScope({
|
||||
variables: {
|
||||
scope: [
|
||||
...scope.map(({ url }) => ({ url })),
|
||||
{ url: expression.value },
|
||||
],
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{error && (
|
||||
<Box mb={4}>
|
||||
<Alert severity="error">Error adding rule: {error.message}</Alert>
|
||||
</Box>
|
||||
)}
|
||||
<form onSubmit={handleSubmit} autoComplete="off">
|
||||
<FormControl fullWidth>
|
||||
<FormLabel color="secondary" component="legend">
|
||||
Rule Type
|
||||
</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
name="ruleType"
|
||||
value={ruleType}
|
||||
onChange={handleTypeChange}
|
||||
>
|
||||
<FormControlLabel value="url" control={<Radio />} label="URL" />
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<FormControl fullWidth>
|
||||
<TextField
|
||||
label="Expression"
|
||||
placeholder="^https:\/\/(.*)example.com(.*)"
|
||||
helperText="Regular expression to match on."
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
required
|
||||
InputProps={{
|
||||
className: classes.ruleExpression,
|
||||
}}
|
||||
InputLabelProps={{
|
||||
shrink: true,
|
||||
}}
|
||||
inputProps={{
|
||||
ref: (node) => {
|
||||
setExpression(node);
|
||||
},
|
||||
}}
|
||||
margin="normal"
|
||||
/>
|
||||
</FormControl>
|
||||
<Box my={2}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
disabled={loading}
|
||||
startIcon={loading ? <CircularProgress size={22} /> : <AddIcon />}
|
||||
>
|
||||
Add rule
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default AddRule;
|
86
admin/src/components/scope/RuleListItem.tsx
Normal file
86
admin/src/components/scope/RuleListItem.tsx
Normal file
@ -0,0 +1,86 @@
|
||||
import { gql, useApolloClient, useMutation, useQuery } from "@apollo/client";
|
||||
import {
|
||||
Avatar,
|
||||
Chip,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemSecondaryAction,
|
||||
ListItemText,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
import CodeIcon from "@material-ui/icons/Code";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import React from "react";
|
||||
import { SCOPE } from "./Rules";
|
||||
|
||||
const SET_SCOPE = gql`
|
||||
mutation SetScope($scope: [ScopeRuleInput!]!) {
|
||||
setScope(scope: $scope) {
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function RuleListItem({ scope, rule, index }): JSX.Element {
|
||||
const client = useApolloClient();
|
||||
const [setScope, { loading }] = useMutation(SET_SCOPE, {
|
||||
update(_, { data: { setScope } }) {
|
||||
client.writeQuery({
|
||||
query: SCOPE,
|
||||
data: { scope: setScope },
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
const clone = [...scope];
|
||||
clone.splice(index, 1);
|
||||
setScope({
|
||||
variables: {
|
||||
scope: clone.map(({ url }) => ({ url })),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<CodeIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<RuleListItemText rule={rule} />
|
||||
<ListItemSecondaryAction>
|
||||
<RuleTypeChip rule={rule} />
|
||||
<Tooltip title="Delete rule">
|
||||
<span style={{ marginLeft: 8 }}>
|
||||
<IconButton onClick={() => handleDelete(index)} disabled={loading}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
function RuleListItemText({ rule }): JSX.Element {
|
||||
let text: JSX.Element;
|
||||
|
||||
if (rule.url) {
|
||||
text = <code>{rule.url}</code>;
|
||||
}
|
||||
|
||||
// TODO: Parse and handle rule.header and rule.body.
|
||||
|
||||
return <ListItemText>{text}</ListItemText>;
|
||||
}
|
||||
|
||||
function RuleTypeChip({ rule }): JSX.Element {
|
||||
if (rule.url) {
|
||||
return <Chip label="URL" variant="outlined" />;
|
||||
}
|
||||
}
|
||||
|
||||
export default RuleListItem;
|
55
admin/src/components/scope/Rules.tsx
Normal file
55
admin/src/components/scope/Rules.tsx
Normal file
@ -0,0 +1,55 @@
|
||||
import { gql, useQuery } from "@apollo/client";
|
||||
import {
|
||||
CircularProgress,
|
||||
createStyles,
|
||||
List,
|
||||
makeStyles,
|
||||
Theme,
|
||||
} from "@material-ui/core";
|
||||
import { Alert } from "@material-ui/lab";
|
||||
import React from "react";
|
||||
import RuleListItem from "./RuleListItem";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
rulesList: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export const SCOPE = gql`
|
||||
query Scope {
|
||||
scope {
|
||||
url
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
function Rules(): JSX.Element {
|
||||
const classes = useStyles();
|
||||
const { loading, error, data } = useQuery(SCOPE);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{loading && <CircularProgress />}
|
||||
{error && (
|
||||
<Alert severity="error">Error fetching scope: {error.message}</Alert>
|
||||
)}
|
||||
{data?.scope.length > 0 && (
|
||||
<List className={classes.rulesList}>
|
||||
{data.scope.map((rule, index) => (
|
||||
<RuleListItem
|
||||
key={index}
|
||||
rule={rule}
|
||||
scope={data.scope}
|
||||
index={index}
|
||||
/>
|
||||
))}
|
||||
</List>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Rules;
|
5
admin/src/lib/omitTypename.ts
Normal file
5
admin/src/lib/omitTypename.ts
Normal file
@ -0,0 +1,5 @@
|
||||
const omitTypename = (key, value) => (key === "__typename" ? undefined : value);
|
||||
|
||||
export function withoutTypename(input: any): any {
|
||||
return JSON.parse(JSON.stringify(input), omitTypename);
|
||||
}
|
@ -1,9 +1,15 @@
|
||||
import { Box } from "@material-ui/core";
|
||||
|
||||
import LogsOverview from "../../../components/reqlog/LogsOverview";
|
||||
import Layout, { Page } from "../../../components/Layout";
|
||||
import Search from "../../../components/reqlog/Search";
|
||||
|
||||
function ProxyLogs(): JSX.Element {
|
||||
return (
|
||||
<Layout page={Page.ProxyLogs} title="Proxy logs">
|
||||
<Box mb={2}>
|
||||
<Search />
|
||||
</Box>
|
||||
<LogsOverview />
|
||||
</Layout>
|
||||
);
|
||||
|
39
admin/src/pages/scope/index.tsx
Normal file
39
admin/src/pages/scope/index.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Box, Divider, Grid, Typography } from "@material-ui/core";
|
||||
import React from "react";
|
||||
|
||||
import Layout, { Page } from "../../components/Layout";
|
||||
import AddRule from "../../components/scope/AddRule";
|
||||
import Rules from "../../components/scope/Rules";
|
||||
|
||||
function Index(): JSX.Element {
|
||||
return (
|
||||
<Layout page={Page.Scope} title="Scope">
|
||||
<Box p={4}>
|
||||
<Box mb={3}>
|
||||
<Typography variant="h4">Scope</Typography>
|
||||
</Box>
|
||||
<Typography paragraph>
|
||||
Scope rules are used by various modules in Hetty and can influence
|
||||
their behavior. For example: the Proxy logs module can match incoming
|
||||
requests against scope rules and decide its behavior (e.g. log or
|
||||
bypass) based on the outcome of the match. All scope configuration is
|
||||
stored per project.
|
||||
</Typography>
|
||||
<Box my={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
<Grid container>
|
||||
<Grid item xs={12} sm={12} md={8} lg={6}>
|
||||
<AddRule />
|
||||
<Box my={4}>
|
||||
<Divider />
|
||||
</Box>
|
||||
<Rules />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
export default Index;
|
Reference in New Issue
Block a user