Add API tokens support #9
@ -53,6 +53,7 @@ struct ServerConstraints {
|
|||||||
nwfilter_selectors_count: LenConstraints,
|
nwfilter_selectors_count: LenConstraints,
|
||||||
api_token_name_size: LenConstraints,
|
api_token_name_size: LenConstraints,
|
||||||
api_token_description_size: LenConstraints,
|
api_token_description_size: LenConstraints,
|
||||||
|
api_token_right_path_size: LenConstraints,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
||||||
@ -110,6 +111,8 @@ pub async fn static_config(local_auth: LocalAuthEnabled) -> impl Responder {
|
|||||||
min: constants::API_TOKEN_DESCRIPTION_MIN_LENGTH,
|
min: constants::API_TOKEN_DESCRIPTION_MIN_LENGTH,
|
||||||
max: constants::API_TOKEN_DESCRIPTION_MAX_LENGTH,
|
max: constants::API_TOKEN_DESCRIPTION_MAX_LENGTH,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
api_token_right_path_size: LenConstraints { min: 0, max: 255 },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -85,7 +85,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
|
|
||||||
let mut cors = Cors::default()
|
let mut cors = Cors::default()
|
||||||
.allowed_origin(&AppConfig::get().website_origin)
|
.allowed_origin(&AppConfig::get().website_origin)
|
||||||
.allowed_methods(vec!["GET", "POST", "DELETE", "PUT"])
|
.allowed_methods(vec!["GET", "POST", "DELETE", "PUT", "PATCH"])
|
||||||
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
|
.allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
|
||||||
.allowed_header(header::CONTENT_TYPE)
|
.allowed_header(header::CONTENT_TYPE)
|
||||||
.supports_credentials()
|
.supports_credentials()
|
||||||
|
@ -29,6 +29,7 @@ export interface ServerConstraints {
|
|||||||
nwfilter_selectors_count: LenConstraint;
|
nwfilter_selectors_count: LenConstraint;
|
||||||
api_token_name_size: LenConstraint;
|
api_token_name_size: LenConstraint;
|
||||||
api_token_description_size: LenConstraint;
|
api_token_description_size: LenConstraint;
|
||||||
|
api_token_right_path_size: LenConstraint;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LenConstraint {
|
export interface LenConstraint {
|
||||||
|
@ -16,7 +16,7 @@ export interface SelectOption {
|
|||||||
export function SelectInput(p: {
|
export function SelectInput(p: {
|
||||||
value?: string;
|
value?: string;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
label: string;
|
label?: string;
|
||||||
options: SelectOption[];
|
options: SelectOption[];
|
||||||
onValueChange: (o?: string) => void;
|
onValueChange: (o?: string) => void;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
@ -29,7 +29,7 @@ export function SelectInput(p: {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}>
|
<FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}>
|
||||||
<InputLabel>{p.label}</InputLabel>
|
{p.label && <InputLabel>{p.label}</InputLabel>}
|
||||||
<Select
|
<Select
|
||||||
value={p.value ?? ""}
|
value={p.value ?? ""}
|
||||||
label={p.label}
|
label={p.label}
|
||||||
|
@ -15,6 +15,7 @@ import { EditSection } from "../forms/EditSection";
|
|||||||
import { IPInputWithMask } from "../forms/IPInput";
|
import { IPInputWithMask } from "../forms/IPInput";
|
||||||
import { RadioGroupInput } from "../forms/RadioGroupInput";
|
import { RadioGroupInput } from "../forms/RadioGroupInput";
|
||||||
import { TextInput } from "../forms/TextInput";
|
import { TextInput } from "../forms/TextInput";
|
||||||
|
import { RawRightsEditor } from "./RawRightsEditor";
|
||||||
|
|
||||||
const SECS_PER_DAY = 3600 * 24;
|
const SECS_PER_DAY = 3600 * 24;
|
||||||
|
|
||||||
@ -104,14 +105,15 @@ function APITokenDetailsInner(p: DetailsInnerProps): React.ReactElement {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{currTab === TokenTab.General && <NetworkDetailsTabGeneral {...p} />}
|
{currTab === TokenTab.General && <APITokenTabGeneral {...p} />}
|
||||||
{/* todo: rights */}
|
{/* todo: rights */}
|
||||||
|
{currTab === TokenTab.RawRights && <APITokenRawRights {...p} />}
|
||||||
{currTab === TokenTab.Danger && <APITokenTabDanger {...p} />}
|
{currTab === TokenTab.Danger && <APITokenTabDanger {...p} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function NetworkDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
function APITokenTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
||||||
const [ipVersion, setIpVersion] = React.useState<4 | 6>(
|
const [ipVersion, setIpVersion] = React.useState<4 | 6>(
|
||||||
(p.token.ip_restriction ?? "").includes(":") ? 6 : 4
|
(p.token.ip_restriction ?? "").includes(":") ? 6 : 4
|
||||||
);
|
);
|
||||||
@ -196,6 +198,14 @@ function NetworkDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function APITokenRawRights(p: DetailsInnerProps): React.ReactElement {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: "30px" }}>
|
||||||
|
<RawRightsEditor {...p} editable={p.status !== TokenWidgetStatus.Read} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function APITokenTabDanger(p: DetailsInnerProps): React.ReactElement {
|
function APITokenTabDanger(p: DetailsInnerProps): React.ReactElement {
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
const snackbar = useSnackbar();
|
const snackbar = useSnackbar();
|
||||||
|
111
virtweb_frontend/src/widgets/tokens/RawRightsEditor.tsx
Normal file
111
virtweb_frontend/src/widgets/tokens/RawRightsEditor.tsx
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
|
import DeleteIcon from "@mui/icons-material/Delete";
|
||||||
|
import {
|
||||||
|
IconButton,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
|
import { APIToken } from "../../api/TokensApi";
|
||||||
|
import { SelectInput } from "../forms/SelectInput";
|
||||||
|
import { TextInput } from "../forms/TextInput";
|
||||||
|
|
||||||
|
export function RawRightsEditor(p: {
|
||||||
|
token: APIToken;
|
||||||
|
editable: boolean;
|
||||||
|
onChange?: () => void;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const addRule = () => {
|
||||||
|
p.token.rights.push({ path: "/api/", verb: "GET" });
|
||||||
|
p.onChange?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteRule = (id: number) => {
|
||||||
|
p.token.rights.splice(id, 1);
|
||||||
|
p.onChange?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TableContainer component={Paper}>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
padding: "10px 10px 0px 10px",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h5">Raw rights</Typography>
|
||||||
|
<div>
|
||||||
|
{p.editable && (
|
||||||
|
<Tooltip title="Add a new right rule">
|
||||||
|
<IconButton onClick={addRule}>
|
||||||
|
<AddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Table>
|
||||||
|
<TableHead>
|
||||||
|
<TableRow>
|
||||||
|
<TableCell>Verb</TableCell>
|
||||||
|
<TableCell>URI</TableCell>
|
||||||
|
{p.editable && <TableCell>Actions</TableCell>}
|
||||||
|
</TableRow>
|
||||||
|
</TableHead>
|
||||||
|
<TableBody>
|
||||||
|
{p.token.rights.map((r, num) => (
|
||||||
|
<TableRow key={num} hover>
|
||||||
|
<TableCell style={{ width: "100px" }}>
|
||||||
|
<SelectInput
|
||||||
|
{...p}
|
||||||
|
value={r.verb}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
r.verb = v as any;
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
options={[
|
||||||
|
{ value: "GET" },
|
||||||
|
{ value: "POST" },
|
||||||
|
{ value: "PATCH" },
|
||||||
|
{ value: "PUT" },
|
||||||
|
{ value: "DELETE" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<TextInput
|
||||||
|
{...p}
|
||||||
|
value={r.path}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
r.path = v ?? "";
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
checkValue={(v) => v.startsWith("/api/")}
|
||||||
|
size={ServerApi.Config.constraints.api_token_right_path_size}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
{p.editable && (
|
||||||
|
<TableCell style={{ width: "100px" }}>
|
||||||
|
<IconButton onClick={() => deleteRule(num)}>
|
||||||
|
<Tooltip title="Remove the rule">
|
||||||
|
<DeleteIcon />
|
||||||
|
</Tooltip>
|
||||||
|
</IconButton>
|
||||||
|
</TableCell>
|
||||||
|
)}
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableContainer>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user