294 lines
7.8 KiB
TypeScript
294 lines
7.8 KiB
TypeScript
import AddIcon from "@mui/icons-material/Add";
|
|
import RefreshIcon from "@mui/icons-material/Refresh";
|
|
import { Alert, AlertTitle, IconButton, Tooltip } from "@mui/material";
|
|
import type { GridColDef } from "@mui/x-data-grid";
|
|
import { DataGrid, GridActionsCellItem } from "@mui/x-data-grid";
|
|
import { QRCodeCanvas } from "qrcode.react";
|
|
import React from "react";
|
|
import { APIClient } from "../api/ApiClient";
|
|
import { TokensApi, type Token, type TokenWithSecret } from "../api/TokensApi";
|
|
import { CreateTokenDialog } from "../dialogs/CreateTokenDialog";
|
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
|
import { CopyTextChip } from "../widgets/CopyTextChip";
|
|
import { MatrixGWRouteContainer } from "../widgets/MatrixGWRouteContainer";
|
|
import { TimeWidget } from "../widgets/TimeWidget";
|
|
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
|
|
import { useSnackbar } from "../hooks/contexts_provider/SnackbarProvider";
|
|
import { useConfirm } from "../hooks/contexts_provider/ConfirmDialogProvider";
|
|
import { useAlert } from "../hooks/contexts_provider/AlertDialogProvider";
|
|
import { time } from "../utils/DateUtils";
|
|
|
|
export function APITokensRoute(): React.ReactElement {
|
|
const count = React.useRef(0);
|
|
|
|
const [openCreateTokenDialog, setOpenCreateTokenDialog] =
|
|
React.useState(false);
|
|
|
|
const [createdToken, setCreatedToken] =
|
|
React.useState<TokenWithSecret | null>(null);
|
|
|
|
const [list, setList] = React.useState<Token[] | undefined>();
|
|
|
|
const load = async () => {
|
|
setList(await TokensApi.GetList());
|
|
};
|
|
|
|
const handleRefreshTokensList = () => {
|
|
count.current += 1;
|
|
setList(undefined);
|
|
};
|
|
|
|
const handleOpenCreateTokenDialog = () => setOpenCreateTokenDialog(true);
|
|
|
|
const handleCancelCreateToken = () => setOpenCreateTokenDialog(false);
|
|
|
|
const handleCreatedToken = (s: TokenWithSecret) => {
|
|
setCreatedToken(s);
|
|
setOpenCreateTokenDialog(false);
|
|
handleRefreshTokensList();
|
|
};
|
|
|
|
return (
|
|
<MatrixGWRouteContainer
|
|
label={"API tokens"}
|
|
actions={
|
|
<span>
|
|
<Tooltip title="Create new token">
|
|
<IconButton onClick={handleOpenCreateTokenDialog}>
|
|
<AddIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
|
|
<Tooltip title="Refresh tokens list">
|
|
<IconButton onClick={handleRefreshTokensList}>
|
|
<RefreshIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</span>
|
|
}
|
|
>
|
|
{/* Create token dialog anchor */}
|
|
<CreateTokenDialog
|
|
open={openCreateTokenDialog}
|
|
onCreated={handleCreatedToken}
|
|
onClose={handleCancelCreateToken}
|
|
/>
|
|
|
|
{/* Info about created token */}
|
|
{createdToken && <CreatedToken token={createdToken!} />}
|
|
|
|
{/* Tokens list */}
|
|
<AsyncWidget
|
|
loadKey={count.current}
|
|
ready={list !== undefined}
|
|
load={load}
|
|
errMsg="Failed to load the list of tokens!"
|
|
build={() => (
|
|
<TokensListGrid list={list!} onReload={handleRefreshTokensList} />
|
|
)}
|
|
/>
|
|
</MatrixGWRouteContainer>
|
|
);
|
|
}
|
|
|
|
function CreatedToken(p: { token: TokenWithSecret }): React.ReactElement {
|
|
return (
|
|
<Alert severity="success" style={{ margin: "10px" }}>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
flexDirection: "row",
|
|
}}
|
|
>
|
|
<div style={{ textAlign: "center", marginRight: "10px" }}>
|
|
<div style={{ padding: "15px", backgroundColor: "white" }}>
|
|
<QRCodeCanvas
|
|
value={`matrixgw://api=${encodeURIComponent(
|
|
APIClient.ActualBackendURL()
|
|
)}&id=${p.token.id}&secret=${p.token.secret}`}
|
|
/>
|
|
</div>
|
|
<br />
|
|
<em>Mobile App Qr Code</em>
|
|
</div>
|
|
<div>
|
|
<AlertTitle>Token successfully created</AlertTitle>
|
|
The API token <i>{p.token.name}</i> was successfully created. Please
|
|
note the following information as they won't be available after.
|
|
<br />
|
|
<br />
|
|
API URL: <CopyTextChip text={APIClient.ActualBackendURL()} />
|
|
<br />
|
|
Token ID: <CopyTextChip text={p.token.id.toString()} />
|
|
<br />
|
|
Token secret: <CopyTextChip text={p.token.secret} />
|
|
</div>
|
|
</div>
|
|
</Alert>
|
|
);
|
|
}
|
|
|
|
function TokensListGrid(p: {
|
|
list: Token[];
|
|
onReload: () => void;
|
|
}): React.ReactElement {
|
|
const snackbar = useSnackbar();
|
|
const confirm = useConfirm();
|
|
const alert = useAlert();
|
|
|
|
// Delete a token
|
|
const handleDeleteClick = (token: Token) => async () => {
|
|
try {
|
|
if (
|
|
!(await confirm(
|
|
`Do you really want to delete the token named '${token.name}' ?`
|
|
))
|
|
)
|
|
return;
|
|
|
|
await TokensApi.Delete(token);
|
|
p.onReload();
|
|
|
|
snackbar("The token was successfully deleted!");
|
|
} catch (e) {
|
|
console.error(e);
|
|
alert(`Failed to delete API token! ${e}`);
|
|
}
|
|
};
|
|
|
|
const columns: GridColDef<(typeof p.list)[number]>[] = [
|
|
{ field: "id", headerName: "ID", flex: 1 },
|
|
{
|
|
field: "name",
|
|
headerName: "Name",
|
|
flex: 3,
|
|
},
|
|
{
|
|
field: "networks",
|
|
headerName: "Networks restriction",
|
|
flex: 3,
|
|
renderCell(params) {
|
|
return (
|
|
params.row.networks?.join(", ") ?? (
|
|
<span style={{ fontStyle: "italic" }}>Unrestricted</span>
|
|
)
|
|
);
|
|
},
|
|
},
|
|
{
|
|
field: "created",
|
|
headerName: "Creation",
|
|
flex: 3,
|
|
renderCell(params) {
|
|
return <TimeWidget time={params.row.created} />;
|
|
},
|
|
},
|
|
{
|
|
field: "last_used",
|
|
headerName: "Last usage",
|
|
flex: 3,
|
|
renderCell(params) {
|
|
return (
|
|
<span
|
|
style={{
|
|
color:
|
|
params.row.last_used + params.row.max_inactivity < time()
|
|
? "red"
|
|
: undefined,
|
|
}}
|
|
>
|
|
<TimeWidget time={params.row.last_used} />
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
field: "max_inactivity",
|
|
headerName: "Max inactivity",
|
|
flex: 3,
|
|
renderCell(params) {
|
|
return (
|
|
<span
|
|
style={{
|
|
color:
|
|
params.row.last_used + params.row.max_inactivity < time()
|
|
? "red"
|
|
: undefined,
|
|
}}
|
|
>
|
|
<TimeWidget time={params.row.max_inactivity} isDuration />
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
field: "expiration",
|
|
headerName: "Expiration",
|
|
flex: 3,
|
|
renderCell(params) {
|
|
return (
|
|
<span
|
|
style={{
|
|
color:
|
|
params.row.expiration && params.row.expiration < time()
|
|
? "red"
|
|
: undefined,
|
|
}}
|
|
>
|
|
<TimeWidget time={params.row.expiration} showDate />
|
|
</span>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
field: "read_only",
|
|
headerName: "Read only",
|
|
flex: 2,
|
|
type: "boolean",
|
|
},
|
|
{
|
|
field: "actions",
|
|
type: "actions",
|
|
headerName: "Actions",
|
|
flex: 2,
|
|
cellClassName: "actions",
|
|
getActions: ({ row }) => {
|
|
return [
|
|
<GridActionsCellItem
|
|
key={row.id}
|
|
icon={<DeleteIcon />}
|
|
label="Delete"
|
|
onClick={handleDeleteClick(row)}
|
|
color="inherit"
|
|
/>,
|
|
];
|
|
},
|
|
},
|
|
];
|
|
|
|
if (p.list.length === 0)
|
|
return (
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
You do not have created any token yet!
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<DataGrid
|
|
style={{ flex: "1" }}
|
|
rows={p.list}
|
|
columns={columns}
|
|
autoPageSize
|
|
getRowId={(c) => c.id}
|
|
isCellEditable={() => false}
|
|
isRowSelectable={() => false}
|
|
/>
|
|
);
|
|
}
|