Add API tokens support #9
@ -9,30 +9,35 @@ import "./App.css";
|
|||||||
import { AuthApi } from "./api/AuthApi";
|
import { AuthApi } from "./api/AuthApi";
|
||||||
import { ServerApi } from "./api/ServerApi";
|
import { ServerApi } from "./api/ServerApi";
|
||||||
import {
|
import {
|
||||||
CreateNetworkRoute,
|
CreateApiTokenRoute,
|
||||||
EditNetworkRoute,
|
EditApiTokenRoute,
|
||||||
} from "./routes/EditNetworkRoute";
|
} from "./routes/EditAPITokenRoute";
|
||||||
import { CreateVMRoute, EditVMRoute } from "./routes/EditVMRoute";
|
|
||||||
import { IsoFilesRoute } from "./routes/IsoFilesRoute";
|
|
||||||
import { NetworksListRoute } from "./routes/NetworksListRoute";
|
|
||||||
import { NotFoundRoute } from "./routes/NotFound";
|
|
||||||
import { SysInfoRoute } from "./routes/SysInfoRoute";
|
|
||||||
import { VMListRoute } from "./routes/VMListRoute";
|
|
||||||
import { VMRoute } from "./routes/VMRoute";
|
|
||||||
import { VNCRoute } from "./routes/VNCRoute";
|
|
||||||
import { LoginRoute } from "./routes/auth/LoginRoute";
|
|
||||||
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
|
||||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
|
||||||
import { BaseLoginPage } from "./widgets/BaseLoginPage";
|
|
||||||
import { ViewNetworkRoute } from "./routes/ViewNetworkRoute";
|
|
||||||
import { HomeRoute } from "./routes/HomeRoute";
|
|
||||||
import { NetworkFiltersListRoute } from "./routes/NetworkFiltersListRoute";
|
|
||||||
import { ViewNWFilterRoute } from "./routes/ViewNWFilterRoute";
|
|
||||||
import {
|
import {
|
||||||
CreateNWFilterRoute,
|
CreateNWFilterRoute,
|
||||||
EditNWFilterRoute,
|
EditNWFilterRoute,
|
||||||
} from "./routes/EditNWFilterRoute";
|
} from "./routes/EditNWFilterRoute";
|
||||||
|
import {
|
||||||
|
CreateNetworkRoute,
|
||||||
|
EditNetworkRoute,
|
||||||
|
} from "./routes/EditNetworkRoute";
|
||||||
|
import { CreateVMRoute, EditVMRoute } from "./routes/EditVMRoute";
|
||||||
|
import { HomeRoute } from "./routes/HomeRoute";
|
||||||
|
import { IsoFilesRoute } from "./routes/IsoFilesRoute";
|
||||||
|
import { NetworkFiltersListRoute } from "./routes/NetworkFiltersListRoute";
|
||||||
|
import { NetworksListRoute } from "./routes/NetworksListRoute";
|
||||||
|
import { NotFoundRoute } from "./routes/NotFound";
|
||||||
|
import { SysInfoRoute } from "./routes/SysInfoRoute";
|
||||||
import { TokensListRoute } from "./routes/TokensListRoute";
|
import { TokensListRoute } from "./routes/TokensListRoute";
|
||||||
|
import { VMListRoute } from "./routes/VMListRoute";
|
||||||
|
import { VMRoute } from "./routes/VMRoute";
|
||||||
|
import { VNCRoute } from "./routes/VNCRoute";
|
||||||
|
import { ViewApiTokenRoute } from "./routes/ViewApiTokenRoute";
|
||||||
|
import { ViewNWFilterRoute } from "./routes/ViewNWFilterRoute";
|
||||||
|
import { ViewNetworkRoute } from "./routes/ViewNetworkRoute";
|
||||||
|
import { LoginRoute } from "./routes/auth/LoginRoute";
|
||||||
|
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
|
||||||
|
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
|
||||||
|
import { BaseLoginPage } from "./widgets/BaseLoginPage";
|
||||||
|
|
||||||
interface AuthContext {
|
interface AuthContext {
|
||||||
signedIn: boolean;
|
signedIn: boolean;
|
||||||
@ -74,6 +79,9 @@ export function App() {
|
|||||||
<Route path="nwfilter/:uuid/edit" element={<EditNWFilterRoute />} />
|
<Route path="nwfilter/:uuid/edit" element={<EditNWFilterRoute />} />
|
||||||
|
|
||||||
<Route path="tokens" element={<TokensListRoute />} />
|
<Route path="tokens" element={<TokensListRoute />} />
|
||||||
|
<Route path="tokens/new" element={<CreateApiTokenRoute />} />
|
||||||
|
<Route path="tokens/:id" element={<ViewApiTokenRoute />} />
|
||||||
|
<Route path="tokens/:id/edit" element={<EditApiTokenRoute />} />
|
||||||
|
|
||||||
<Route path="sysinfo" element={<SysInfoRoute />} />
|
<Route path="sysinfo" element={<SysInfoRoute />} />
|
||||||
<Route path="*" element={<NotFoundRoute />} />
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
|
@ -27,6 +27,8 @@ export interface ServerConstraints {
|
|||||||
nwfilter_comment_size: LenConstraint;
|
nwfilter_comment_size: LenConstraint;
|
||||||
nwfilter_priority: LenConstraint;
|
nwfilter_priority: LenConstraint;
|
||||||
nwfilter_selectors_count: LenConstraint;
|
nwfilter_selectors_count: LenConstraint;
|
||||||
|
api_token_name_size: LenConstraint;
|
||||||
|
api_token_description_size: LenConstraint;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LenConstraint {
|
export interface LenConstraint {
|
||||||
|
@ -21,7 +21,30 @@ export function APITokenURL(t: APIToken, edit: boolean = false): string {
|
|||||||
return `/tokens/${t.id}${edit ? "/edit" : ""}`;
|
return `/tokens/${t.id}${edit ? "/edit" : ""}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface APITokenPrivateKey {
|
||||||
|
alg: string;
|
||||||
|
priv: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreatedAPIToken {
|
||||||
|
token: APIToken;
|
||||||
|
priv_key: APITokenPrivateKey;
|
||||||
|
}
|
||||||
|
|
||||||
export class TokensApi {
|
export class TokensApi {
|
||||||
|
/**
|
||||||
|
* Create a new API token
|
||||||
|
*/
|
||||||
|
static async Create(n: APIToken): Promise<CreatedAPIToken> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "POST",
|
||||||
|
uri: "/tokens/create",
|
||||||
|
jsonData: n,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the full list of tokens
|
* Get the full list of tokens
|
||||||
*/
|
*/
|
||||||
@ -33,4 +56,39 @@ export class TokensApi {
|
|||||||
})
|
})
|
||||||
).data;
|
).data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the information about a single token
|
||||||
|
*/
|
||||||
|
static async GetSingle(uuid: string): Promise<APIToken> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri: `/tokens/${uuid}`,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an existing API token information
|
||||||
|
*/
|
||||||
|
static async Update(n: APIToken): Promise<void> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "PATCH",
|
||||||
|
uri: `/tokens/${n.id}`,
|
||||||
|
jsonData: n,
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an API token
|
||||||
|
*/
|
||||||
|
static async Delete(n: APIToken): Promise<void> {
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "DELETE",
|
||||||
|
uri: `/tokens/${n.id}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
148
virtweb_frontend/src/routes/EditAPITokenRoute.tsx
Normal file
148
virtweb_frontend/src/routes/EditAPITokenRoute.tsx
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import { Button } from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { APIToken, APITokenURL, TokensApi } from "../api/TokensApi";
|
||||||
|
import { useAlert } from "../hooks/providers/AlertDialogProvider";
|
||||||
|
import { useLoadingMessage } from "../hooks/providers/LoadingMessageProvider";
|
||||||
|
import { useSnackbar } from "../hooks/providers/SnackbarProvider";
|
||||||
|
import { time } from "../utils/DateUtils";
|
||||||
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
|
import {
|
||||||
|
APITokenDetails,
|
||||||
|
TokenWidgetStatus,
|
||||||
|
} from "../widgets/tokens/APITokenDetails";
|
||||||
|
|
||||||
|
export function CreateApiTokenRoute(): React.ReactElement {
|
||||||
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [token] = React.useState<APIToken>({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
description: "",
|
||||||
|
created: time(),
|
||||||
|
updated: time(),
|
||||||
|
last_used: time(),
|
||||||
|
rights: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
const createApiToken = async (n: APIToken) => {
|
||||||
|
try {
|
||||||
|
const res = await TokensApi.Create(n);
|
||||||
|
snackbar("The api token was successfully created!");
|
||||||
|
// TODO : show a modal to give token information
|
||||||
|
navigate(APITokenURL(res.token));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert(`Failed to create API token!\n${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EditApiTokenRouteInner
|
||||||
|
token={token}
|
||||||
|
creating={true}
|
||||||
|
onCancel={() => navigate("/tokens")}
|
||||||
|
onSave={createApiToken}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function EditApiTokenRoute(): React.ReactElement {
|
||||||
|
const alert = useAlert();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
|
||||||
|
const { id } = useParams();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [token, setToken] = React.useState<APIToken | undefined>();
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
setToken(await TokensApi.GetSingle(id!));
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateApiToken = async (n: APIToken) => {
|
||||||
|
try {
|
||||||
|
await TokensApi.Update(n);
|
||||||
|
snackbar("The token was successfully updated!");
|
||||||
|
navigate(APITokenURL(token!));
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert(`Failed to update token!\n${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={id}
|
||||||
|
ready={token !== undefined}
|
||||||
|
errMsg="Failed to fetch API token informations!"
|
||||||
|
load={load}
|
||||||
|
build={() => (
|
||||||
|
<EditApiTokenRouteInner
|
||||||
|
token={token!}
|
||||||
|
creating={false}
|
||||||
|
onCancel={() => navigate(`/token/${id}`)}
|
||||||
|
onSave={updateApiToken}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function EditApiTokenRouteInner(p: {
|
||||||
|
token: APIToken;
|
||||||
|
creating: boolean;
|
||||||
|
onCancel: () => void;
|
||||||
|
onSave: (token: APIToken) => Promise<void>;
|
||||||
|
}): React.ReactElement {
|
||||||
|
const loadingMessage = useLoadingMessage();
|
||||||
|
|
||||||
|
const [changed, setChanged] = React.useState(false);
|
||||||
|
|
||||||
|
const [, updateState] = React.useState<any>();
|
||||||
|
const forceUpdate = React.useCallback(() => updateState({}), []);
|
||||||
|
|
||||||
|
const valueChanged = () => {
|
||||||
|
setChanged(true);
|
||||||
|
forceUpdate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const save = async () => {
|
||||||
|
loadingMessage.show("Saving API token configuration...");
|
||||||
|
await p.onSave(p.token);
|
||||||
|
loadingMessage.hide();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtWebRouteContainer
|
||||||
|
label={p.creating ? "Create an API Token" : "Edit API Token"}
|
||||||
|
actions={
|
||||||
|
<span>
|
||||||
|
{changed && (
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
onClick={save}
|
||||||
|
style={{ marginRight: "10px" }}
|
||||||
|
>
|
||||||
|
{p.creating ? "Create" : "Save"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button onClick={p.onCancel} variant="outlined">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<APITokenDetails
|
||||||
|
token={p.token}
|
||||||
|
status={
|
||||||
|
p.creating ? TokenWidgetStatus.Create : TokenWidgetStatus.Update
|
||||||
|
}
|
||||||
|
onChange={valueChanged}
|
||||||
|
/>
|
||||||
|
</VirtWebRouteContainer>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import DeleteIcon from "@mui/icons-material/Delete";
|
|
||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@ -13,13 +12,13 @@ import {
|
|||||||
Typography,
|
Typography,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi";
|
import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi";
|
||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
import { RouterLink } from "../widgets/RouterLink";
|
import { RouterLink } from "../widgets/RouterLink";
|
||||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import { NetworkHookStatusWidget } from "../widgets/net/NetworkHookStatusWidget";
|
import { NetworkHookStatusWidget } from "../widgets/net/NetworkHookStatusWidget";
|
||||||
|
import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
|
||||||
|
|
||||||
export function NetworksListRoute(): React.ReactElement {
|
export function NetworksListRoute(): React.ReactElement {
|
||||||
const [list, setList] = React.useState<NetworkInfo[] | undefined>();
|
const [list, setList] = React.useState<NetworkInfo[] | undefined>();
|
||||||
|
53
virtweb_frontend/src/routes/ViewApiTokenRoute.tsx
Normal file
53
virtweb_frontend/src/routes/ViewApiTokenRoute.tsx
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import { Button } from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
|
import { APIToken, APITokenURL, TokensApi } from "../api/TokensApi";
|
||||||
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
|
import {
|
||||||
|
APITokenDetails,
|
||||||
|
TokenWidgetStatus,
|
||||||
|
} from "../widgets/tokens/APITokenDetails";
|
||||||
|
|
||||||
|
export function ViewApiTokenRoute() {
|
||||||
|
const { id } = useParams();
|
||||||
|
|
||||||
|
const [token, setToken] = React.useState<APIToken | undefined>();
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
setToken(await TokensApi.GetSingle(id!));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={id}
|
||||||
|
ready={token !== undefined}
|
||||||
|
errMsg="Failed to fetch API token information!"
|
||||||
|
load={load}
|
||||||
|
build={() => <ViewAPITokenRouteInner token={token!} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ViewAPITokenRouteInner(p: { token: APIToken }): React.ReactElement {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<VirtWebRouteContainer
|
||||||
|
label={`API token ${p.token.name}`}
|
||||||
|
actions={
|
||||||
|
<span style={{ display: "flex", alignItems: "center" }}>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
style={{ marginLeft: "15px" }}
|
||||||
|
onClick={() => navigate(APITokenURL(p.token, true))}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<APITokenDetails token={p.token} status={TokenWidgetStatus.Read} />
|
||||||
|
</VirtWebRouteContainer>
|
||||||
|
);
|
||||||
|
}
|
@ -22,14 +22,16 @@ export function IPInput(p: {
|
|||||||
export function IPInputWithMask(p: {
|
export function IPInputWithMask(p: {
|
||||||
label: string;
|
label: string;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
|
ipAndMask?: string;
|
||||||
ip?: string;
|
ip?: string;
|
||||||
mask?: number;
|
mask?: number;
|
||||||
onValueChange?: (ip?: string, mask?: number) => void;
|
onValueChange?: (ip?: string, mask?: number, ipAndMask?: string) => void;
|
||||||
version: 4 | 6;
|
version: 4 | 6;
|
||||||
}): React.ReactElement {
|
}): React.ReactElement {
|
||||||
const showSlash = React.useRef(!!p.mask);
|
const showSlash = React.useRef(!!p.mask);
|
||||||
|
|
||||||
const currValue =
|
const currValue =
|
||||||
|
p.ipAndMask ??
|
||||||
(p.ip ?? "") + (p.mask || showSlash.current ? "/" : "") + (p.mask ?? "");
|
(p.ip ?? "") + (p.mask || showSlash.current ? "/" : "") + (p.mask ?? "");
|
||||||
|
|
||||||
const { onValueChange, ...props } = p;
|
const { onValueChange, ...props } = p;
|
||||||
@ -38,7 +40,7 @@ export function IPInputWithMask(p: {
|
|||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
showSlash.current = false;
|
showSlash.current = false;
|
||||||
if (!v) {
|
if (!v) {
|
||||||
onValueChange?.(undefined, undefined);
|
onValueChange?.(undefined, undefined, undefined);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,7 +54,11 @@ export function IPInputWithMask(p: {
|
|||||||
mask = sanitizeMask(p.version, split[1]);
|
mask = sanitizeMask(p.version, split[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
onValueChange?.(ip, mask);
|
onValueChange?.(
|
||||||
|
ip,
|
||||||
|
mask,
|
||||||
|
mask || showSlash.current ? `${ip}/${mask ?? ""}` : ip
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
value={currValue}
|
value={currValue}
|
||||||
{...props}
|
{...props}
|
||||||
|
216
virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx
Normal file
216
virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
import { Button, Grid } from "@mui/material";
|
||||||
|
import React from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
|
||||||
|
import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
|
||||||
|
import { ServerApi } from "../../api/ServerApi";
|
||||||
|
import { APIToken, TokensApi } from "../../api/TokensApi";
|
||||||
|
import { VMApi, VMInfo } from "../../api/VMApi";
|
||||||
|
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
|
||||||
|
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
|
||||||
|
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
|
||||||
|
import { AsyncWidget } from "../AsyncWidget";
|
||||||
|
import { TabsWidget } from "../TabsWidget";
|
||||||
|
import { EditSection } from "../forms/EditSection";
|
||||||
|
import { IPInput, IPInputWithMask } from "../forms/IPInput";
|
||||||
|
import { ResAutostartInput } from "../forms/ResAutostartInput";
|
||||||
|
import { SelectInput } from "../forms/SelectInput";
|
||||||
|
import { TextInput } from "../forms/TextInput";
|
||||||
|
import { RadioGroupInput } from "../forms/RadioGroupInput";
|
||||||
|
|
||||||
|
export enum TokenWidgetStatus {
|
||||||
|
Create,
|
||||||
|
Read,
|
||||||
|
Update,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DetailsProps {
|
||||||
|
token: APIToken;
|
||||||
|
status: TokenWidgetStatus;
|
||||||
|
onChange?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function APITokenDetails(p: DetailsProps): React.ReactElement {
|
||||||
|
const [vms, setVMs] = React.useState<VMInfo[]>();
|
||||||
|
const [networks, setNetworks] = React.useState<NetworkInfo[]>();
|
||||||
|
const [nwFilters, setNetworkFilters] = React.useState<NWFilter[]>();
|
||||||
|
const [tokens, setTokens] = React.useState<APIToken[]>();
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
setVMs(await VMApi.GetList());
|
||||||
|
setNetworks(await NetworkApi.GetList());
|
||||||
|
setNetworkFilters(await NWFilterApi.GetList());
|
||||||
|
setTokens(await TokensApi.GetList());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncWidget
|
||||||
|
loadKey={"1"}
|
||||||
|
load={load}
|
||||||
|
errMsg="Failed to load some system entities!"
|
||||||
|
build={() => (
|
||||||
|
<APITokenDetailsInner
|
||||||
|
vms={vms!}
|
||||||
|
networks={networks!}
|
||||||
|
nwFilters={nwFilters!}
|
||||||
|
tokens={tokens!}
|
||||||
|
{...p}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TokenTab {
|
||||||
|
General = 0,
|
||||||
|
Rights,
|
||||||
|
RawRights,
|
||||||
|
Danger,
|
||||||
|
}
|
||||||
|
|
||||||
|
type DetailsInnerProps = DetailsProps & {
|
||||||
|
vms: VMInfo[];
|
||||||
|
networks: NetworkInfo[];
|
||||||
|
nwFilters: NWFilter[];
|
||||||
|
tokens: APIToken[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function APITokenDetailsInner(p: DetailsInnerProps): React.ReactElement {
|
||||||
|
const [currTab, setCurrTab] = React.useState(TokenTab.General);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TabsWidget
|
||||||
|
currTab={currTab}
|
||||||
|
onTabChange={setCurrTab}
|
||||||
|
options={[
|
||||||
|
{ label: "General", value: TokenTab.General, visible: true },
|
||||||
|
{
|
||||||
|
label: "Rights",
|
||||||
|
value: TokenTab.Rights,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Raw rights",
|
||||||
|
value: TokenTab.RawRights,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
label: "Danger zone",
|
||||||
|
value: TokenTab.Danger,
|
||||||
|
color: "red",
|
||||||
|
visible: p.status === TokenWidgetStatus.Read,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
{currTab === TokenTab.General && <NetworkDetailsTabGeneral {...p} />}
|
||||||
|
{/* todo: rights */}
|
||||||
|
{currTab === TokenTab.Danger && <APITokenTabDanger {...p} />}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function NetworkDetailsTabGeneral(p: DetailsInnerProps): React.ReactElement {
|
||||||
|
const [ipVersion, setIpVersion] = React.useState<4 | 6>(
|
||||||
|
(p.token.ip_restriction ?? "").includes(":") ? 6 : 4
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid container spacing={2}>
|
||||||
|
{/* Metadata section */}
|
||||||
|
<EditSection title="Metadata">
|
||||||
|
{p.status !== TokenWidgetStatus.Create && (
|
||||||
|
<TextInput label="UUID" editable={false} value={p.token.id} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Name"
|
||||||
|
editable={p.status === TokenWidgetStatus.Create}
|
||||||
|
value={p.token.name}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
p.token.name = v ?? "";
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
size={ServerApi.Config.constraints.api_token_name_size}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
label="Description"
|
||||||
|
editable={p.status === TokenWidgetStatus.Create}
|
||||||
|
value={p.token.description}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
p.token.description = v ?? "";
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
multiline={true}
|
||||||
|
size={ServerApi.Config.constraints.api_token_description_size}
|
||||||
|
/>
|
||||||
|
</EditSection>
|
||||||
|
|
||||||
|
<EditSection title="General settings">
|
||||||
|
{(p.status === TokenWidgetStatus.Create || p.token.ip_restriction) && (
|
||||||
|
<RadioGroupInput
|
||||||
|
{...p}
|
||||||
|
editable={p.status === TokenWidgetStatus.Create}
|
||||||
|
options={[
|
||||||
|
{ label: "IPv4", value: "4" },
|
||||||
|
{ label: "IPv6", value: "6" },
|
||||||
|
]}
|
||||||
|
value={ipVersion.toString()}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
setIpVersion(Number(v) as any);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<IPInputWithMask
|
||||||
|
{...p}
|
||||||
|
label="Token IP network restriction"
|
||||||
|
ipAndMask={p.token.ip_restriction}
|
||||||
|
editable={p.status === TokenWidgetStatus.Create}
|
||||||
|
version={ipVersion}
|
||||||
|
onValueChange={(_ip, _mask, ipAndMask) => {
|
||||||
|
p.token.ip_restriction = ipAndMask;
|
||||||
|
p.onChange?.();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* TODO : remaining */}
|
||||||
|
</EditSection>
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function APITokenTabDanger(p: DetailsInnerProps): React.ReactElement {
|
||||||
|
const confirm = useConfirm();
|
||||||
|
const snackbar = useSnackbar();
|
||||||
|
const alert = useAlert();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const requestDelete = async () => {
|
||||||
|
try {
|
||||||
|
if (
|
||||||
|
!(await confirm(
|
||||||
|
"Do you really want to delete this API token?",
|
||||||
|
`Delete API token ${p.token.name}`,
|
||||||
|
"Delete"
|
||||||
|
))
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await TokensApi.Delete(p.token);
|
||||||
|
|
||||||
|
navigate("/tokens");
|
||||||
|
snackbar("The API token was successfully deleted!");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
alert(`Failed to delete the API token!\n${e}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button color="error" onClick={requestDelete}>
|
||||||
|
Delete this API token
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user