Add API tokens support (#9)
All checks were successful
continuous-integration/drone/push Build is passing

Make it possible to create token authorized to query predetermined set of routes.

Reviewed-on: #9
Co-authored-by: Pierre HUBERT <pierre.git@communiquons.org>
Co-committed-by: Pierre HUBERT <pierre.git@communiquons.org>
This commit is contained in:
2024-04-23 17:04:43 +00:00
committed by Pierre Hubert
parent 149e3f4d72
commit c7de64cc02
33 changed files with 2686 additions and 60 deletions

View File

@ -0,0 +1,161 @@
import { Button } from "@mui/material";
import React from "react";
import { useNavigate, useParams } from "react-router-dom";
import {
APIToken,
APITokenURL,
CreatedAPIToken,
TokensApi,
} from "../api/TokensApi";
import { CreatedTokenDialog } from "../dialogs/CreatedTokenDialog";
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 [createdToken, setCreatedToken] = React.useState<
CreatedAPIToken | undefined
>();
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!");
setCreatedToken(res);
} catch (e) {
console.error(e);
alert(`Failed to create API token!\n${e}`);
}
};
return (
<>
{createdToken && <CreatedTokenDialog createdToken={createdToken} />}
<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>
);
}

View File

@ -1,4 +1,3 @@
import DeleteIcon from "@mui/icons-material/Delete";
import VisibilityIcon from "@mui/icons-material/Visibility";
import {
Button,
@ -13,13 +12,13 @@ import {
Typography,
} from "@mui/material";
import React from "react";
import { useNavigate } from "react-router-dom";
import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { RouterLink } from "../widgets/RouterLink";
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
import { useNavigate } from "react-router-dom";
import { NetworkHookStatusWidget } from "../widgets/net/NetworkHookStatusWidget";
import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
export function NetworksListRoute(): React.ReactElement {
const [list, setList] = React.useState<NetworkInfo[] | undefined>();

View File

@ -0,0 +1,126 @@
import VisibilityIcon from "@mui/icons-material/Visibility";
import {
Button,
IconButton,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@mui/material";
import React from "react";
import { useNavigate } from "react-router-dom";
import {
APIToken,
APITokenURL,
ExpiredAPIToken,
TokensApi,
} from "../api/TokensApi";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { RouterLink } from "../widgets/RouterLink";
import { TimeWidget, timeDiff } from "../widgets/TimeWidget";
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
export function TokensListRoute(): React.ReactElement {
const [list, setList] = React.useState<APIToken[] | undefined>();
const [count] = React.useState(1);
const load = async () => {
setList(await TokensApi.GetList());
};
return (
<AsyncWidget
loadKey={count}
load={load}
ready={list !== undefined}
errMsg="Failed to load the list of tokens!"
build={() => <TokensListRouteInner list={list!} />}
/>
);
}
export function TokensListRouteInner(p: {
list: APIToken[];
}): React.ReactElement {
const navigate = useNavigate();
return (
<VirtWebRouteContainer
label="API tokens"
actions={
<RouterLink to="/token/new">
<Button>New</Button>
</RouterLink>
}
>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Description</TableCell>
<TableCell>Created</TableCell>
<TableCell>Updated</TableCell>
<TableCell>Last used</TableCell>
<TableCell>IP restriction</TableCell>
<TableCell>Max inactivity</TableCell>
<TableCell>Rights</TableCell>
<TableCell>Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{p.list.map((t) => {
return (
<TableRow
key={t.id}
hover
onDoubleClick={() => navigate(APITokenURL(t))}
style={{ backgroundColor: ExpiredAPIToken(t) ? "red" : "" }}
>
<TableCell>
{t.name} {ExpiredAPIToken(t) && <i>(Expired)</i>}
</TableCell>
<TableCell>{t.description}</TableCell>
<TableCell>
<TimeWidget time={t.created} />
</TableCell>
<TableCell>
<TimeWidget time={t.updated} />
</TableCell>
<TableCell>
<TimeWidget time={t.last_used} />
</TableCell>
<TableCell>{t.ip_restriction}</TableCell>
<TableCell>
{t.max_inactivity && timeDiff(0, t.max_inactivity)}
</TableCell>
<TableCell>
{t.rights.map((r) => {
return (
<div>
{r.verb} {r.path}
</div>
);
})}
</TableCell>
<TableCell>
<RouterLink to={APITokenURL(t)}>
<IconButton>
<VisibilityIcon />
</IconButton>
</RouterLink>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
</VirtWebRouteContainer>
);
}

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