List the tokens from the WebUI
This commit is contained in:
		
							
								
								
									
										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/uuid": "^9.0.5",
 | 
			
		||||
        "@vitejs/plugin-react": "^4.2.1",
 | 
			
		||||
        "date-and-time": "^3.1.1",
 | 
			
		||||
        "filesize": "^10.0.12",
 | 
			
		||||
        "humanize-duration": "^3.29.0",
 | 
			
		||||
        "mui-file-input": "^4.0.4",
 | 
			
		||||
@@ -35,7 +36,7 @@
 | 
			
		||||
        "react-router-dom": "^6.15.0",
 | 
			
		||||
        "react-syntax-highlighter": "^15.5.0",
 | 
			
		||||
        "react-vnc": "^1.0.0",
 | 
			
		||||
        "typescript": "^4.1.6",
 | 
			
		||||
        "typescript": "^5.0.0",
 | 
			
		||||
        "uuid": "^9.0.1",
 | 
			
		||||
        "vite": "^5.0.8",
 | 
			
		||||
        "vite-tsconfig-paths": "^4.2.2",
 | 
			
		||||
@@ -8877,6 +8878,11 @@
 | 
			
		||||
        "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": {
 | 
			
		||||
      "version": "4.3.4",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
 | 
			
		||||
@@ -21890,15 +21896,15 @@
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "node_modules/typescript": {
 | 
			
		||||
      "version": "4.9.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
 | 
			
		||||
      "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
 | 
			
		||||
      "version": "5.4.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
 | 
			
		||||
      "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
 | 
			
		||||
      "bin": {
 | 
			
		||||
        "tsc": "bin/tsc",
 | 
			
		||||
        "tsserver": "bin/tsserver"
 | 
			
		||||
      },
 | 
			
		||||
      "engines": {
 | 
			
		||||
        "node": ">=4.2.0"
 | 
			
		||||
        "node": ">=14.17"
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    "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": {
 | 
			
		||||
      "version": "1.0.2",
 | 
			
		||||
      "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/uuid": "^9.0.5",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.2.1",
 | 
			
		||||
    "date-and-time": "^3.1.1",
 | 
			
		||||
    "filesize": "^10.0.12",
 | 
			
		||||
    "humanize-duration": "^3.29.0",
 | 
			
		||||
    "mui-file-input": "^4.0.4",
 | 
			
		||||
 
 | 
			
		||||
@@ -32,6 +32,7 @@ import {
 | 
			
		||||
  CreateNWFilterRoute,
 | 
			
		||||
  EditNWFilterRoute,
 | 
			
		||||
} from "./routes/EditNWFilterRoute";
 | 
			
		||||
import { TokensListRoute } from "./routes/TokensListRoute";
 | 
			
		||||
 | 
			
		||||
interface AuthContext {
 | 
			
		||||
  signedIn: boolean;
 | 
			
		||||
@@ -72,6 +73,8 @@ export function App() {
 | 
			
		||||
          <Route path="nwfilter/:uuid" element={<ViewNWFilterRoute />} />
 | 
			
		||||
          <Route path="nwfilter/:uuid/edit" element={<EditNWFilterRoute />} />
 | 
			
		||||
 | 
			
		||||
          <Route path="tokens" element={<TokensListRoute />} />
 | 
			
		||||
 | 
			
		||||
          <Route path="sysinfo" element={<SysInfoRoute />} />
 | 
			
		||||
          <Route path="*" element={<NotFoundRoute />} />
 | 
			
		||||
        </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 {
 | 
			
		||||
  mdiApi,
 | 
			
		||||
  mdiBoxShadow,
 | 
			
		||||
  mdiDisc,
 | 
			
		||||
  mdiHome,
 | 
			
		||||
@@ -72,6 +73,11 @@ export function BaseAuthenticatedPage(): React.ReactElement {
 | 
			
		||||
            uri="/iso"
 | 
			
		||||
            icon={<Icon path={mdiDisc} size={1} />}
 | 
			
		||||
          />
 | 
			
		||||
          <NavLink
 | 
			
		||||
            label="API tokens"
 | 
			
		||||
            uri="/tokens"
 | 
			
		||||
            icon={<Icon path={mdiApi} size={1} />}
 | 
			
		||||
          />
 | 
			
		||||
          <NavLink
 | 
			
		||||
            label="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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user