Add API tokens support #9
@ -14,6 +14,7 @@ mod rest_token {
|
|||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize)]
|
||||||
pub struct RestToken {
|
pub struct RestToken {
|
||||||
|
#[serde(flatten)]
|
||||||
token: Token,
|
token: Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
30
virtweb_frontend/package-lock.json
generated
30
virtweb_frontend/package-lock.json
generated
@ -27,6 +27,7 @@
|
|||||||
"@types/react-syntax-highlighter": "^15.5.11",
|
"@types/react-syntax-highlighter": "^15.5.11",
|
||||||
"@types/uuid": "^9.0.5",
|
"@types/uuid": "^9.0.5",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"date-and-time": "^3.1.1",
|
||||||
"filesize": "^10.0.12",
|
"filesize": "^10.0.12",
|
||||||
"humanize-duration": "^3.29.0",
|
"humanize-duration": "^3.29.0",
|
||||||
"mui-file-input": "^4.0.4",
|
"mui-file-input": "^4.0.4",
|
||||||
@ -35,7 +36,7 @@
|
|||||||
"react-router-dom": "^6.15.0",
|
"react-router-dom": "^6.15.0",
|
||||||
"react-syntax-highlighter": "^15.5.0",
|
"react-syntax-highlighter": "^15.5.0",
|
||||||
"react-vnc": "^1.0.0",
|
"react-vnc": "^1.0.0",
|
||||||
"typescript": "^4.1.6",
|
"typescript": "^5.0.0",
|
||||||
"uuid": "^9.0.1",
|
"uuid": "^9.0.1",
|
||||||
"vite": "^5.0.8",
|
"vite": "^5.0.8",
|
||||||
"vite-tsconfig-paths": "^4.2.2",
|
"vite-tsconfig-paths": "^4.2.2",
|
||||||
@ -8877,6 +8878,11 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/date-and-time": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-and-time/-/date-and-time-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-N9kstidT3P0VUk1iKOFilOZ6251r6iTUNx9M9kvgL2jqOk9mljWZUq5CjAtYwCnppWHbERk5YFQUrSbY7FQOpA=="
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
@ -21890,15 +21896,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "4.9.5",
|
"version": "5.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||||
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
|
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=4.2.0"
|
"node": ">=14.17"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/unbox-primitive": {
|
"node_modules/unbox-primitive": {
|
||||||
@ -22231,20 +22237,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-tsconfig-paths/node_modules/typescript": {
|
|
||||||
"version": "5.4.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz",
|
|
||||||
"integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
|
||||||
"tsc": "bin/tsc",
|
|
||||||
"tsserver": "bin/tsserver"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.17"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/w3c-hr-time": {
|
"node_modules/w3c-hr-time": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz",
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
"@types/react-syntax-highlighter": "^15.5.11",
|
"@types/react-syntax-highlighter": "^15.5.11",
|
||||||
"@types/uuid": "^9.0.5",
|
"@types/uuid": "^9.0.5",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
|
"date-and-time": "^3.1.1",
|
||||||
"filesize": "^10.0.12",
|
"filesize": "^10.0.12",
|
||||||
"humanize-duration": "^3.29.0",
|
"humanize-duration": "^3.29.0",
|
||||||
"mui-file-input": "^4.0.4",
|
"mui-file-input": "^4.0.4",
|
||||||
|
@ -32,6 +32,7 @@ import {
|
|||||||
CreateNWFilterRoute,
|
CreateNWFilterRoute,
|
||||||
EditNWFilterRoute,
|
EditNWFilterRoute,
|
||||||
} from "./routes/EditNWFilterRoute";
|
} from "./routes/EditNWFilterRoute";
|
||||||
|
import { TokensListRoute } from "./routes/TokensListRoute";
|
||||||
|
|
||||||
interface AuthContext {
|
interface AuthContext {
|
||||||
signedIn: boolean;
|
signedIn: boolean;
|
||||||
@ -72,6 +73,8 @@ export function App() {
|
|||||||
<Route path="nwfilter/:uuid" element={<ViewNWFilterRoute />} />
|
<Route path="nwfilter/:uuid" element={<ViewNWFilterRoute />} />
|
||||||
<Route path="nwfilter/:uuid/edit" element={<EditNWFilterRoute />} />
|
<Route path="nwfilter/:uuid/edit" element={<EditNWFilterRoute />} />
|
||||||
|
|
||||||
|
<Route path="tokens" element={<TokensListRoute />} />
|
||||||
|
|
||||||
<Route path="sysinfo" element={<SysInfoRoute />} />
|
<Route path="sysinfo" element={<SysInfoRoute />} />
|
||||||
<Route path="*" element={<NotFoundRoute />} />
|
<Route path="*" element={<NotFoundRoute />} />
|
||||||
</Route>
|
</Route>
|
||||||
|
36
virtweb_frontend/src/api/TokensApi.ts
Normal file
36
virtweb_frontend/src/api/TokensApi.ts
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { APIClient } from "./ApiClient";
|
||||||
|
|
||||||
|
export interface TokenRight {
|
||||||
|
verb: "POST" | "GET" | "PUT" | "DELETE" | "PATCH";
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface APIToken {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
created: number;
|
||||||
|
updated: number;
|
||||||
|
rights: TokenRight[];
|
||||||
|
last_used: number;
|
||||||
|
ip_restriction?: string;
|
||||||
|
max_inactivity?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function APITokenURL(t: APIToken, edit: boolean = false): string {
|
||||||
|
return `/tokens/${t.id}${edit ? "/edit" : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class TokensApi {
|
||||||
|
/**
|
||||||
|
* Get the full list of tokens
|
||||||
|
*/
|
||||||
|
static async GetList(): Promise<APIToken[]> {
|
||||||
|
return (
|
||||||
|
await APIClient.exec({
|
||||||
|
method: "GET",
|
||||||
|
uri: "/tokens/list",
|
||||||
|
})
|
||||||
|
).data;
|
||||||
|
}
|
||||||
|
}
|
118
virtweb_frontend/src/routes/TokensListRoute.tsx
Normal file
118
virtweb_frontend/src/routes/TokensListRoute.tsx
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { APIToken, APITokenURL, TokensApi } from "../api/TokensApi";
|
||||||
|
import { AsyncWidget } from "../widgets/AsyncWidget";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Paper,
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableContainer,
|
||||||
|
TableHead,
|
||||||
|
TableRow,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { RouterLink } from "../widgets/RouterLink";
|
||||||
|
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import VisibilityIcon from "@mui/icons-material/Visibility";
|
||||||
|
import { TimeWidget, timeDiff } from "../widgets/TimeWidget";
|
||||||
|
|
||||||
|
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="/tokens/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))}
|
||||||
|
>
|
||||||
|
<TableCell>{t.name}</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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
|
mdiApi,
|
||||||
mdiBoxShadow,
|
mdiBoxShadow,
|
||||||
mdiDisc,
|
mdiDisc,
|
||||||
mdiHome,
|
mdiHome,
|
||||||
@ -72,6 +73,11 @@ export function BaseAuthenticatedPage(): React.ReactElement {
|
|||||||
uri="/iso"
|
uri="/iso"
|
||||||
icon={<Icon path={mdiDisc} size={1} />}
|
icon={<Icon path={mdiDisc} size={1} />}
|
||||||
/>
|
/>
|
||||||
|
<NavLink
|
||||||
|
label="API tokens"
|
||||||
|
uri="/tokens"
|
||||||
|
icon={<Icon path={mdiApi} size={1} />}
|
||||||
|
/>
|
||||||
<NavLink
|
<NavLink
|
||||||
label="Sysinfo"
|
label="Sysinfo"
|
||||||
uri="/sysinfo"
|
uri="/sysinfo"
|
||||||
|
64
virtweb_frontend/src/widgets/TimeWidget.tsx
Normal file
64
virtweb_frontend/src/widgets/TimeWidget.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { Tooltip } from "@mui/material";
|
||||||
|
import date from "date-and-time";
|
||||||
|
|
||||||
|
export function formatDate(time: number): string {
|
||||||
|
const t = new Date();
|
||||||
|
t.setTime(1000 * time);
|
||||||
|
return date.format(t, "DD/MM/YYYY HH:mm:ss");
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timeDiff(a: number, b: number): string {
|
||||||
|
let diff = b - a;
|
||||||
|
|
||||||
|
if (diff === 0) return "now";
|
||||||
|
if (diff === 1) return "1 second";
|
||||||
|
|
||||||
|
if (diff < 60) {
|
||||||
|
return `${diff} seconds`;
|
||||||
|
}
|
||||||
|
|
||||||
|
diff = Math.floor(diff / 60);
|
||||||
|
|
||||||
|
if (diff === 1) return "1 minute";
|
||||||
|
if (diff < 24) {
|
||||||
|
return `${diff} minutes`;
|
||||||
|
}
|
||||||
|
|
||||||
|
diff = Math.floor(diff / 60);
|
||||||
|
|
||||||
|
if (diff === 1) return "1 hour";
|
||||||
|
if (diff < 24) {
|
||||||
|
return `${diff} hours`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffDays = Math.floor(diff / 24);
|
||||||
|
|
||||||
|
if (diffDays === 1) return "1 day";
|
||||||
|
if (diffDays < 31) {
|
||||||
|
return `${diffDays} days`;
|
||||||
|
}
|
||||||
|
|
||||||
|
diff = Math.floor(diffDays / 31);
|
||||||
|
|
||||||
|
if (diff < 12) {
|
||||||
|
return `${diff} month`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diffYears = Math.floor(diffDays / 365);
|
||||||
|
|
||||||
|
if (diffYears === 1) return "1 year";
|
||||||
|
return `${diffYears} years`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function timeDiffFromNow(time: number): string {
|
||||||
|
return timeDiff(time, Math.floor(new Date().getTime() / 1000));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TimeWidget(p: { time?: number }): React.ReactElement {
|
||||||
|
if (!p.time) return <></>;
|
||||||
|
return (
|
||||||
|
<Tooltip title={formatDate(p.time)}>
|
||||||
|
<span>{timeDiffFromNow(p.time)}</span>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user