MoneyMgr/moneymgr_web/src/routes/TokensRoute.tsx
2025-04-09 21:13:25 +02:00

279 lines
7.0 KiB
TypeScript

import AddIcon from "@mui/icons-material/Add";
import DeleteIcon from "@mui/icons-material/DeleteOutlined";
import RefreshIcon from "@mui/icons-material/Refresh";
import { Alert, AlertTitle, IconButton, Tooltip } from "@mui/material";
import {
DataGrid,
GridActionsCellItem,
GridColDef,
GridRowId,
} from "@mui/x-data-grid";
import { QRCodeCanvas } from "qrcode.react";
import React from "react";
import { APIClient } from "../api/ApiClient";
import { Token, TokenWithSecret, TokensApi } from "../api/TokensApi";
import { CreateTokenDialog } from "../dialogs/CreateTokenDialog";
import { useAlert } from "../hooks/context_providers/AlertDialogProvider";
import { useConfirm } from "../hooks/context_providers/ConfirmDialogProvider";
import { useSnackbar } from "../hooks/context_providers/SnackbarProvider";
import { AsyncWidget } from "../widgets/AsyncWidget";
import { CopyTextChip } from "../widgets/CopyTextChip";
import { MoneyMgrWebRouteContainer } from "../widgets/MoneyMgrWebRouteContainer";
import { TimeWidget } from "../widgets/TimeWidget";
export function TokensRoute(): React.ReactElement {
const count = React.useRef(0);
const [list, setList] = React.useState<Token[] | undefined>();
const [createdToken, setCreatedToken] = React.useState<
TokenWithSecret | undefined
>();
const [openCreateTokenDialog, setOpenCreateTokenDialog] =
React.useState(false);
const load = async () => {
setList(await TokensApi.GetList());
};
const reload = () => {
count.current += 1;
setList(undefined);
};
const onRequestCreateToken = () => {
setOpenCreateTokenDialog(true);
};
const closeCreateTokenDialog = () => {
setOpenCreateTokenDialog(false);
};
const onCreatedToken = (t: TokenWithSecret) => {
setOpenCreateTokenDialog(false);
setCreatedToken(t);
reload();
};
return (
<AsyncWidget
loadKey={count.current}
ready={list !== undefined}
load={load}
errMsg="Failed to load API token list!"
build={() => (
<>
<CreateTokenDialog
open={openCreateTokenDialog}
onClose={closeCreateTokenDialog}
onCreated={onCreatedToken}
/>
<TokensRouteInner
list={list!}
onReload={reload}
onRequestCreateToken={onRequestCreateToken}
createdToken={createdToken}
/>
</>
)}
/>
);
}
function TokensRouteInner(p: {
list: Token[];
onReload: () => void;
onRequestCreateToken: () => void;
createdToken?: TokenWithSecret;
}): React.ReactElement {
const confirm = useConfirm();
const alert = useAlert();
const snackbar = useSnackbar();
// Delete a token
const handleDeleteClick = (id: GridRowId) => async () => {
try {
const token = p.list.find((t) => t.id === id)!;
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[] = [
{ field: "id", headerName: "ID", flex: 1 },
{
field: "name",
headerName: "Name",
flex: 3,
},
{
field: "ip_net",
headerName: "IP restriction",
flex: 3,
renderCell(params) {
return (
params.row.ip_net ?? (
<span style={{ fontStyle: "italic" }}>Unrestricted</span>
)
);
},
},
{
field: "time_create",
headerName: "Creation",
flex: 3,
renderCell(params) {
return <TimeWidget time={params.row.time_create} />;
},
},
{
field: "time_used",
headerName: "Last usage",
flex: 3,
renderCell(params) {
return <TimeWidget time={params.row.time_used} />;
},
},
{
field: "max_inactivity",
headerName: "Max inactivity",
flex: 3,
renderCell(params) {
return <TimeWidget time={params.row.max_inactivity} isDuration />;
},
},
{
field: "read_only",
headerName: "Read only",
flex: 2,
type: "boolean",
},
{
field: "right_account",
headerName: "Account",
flex: 2,
type: "boolean",
},
{
field: "right_movement",
headerName: "Movement",
flex: 2,
type: "boolean",
},
{
field: "right_inbox",
headerName: "Inbox",
flex: 2,
type: "boolean",
},
{
field: "right_file",
headerName: "File",
flex: 2,
type: "boolean",
},
{
field: "right_auth",
headerName: "Auth",
flex: 2,
type: "boolean",
},
{
field: "actions",
type: "actions",
headerName: "Actions",
flex: 2,
cellClassName: "actions",
getActions: ({ id }) => {
return [
<GridActionsCellItem
key={id}
icon={<DeleteIcon />}
label="Delete"
onClick={handleDeleteClick(id)}
color="inherit"
/>,
];
},
},
];
return (
<MoneyMgrWebRouteContainer
label="API Tokens"
actions={
<span>
<Tooltip title="Create new token">
<IconButton onClick={p.onRequestCreateToken}>
<AddIcon />
</IconButton>
</Tooltip>
<Tooltip title="Refresh table">
<IconButton onClick={p.onReload}>
<RefreshIcon />
</IconButton>
</Tooltip>
</span>
}
>
{p.createdToken && <CreatedToken token={p.createdToken} />}
<DataGrid
style={{ flex: "1" }}
rows={p.list}
columns={columns}
autoPageSize
getRowId={(c) => c.id}
isCellEditable={() => false}
isRowSelectable={() => false}
/>
</MoneyMgrWebRouteContainer>
);
}
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={`moneymgr://api=${encodeURIComponent(
APIClient.backendURL()
)}&id=${p.token.id}&secret=${p.token.token}`}
/>
</div>
<br />
<em>Mobile App Qr Code</em>
</div>
<div>
<AlertTitle>Token successfully created</AlertTitle>
The API token was successfully created. Please note the following
information as they won't be available next.
<br />
Token ID: <CopyTextChip text={p.token.id.toString()} />
<br />
Token value: <CopyTextChip text={p.token.token} />
</div>
</div>
</Alert>
);
}