Add API tokens support (#9)
	
		
			
	
		
	
	
		
	
		
			All checks were successful
		
		
	
	
		
			
				
	
				continuous-integration/drone/push Build is passing
				
			
		
		
	
	
				
					
				
			
		
			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:
		@@ -9,29 +9,35 @@ import "./App.css";
 | 
			
		||||
import { AuthApi } from "./api/AuthApi";
 | 
			
		||||
import { ServerApi } from "./api/ServerApi";
 | 
			
		||||
import {
 | 
			
		||||
  CreateNetworkRoute,
 | 
			
		||||
  EditNetworkRoute,
 | 
			
		||||
} from "./routes/EditNetworkRoute";
 | 
			
		||||
import { CreateVMRoute, EditVMRoute } from "./routes/EditVMRoute";
 | 
			
		||||
import { IsoFilesRoute } from "./routes/IsoFilesRoute";
 | 
			
		||||
import { NetworksListRoute } from "./routes/NetworksListRoute";
 | 
			
		||||
import { NotFoundRoute } from "./routes/NotFound";
 | 
			
		||||
import { SysInfoRoute } from "./routes/SysInfoRoute";
 | 
			
		||||
import { VMListRoute } from "./routes/VMListRoute";
 | 
			
		||||
import { VMRoute } from "./routes/VMRoute";
 | 
			
		||||
import { VNCRoute } from "./routes/VNCRoute";
 | 
			
		||||
import { LoginRoute } from "./routes/auth/LoginRoute";
 | 
			
		||||
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
 | 
			
		||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
 | 
			
		||||
import { BaseLoginPage } from "./widgets/BaseLoginPage";
 | 
			
		||||
import { ViewNetworkRoute } from "./routes/ViewNetworkRoute";
 | 
			
		||||
import { HomeRoute } from "./routes/HomeRoute";
 | 
			
		||||
import { NetworkFiltersListRoute } from "./routes/NetworkFiltersListRoute";
 | 
			
		||||
import { ViewNWFilterRoute } from "./routes/ViewNWFilterRoute";
 | 
			
		||||
  CreateApiTokenRoute,
 | 
			
		||||
  EditApiTokenRoute,
 | 
			
		||||
} from "./routes/EditAPITokenRoute";
 | 
			
		||||
import {
 | 
			
		||||
  CreateNWFilterRoute,
 | 
			
		||||
  EditNWFilterRoute,
 | 
			
		||||
} from "./routes/EditNWFilterRoute";
 | 
			
		||||
import {
 | 
			
		||||
  CreateNetworkRoute,
 | 
			
		||||
  EditNetworkRoute,
 | 
			
		||||
} from "./routes/EditNetworkRoute";
 | 
			
		||||
import { CreateVMRoute, EditVMRoute } from "./routes/EditVMRoute";
 | 
			
		||||
import { HomeRoute } from "./routes/HomeRoute";
 | 
			
		||||
import { IsoFilesRoute } from "./routes/IsoFilesRoute";
 | 
			
		||||
import { NetworkFiltersListRoute } from "./routes/NetworkFiltersListRoute";
 | 
			
		||||
import { NetworksListRoute } from "./routes/NetworksListRoute";
 | 
			
		||||
import { NotFoundRoute } from "./routes/NotFound";
 | 
			
		||||
import { SysInfoRoute } from "./routes/SysInfoRoute";
 | 
			
		||||
import { TokensListRoute } from "./routes/TokensListRoute";
 | 
			
		||||
import { VMListRoute } from "./routes/VMListRoute";
 | 
			
		||||
import { VMRoute } from "./routes/VMRoute";
 | 
			
		||||
import { VNCRoute } from "./routes/VNCRoute";
 | 
			
		||||
import { ViewApiTokenRoute } from "./routes/ViewApiTokenRoute";
 | 
			
		||||
import { ViewNWFilterRoute } from "./routes/ViewNWFilterRoute";
 | 
			
		||||
import { ViewNetworkRoute } from "./routes/ViewNetworkRoute";
 | 
			
		||||
import { LoginRoute } from "./routes/auth/LoginRoute";
 | 
			
		||||
import { OIDCCbRoute } from "./routes/auth/OIDCCbRoute";
 | 
			
		||||
import { BaseAuthenticatedPage } from "./widgets/BaseAuthenticatedPage";
 | 
			
		||||
import { BaseLoginPage } from "./widgets/BaseLoginPage";
 | 
			
		||||
 | 
			
		||||
interface AuthContext {
 | 
			
		||||
  signedIn: boolean;
 | 
			
		||||
@@ -72,6 +78,11 @@ export function App() {
 | 
			
		||||
          <Route path="nwfilter/:uuid" element={<ViewNWFilterRoute />} />
 | 
			
		||||
          <Route path="nwfilter/:uuid/edit" element={<EditNWFilterRoute />} />
 | 
			
		||||
 | 
			
		||||
          <Route path="tokens" element={<TokensListRoute />} />
 | 
			
		||||
          <Route path="token/new" element={<CreateApiTokenRoute />} />
 | 
			
		||||
          <Route path="token/:id" element={<ViewApiTokenRoute />} />
 | 
			
		||||
          <Route path="token/:id/edit" element={<EditApiTokenRoute />} />
 | 
			
		||||
 | 
			
		||||
          <Route path="sysinfo" element={<SysInfoRoute />} />
 | 
			
		||||
          <Route path="*" element={<NotFoundRoute />} />
 | 
			
		||||
        </Route>
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,9 @@ export interface ServerConstraints {
 | 
			
		||||
  nwfilter_comment_size: LenConstraint;
 | 
			
		||||
  nwfilter_priority: LenConstraint;
 | 
			
		||||
  nwfilter_selectors_count: LenConstraint;
 | 
			
		||||
  api_token_name_size: LenConstraint;
 | 
			
		||||
  api_token_description_size: LenConstraint;
 | 
			
		||||
  api_token_right_path_size: LenConstraint;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface LenConstraint {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										102
									
								
								virtweb_frontend/src/api/TokensApi.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								virtweb_frontend/src/api/TokensApi.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,102 @@
 | 
			
		||||
import { time } from "../utils/DateUtils";
 | 
			
		||||
import { APIClient } from "./ApiClient";
 | 
			
		||||
 | 
			
		||||
export type RightVerb = "POST" | "GET" | "PUT" | "DELETE" | "PATCH";
 | 
			
		||||
 | 
			
		||||
export interface TokenRight {
 | 
			
		||||
  verb: RightVerb;
 | 
			
		||||
  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 `/token/${t.id}${edit ? "/edit" : ""}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ExpiredAPIToken(t: APIToken): boolean {
 | 
			
		||||
  if (!t.max_inactivity) return false;
 | 
			
		||||
  return t.last_used + t.max_inactivity < time();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface APITokenPrivateKey {
 | 
			
		||||
  alg: string;
 | 
			
		||||
  priv: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CreatedAPIToken {
 | 
			
		||||
  token: APIToken;
 | 
			
		||||
  priv_key: APITokenPrivateKey;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class TokensApi {
 | 
			
		||||
  /**
 | 
			
		||||
   * Create a new API token
 | 
			
		||||
   */
 | 
			
		||||
  static async Create(n: APIToken): Promise<CreatedAPIToken> {
 | 
			
		||||
    return (
 | 
			
		||||
      await APIClient.exec({
 | 
			
		||||
        method: "POST",
 | 
			
		||||
        uri: "/token/create",
 | 
			
		||||
        jsonData: n,
 | 
			
		||||
      })
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the full list of tokens
 | 
			
		||||
   */
 | 
			
		||||
  static async GetList(): Promise<APIToken[]> {
 | 
			
		||||
    return (
 | 
			
		||||
      await APIClient.exec({
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        uri: "/token/list",
 | 
			
		||||
      })
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the information about a single token
 | 
			
		||||
   */
 | 
			
		||||
  static async GetSingle(uuid: string): Promise<APIToken> {
 | 
			
		||||
    return (
 | 
			
		||||
      await APIClient.exec({
 | 
			
		||||
        method: "GET",
 | 
			
		||||
        uri: `/token/${uuid}`,
 | 
			
		||||
      })
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Update an existing API token information
 | 
			
		||||
   */
 | 
			
		||||
  static async Update(n: APIToken): Promise<void> {
 | 
			
		||||
    return (
 | 
			
		||||
      await APIClient.exec({
 | 
			
		||||
        method: "PATCH",
 | 
			
		||||
        uri: `/token/${n.id}`,
 | 
			
		||||
        jsonData: n,
 | 
			
		||||
      })
 | 
			
		||||
    ).data;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Delete an API token
 | 
			
		||||
   */
 | 
			
		||||
  static async Delete(n: APIToken): Promise<void> {
 | 
			
		||||
    await APIClient.exec({
 | 
			
		||||
      method: "DELETE",
 | 
			
		||||
      uri: `/token/${n.id}`,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								virtweb_frontend/src/dialogs/CreatedTokenDialog.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Dialog,
 | 
			
		||||
  DialogActions,
 | 
			
		||||
  DialogContent,
 | 
			
		||||
  DialogTitle,
 | 
			
		||||
  Typography,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { APITokenURL, CreatedAPIToken } from "../api/TokensApi";
 | 
			
		||||
import { CopyToClipboard } from "../widgets/CopyToClipboard";
 | 
			
		||||
import { InlineCode } from "../widgets/InlineCode";
 | 
			
		||||
 | 
			
		||||
export function CreatedTokenDialog(p: {
 | 
			
		||||
  createdToken: CreatedAPIToken;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const close = () => {
 | 
			
		||||
    navigate(APITokenURL(p.createdToken.token));
 | 
			
		||||
  };
 | 
			
		||||
  return (
 | 
			
		||||
    <Dialog open>
 | 
			
		||||
      <DialogTitle>Token successfully created</DialogTitle>
 | 
			
		||||
      <DialogContent>
 | 
			
		||||
        <Typography>
 | 
			
		||||
          Your token was successfully created. You need now to copy the private
 | 
			
		||||
          key, as it will be technically impossible to recover it after closing
 | 
			
		||||
          this dialog.
 | 
			
		||||
        </Typography>
 | 
			
		||||
 | 
			
		||||
        <InfoBlock label="Token ID" value={p.createdToken.token.id} />
 | 
			
		||||
        <InfoBlock label="Key algorithm" value={p.createdToken.priv_key.alg} />
 | 
			
		||||
        <InfoBlock label="Private key" value={p.createdToken.priv_key.priv} />
 | 
			
		||||
      </DialogContent>
 | 
			
		||||
      <DialogActions>
 | 
			
		||||
        <Button onClick={close} color="error">
 | 
			
		||||
          I copied the key, close this dialog
 | 
			
		||||
        </Button>
 | 
			
		||||
      </DialogActions>
 | 
			
		||||
    </Dialog>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function InfoBlock(
 | 
			
		||||
  p: React.PropsWithChildren<{ label: string; value: string }>
 | 
			
		||||
): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
      style={{ display: "flex", flexDirection: "column", margin: "20px 10px" }}
 | 
			
		||||
    >
 | 
			
		||||
      <Typography variant="overline">{p.label}</Typography>
 | 
			
		||||
      <CopyToClipboard content={p.value}>
 | 
			
		||||
        <InlineCode>{p.value}</InlineCode>
 | 
			
		||||
      </CopyToClipboard>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										161
									
								
								virtweb_frontend/src/routes/EditAPITokenRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										161
									
								
								virtweb_frontend/src/routes/EditAPITokenRoute.tsx
									
									
									
									
									
										Normal 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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -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>();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										126
									
								
								virtweb_frontend/src/routes/TokensListRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								virtweb_frontend/src/routes/TokensListRoute.tsx
									
									
									
									
									
										Normal 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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								virtweb_frontend/src/routes/ViewApiTokenRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								virtweb_frontend/src/routes/ViewApiTokenRoute.tsx
									
									
									
									
									
										Normal 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>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								virtweb_frontend/src/widgets/InlineCode.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								virtweb_frontend/src/widgets/InlineCode.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
			
		||||
export function InlineCode(p: React.PropsWithChildren): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <code
 | 
			
		||||
      style={{
 | 
			
		||||
        display: "inline-block",
 | 
			
		||||
        backgroundColor: "black",
 | 
			
		||||
        color: "white",
 | 
			
		||||
        wordBreak: "break-all",
 | 
			
		||||
        wordWrap: "break-word",
 | 
			
		||||
        whiteSpace: "pre-wrap",
 | 
			
		||||
        padding: "0px 7px",
 | 
			
		||||
        borderRadius: "5px",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {p.children}
 | 
			
		||||
    </code>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								virtweb_frontend/src/widgets/TimeWidget.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								virtweb_frontend/src/widgets/TimeWidget.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,65 @@
 | 
			
		||||
import { Tooltip } from "@mui/material";
 | 
			
		||||
import date from "date-and-time";
 | 
			
		||||
import { time } from "../utils/DateUtils";
 | 
			
		||||
 | 
			
		||||
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(t: number): string {
 | 
			
		||||
  return timeDiff(t, time());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function TimeWidget(p: { time?: number }): React.ReactElement {
 | 
			
		||||
  if (!p.time) return <></>;
 | 
			
		||||
  return (
 | 
			
		||||
    <Tooltip title={formatDate(p.time)}>
 | 
			
		||||
      <span>{timeDiffFromNow(p.time)}</span>
 | 
			
		||||
    </Tooltip>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -22,14 +22,16 @@ export function IPInput(p: {
 | 
			
		||||
export function IPInputWithMask(p: {
 | 
			
		||||
  label: string;
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  ipAndMask?: string;
 | 
			
		||||
  ip?: string;
 | 
			
		||||
  mask?: number;
 | 
			
		||||
  onValueChange?: (ip?: string, mask?: number) => void;
 | 
			
		||||
  onValueChange?: (ip?: string, mask?: number, ipAndMask?: string) => void;
 | 
			
		||||
  version: 4 | 6;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const showSlash = React.useRef(!!p.mask);
 | 
			
		||||
 | 
			
		||||
  const currValue =
 | 
			
		||||
    p.ipAndMask ??
 | 
			
		||||
    (p.ip ?? "") + (p.mask || showSlash.current ? "/" : "") + (p.mask ?? "");
 | 
			
		||||
 | 
			
		||||
  const { onValueChange, ...props } = p;
 | 
			
		||||
@@ -38,7 +40,7 @@ export function IPInputWithMask(p: {
 | 
			
		||||
      onValueChange={(v) => {
 | 
			
		||||
        showSlash.current = false;
 | 
			
		||||
        if (!v) {
 | 
			
		||||
          onValueChange?.(undefined, undefined);
 | 
			
		||||
          onValueChange?.(undefined, undefined, undefined);
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -52,7 +54,11 @@ export function IPInputWithMask(p: {
 | 
			
		||||
          mask = sanitizeMask(p.version, split[1]);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        onValueChange?.(ip, mask);
 | 
			
		||||
        onValueChange?.(
 | 
			
		||||
          ip,
 | 
			
		||||
          mask,
 | 
			
		||||
          mask || showSlash.current ? `${ip}/${mask ?? ""}` : ip
 | 
			
		||||
        );
 | 
			
		||||
      }}
 | 
			
		||||
      value={currValue}
 | 
			
		||||
      {...props}
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ export interface SelectOption {
 | 
			
		||||
export function SelectInput(p: {
 | 
			
		||||
  value?: string;
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  label: string;
 | 
			
		||||
  label?: string;
 | 
			
		||||
  options: SelectOption[];
 | 
			
		||||
  onValueChange: (o?: string) => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
@@ -29,7 +29,7 @@ export function SelectInput(p: {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <FormControl fullWidth variant="standard" style={{ marginBottom: "15px" }}>
 | 
			
		||||
      <InputLabel>{p.label}</InputLabel>
 | 
			
		||||
      {p.label && <InputLabel>{p.label}</InputLabel>}
 | 
			
		||||
      <Select
 | 
			
		||||
        value={p.value ?? ""}
 | 
			
		||||
        label={p.label}
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,7 @@ import React, { PropsWithChildren } from "react";
 | 
			
		||||
import { NetworkHookStatus, ServerApi } from "../../api/ServerApi";
 | 
			
		||||
import { AsyncWidget } from "../AsyncWidget";
 | 
			
		||||
import { CopyToClipboard } from "../CopyToClipboard";
 | 
			
		||||
import { InlineCode } from "../InlineCode";
 | 
			
		||||
 | 
			
		||||
export function NetworkHookStatusWidget(p: {
 | 
			
		||||
  hiddenIfInstalled: boolean;
 | 
			
		||||
@@ -72,25 +73,6 @@ function NetworkHookStatusWidgetInner(p: {
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function InlineCode(p: PropsWithChildren): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <code
 | 
			
		||||
      style={{
 | 
			
		||||
        display: "inline-block",
 | 
			
		||||
        backgroundColor: "black",
 | 
			
		||||
        color: "white",
 | 
			
		||||
        wordBreak: "break-all",
 | 
			
		||||
        wordWrap: "break-word",
 | 
			
		||||
        whiteSpace: "pre-wrap",
 | 
			
		||||
        padding: "0px 7px",
 | 
			
		||||
        borderRadius: "5px",
 | 
			
		||||
      }}
 | 
			
		||||
    >
 | 
			
		||||
      {p.children}
 | 
			
		||||
    </code>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CodeBlock(p: PropsWithChildren): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <pre
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										256
									
								
								virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										256
									
								
								virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,256 @@
 | 
			
		||||
import { Button, Grid } from "@mui/material";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { useNavigate } from "react-router-dom";
 | 
			
		||||
import { NWFilter, NWFilterApi } from "../../api/NWFilterApi";
 | 
			
		||||
import { NetworkApi, NetworkInfo } from "../../api/NetworksApi";
 | 
			
		||||
import { ServerApi } from "../../api/ServerApi";
 | 
			
		||||
import { APIToken, TokensApi } from "../../api/TokensApi";
 | 
			
		||||
import { VMApi, VMInfo } from "../../api/VMApi";
 | 
			
		||||
import { useAlert } from "../../hooks/providers/AlertDialogProvider";
 | 
			
		||||
import { useConfirm } from "../../hooks/providers/ConfirmDialogProvider";
 | 
			
		||||
import { useSnackbar } from "../../hooks/providers/SnackbarProvider";
 | 
			
		||||
import { AsyncWidget } from "../AsyncWidget";
 | 
			
		||||
import { TabsWidget } from "../TabsWidget";
 | 
			
		||||
import { EditSection } from "../forms/EditSection";
 | 
			
		||||
import { IPInputWithMask } from "../forms/IPInput";
 | 
			
		||||
import { RadioGroupInput } from "../forms/RadioGroupInput";
 | 
			
		||||
import { TextInput } from "../forms/TextInput";
 | 
			
		||||
import { TokenRawRightsEditor } from "./TokenRawRightsEditor";
 | 
			
		||||
import { TokenRightsEditor } from "./TokenRightsEditor";
 | 
			
		||||
 | 
			
		||||
const SECS_PER_DAY = 3600 * 24;
 | 
			
		||||
 | 
			
		||||
export enum TokenWidgetStatus {
 | 
			
		||||
  Create,
 | 
			
		||||
  Read,
 | 
			
		||||
  Update,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface DetailsProps {
 | 
			
		||||
  token: APIToken;
 | 
			
		||||
  status: TokenWidgetStatus;
 | 
			
		||||
  onChange?: () => void;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function APITokenDetails(p: DetailsProps): React.ReactElement {
 | 
			
		||||
  const [vms, setVMs] = React.useState<VMInfo[]>();
 | 
			
		||||
  const [networks, setNetworks] = React.useState<NetworkInfo[]>();
 | 
			
		||||
  const [nwFilters, setNetworkFilters] = React.useState<NWFilter[]>();
 | 
			
		||||
  const [tokens, setTokens] = React.useState<APIToken[]>();
 | 
			
		||||
 | 
			
		||||
  const load = async () => {
 | 
			
		||||
    setVMs(await VMApi.GetList());
 | 
			
		||||
    setNetworks(await NetworkApi.GetList());
 | 
			
		||||
    setNetworkFilters(await NWFilterApi.GetList());
 | 
			
		||||
    setTokens(await TokensApi.GetList());
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <AsyncWidget
 | 
			
		||||
      loadKey={"1"}
 | 
			
		||||
      load={load}
 | 
			
		||||
      errMsg="Failed to load some system entities!"
 | 
			
		||||
      build={() => (
 | 
			
		||||
        <APITokenDetailsInner
 | 
			
		||||
          vms={vms!}
 | 
			
		||||
          networks={networks!}
 | 
			
		||||
          nwFilters={nwFilters!}
 | 
			
		||||
          tokens={tokens!}
 | 
			
		||||
          {...p}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
    />
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum TokenTab {
 | 
			
		||||
  General = 0,
 | 
			
		||||
  Rights,
 | 
			
		||||
  RawRights,
 | 
			
		||||
  Danger,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DetailsInnerProps = DetailsProps & {
 | 
			
		||||
  vms: VMInfo[];
 | 
			
		||||
  networks: NetworkInfo[];
 | 
			
		||||
  nwFilters: NWFilter[];
 | 
			
		||||
  tokens: APIToken[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function APITokenDetailsInner(p: DetailsInnerProps): React.ReactElement {
 | 
			
		||||
  const [currTab, setCurrTab] = React.useState(TokenTab.General);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <TabsWidget
 | 
			
		||||
        currTab={currTab}
 | 
			
		||||
        onTabChange={setCurrTab}
 | 
			
		||||
        options={[
 | 
			
		||||
          { label: "General", value: TokenTab.General, visible: true },
 | 
			
		||||
          {
 | 
			
		||||
            label: "Rights",
 | 
			
		||||
            value: TokenTab.Rights,
 | 
			
		||||
            visible: true,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            label: "Raw rights",
 | 
			
		||||
            value: TokenTab.RawRights,
 | 
			
		||||
            visible: true,
 | 
			
		||||
          },
 | 
			
		||||
 | 
			
		||||
          {
 | 
			
		||||
            label: "Danger zone",
 | 
			
		||||
            value: TokenTab.Danger,
 | 
			
		||||
            color: "red",
 | 
			
		||||
            visible: p.status === TokenWidgetStatus.Read,
 | 
			
		||||
          },
 | 
			
		||||
        ]}
 | 
			
		||||
      />
 | 
			
		||||
      {currTab === TokenTab.General && <APITokenTabGeneral {...p} />}
 | 
			
		||||
      {currTab === TokenTab.Rights && <APITokenRights {...p} />}
 | 
			
		||||
      {currTab === TokenTab.RawRights && <APITokenRawRights {...p} />}
 | 
			
		||||
      {currTab === TokenTab.Danger && <APITokenTabDanger {...p} />}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function APITokenTabGeneral(p: DetailsInnerProps): React.ReactElement {
 | 
			
		||||
  const [ipVersion, setIpVersion] = React.useState<4 | 6>(
 | 
			
		||||
    (p.token.ip_restriction ?? "").includes(":") ? 6 : 4
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid container spacing={2}>
 | 
			
		||||
      {/* Metadata section */}
 | 
			
		||||
      <EditSection title="Metadata">
 | 
			
		||||
        {p.status !== TokenWidgetStatus.Create && (
 | 
			
		||||
          <TextInput label="UUID" editable={false} value={p.token.id} />
 | 
			
		||||
        )}
 | 
			
		||||
 | 
			
		||||
        <TextInput
 | 
			
		||||
          label="Name"
 | 
			
		||||
          editable={p.status === TokenWidgetStatus.Create}
 | 
			
		||||
          value={p.token.name}
 | 
			
		||||
          onValueChange={(v) => {
 | 
			
		||||
            p.token.name = v ?? "";
 | 
			
		||||
            p.onChange?.();
 | 
			
		||||
          }}
 | 
			
		||||
          size={ServerApi.Config.constraints.api_token_name_size}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <TextInput
 | 
			
		||||
          label="Description"
 | 
			
		||||
          editable={p.status === TokenWidgetStatus.Create}
 | 
			
		||||
          value={p.token.description}
 | 
			
		||||
          onValueChange={(v) => {
 | 
			
		||||
            p.token.description = v ?? "";
 | 
			
		||||
            p.onChange?.();
 | 
			
		||||
          }}
 | 
			
		||||
          multiline={true}
 | 
			
		||||
          size={ServerApi.Config.constraints.api_token_description_size}
 | 
			
		||||
        />
 | 
			
		||||
      </EditSection>
 | 
			
		||||
 | 
			
		||||
      <EditSection title="General settings">
 | 
			
		||||
        {p.status === TokenWidgetStatus.Create && (
 | 
			
		||||
          <RadioGroupInput
 | 
			
		||||
            {...p}
 | 
			
		||||
            editable={p.status === TokenWidgetStatus.Create}
 | 
			
		||||
            options={[
 | 
			
		||||
              { label: "IPv4", value: "4" },
 | 
			
		||||
              { label: "IPv6", value: "6" },
 | 
			
		||||
            ]}
 | 
			
		||||
            value={ipVersion.toString()}
 | 
			
		||||
            onValueChange={(v) => {
 | 
			
		||||
              setIpVersion(Number(v) as any);
 | 
			
		||||
            }}
 | 
			
		||||
            label="Token IP restriction version"
 | 
			
		||||
          />
 | 
			
		||||
        )}
 | 
			
		||||
        <IPInputWithMask
 | 
			
		||||
          {...p}
 | 
			
		||||
          label="Token IP network restriction"
 | 
			
		||||
          ipAndMask={p.token.ip_restriction}
 | 
			
		||||
          editable={p.status === TokenWidgetStatus.Create}
 | 
			
		||||
          version={ipVersion}
 | 
			
		||||
          onValueChange={(_ip, _mask, ipAndMask) => {
 | 
			
		||||
            p.token.ip_restriction = ipAndMask;
 | 
			
		||||
            p.onChange?.();
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <TextInput
 | 
			
		||||
          editable={p.status === TokenWidgetStatus.Create}
 | 
			
		||||
          label="Max inactivity of tokens (days)"
 | 
			
		||||
          type="number"
 | 
			
		||||
          value={
 | 
			
		||||
            p.token.max_inactivity
 | 
			
		||||
              ? Math.floor(p.token.max_inactivity / SECS_PER_DAY).toString()
 | 
			
		||||
              : ""
 | 
			
		||||
          }
 | 
			
		||||
          onValueChange={(v) => {
 | 
			
		||||
            const secs = Number(v ?? "0") * SECS_PER_DAY;
 | 
			
		||||
            p.token.max_inactivity = secs === 0 ? undefined : secs;
 | 
			
		||||
            p.onChange?.();
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
      </EditSection>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function APITokenRights(p: DetailsInnerProps): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ padding: "30px" }}>
 | 
			
		||||
      <TokenRightsEditor
 | 
			
		||||
        {...p}
 | 
			
		||||
        editable={p.status !== TokenWidgetStatus.Read}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function APITokenRawRights(p: DetailsInnerProps): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <div style={{ padding: "30px" }}>
 | 
			
		||||
      <TokenRawRightsEditor
 | 
			
		||||
        {...p}
 | 
			
		||||
        editable={p.status !== TokenWidgetStatus.Read}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function APITokenTabDanger(p: DetailsInnerProps): React.ReactElement {
 | 
			
		||||
  const confirm = useConfirm();
 | 
			
		||||
  const snackbar = useSnackbar();
 | 
			
		||||
  const alert = useAlert();
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
 | 
			
		||||
  const requestDelete = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (
 | 
			
		||||
        !(await confirm(
 | 
			
		||||
          "Do you really want to delete this API token?",
 | 
			
		||||
          `Delete API token ${p.token.name}`,
 | 
			
		||||
          "Delete"
 | 
			
		||||
        ))
 | 
			
		||||
      )
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
      await TokensApi.Delete(p.token);
 | 
			
		||||
 | 
			
		||||
      navigate("/tokens");
 | 
			
		||||
      snackbar("The API token was successfully deleted!");
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      console.error(e);
 | 
			
		||||
      alert(`Failed to delete the API token!\n${e}`);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Button color="error" onClick={requestDelete}>
 | 
			
		||||
      Delete this API token
 | 
			
		||||
    </Button>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										111
									
								
								virtweb_frontend/src/widgets/tokens/TokenRawRightsEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								virtweb_frontend/src/widgets/tokens/TokenRawRightsEditor.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
			
		||||
import AddIcon from "@mui/icons-material/Add";
 | 
			
		||||
import DeleteIcon from "@mui/icons-material/Delete";
 | 
			
		||||
import {
 | 
			
		||||
  IconButton,
 | 
			
		||||
  Paper,
 | 
			
		||||
  Table,
 | 
			
		||||
  TableBody,
 | 
			
		||||
  TableCell,
 | 
			
		||||
  TableContainer,
 | 
			
		||||
  TableHead,
 | 
			
		||||
  TableRow,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Typography,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import { ServerApi } from "../../api/ServerApi";
 | 
			
		||||
import { APIToken } from "../../api/TokensApi";
 | 
			
		||||
import { SelectInput } from "../forms/SelectInput";
 | 
			
		||||
import { TextInput } from "../forms/TextInput";
 | 
			
		||||
 | 
			
		||||
export function TokenRawRightsEditor(p: {
 | 
			
		||||
  token: APIToken;
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  onChange?: () => void;
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  const addRule = () => {
 | 
			
		||||
    p.token.rights.push({ path: "/api/", verb: "GET" });
 | 
			
		||||
    p.onChange?.();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const deleteRule = (id: number) => {
 | 
			
		||||
    p.token.rights.splice(id, 1);
 | 
			
		||||
    p.onChange?.();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <TableContainer component={Paper}>
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          padding: "10px 10px 0px 10px",
 | 
			
		||||
          display: "flex",
 | 
			
		||||
          justifyContent: "space-between",
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Typography variant="h5">Raw rights</Typography>
 | 
			
		||||
        <div>
 | 
			
		||||
          {p.editable && (
 | 
			
		||||
            <Tooltip title="Add a new right rule">
 | 
			
		||||
              <IconButton onClick={addRule}>
 | 
			
		||||
                <AddIcon />
 | 
			
		||||
              </IconButton>
 | 
			
		||||
            </Tooltip>
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <Table>
 | 
			
		||||
        <TableHead>
 | 
			
		||||
          <TableRow>
 | 
			
		||||
            <TableCell>Verb</TableCell>
 | 
			
		||||
            <TableCell>URI</TableCell>
 | 
			
		||||
            {p.editable && <TableCell>Actions</TableCell>}
 | 
			
		||||
          </TableRow>
 | 
			
		||||
        </TableHead>
 | 
			
		||||
        <TableBody>
 | 
			
		||||
          {p.token.rights.map((r, num) => (
 | 
			
		||||
            <TableRow key={num} hover>
 | 
			
		||||
              <TableCell style={{ width: "100px" }}>
 | 
			
		||||
                <SelectInput
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  value={r.verb}
 | 
			
		||||
                  onValueChange={(v) => {
 | 
			
		||||
                    r.verb = v as any;
 | 
			
		||||
                    p.onChange?.();
 | 
			
		||||
                  }}
 | 
			
		||||
                  options={[
 | 
			
		||||
                    { value: "GET" },
 | 
			
		||||
                    { value: "POST" },
 | 
			
		||||
                    { value: "PATCH" },
 | 
			
		||||
                    { value: "PUT" },
 | 
			
		||||
                    { value: "DELETE" },
 | 
			
		||||
                  ]}
 | 
			
		||||
                />
 | 
			
		||||
              </TableCell>
 | 
			
		||||
              <TableCell>
 | 
			
		||||
                <TextInput
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  value={r.path}
 | 
			
		||||
                  onValueChange={(v) => {
 | 
			
		||||
                    r.path = v ?? "";
 | 
			
		||||
                    p.onChange?.();
 | 
			
		||||
                  }}
 | 
			
		||||
                  checkValue={(v) => v.startsWith("/api/")}
 | 
			
		||||
                  size={ServerApi.Config.constraints.api_token_right_path_size}
 | 
			
		||||
                />
 | 
			
		||||
              </TableCell>
 | 
			
		||||
              {p.editable && (
 | 
			
		||||
                <TableCell style={{ width: "100px" }}>
 | 
			
		||||
                  <IconButton onClick={() => deleteRule(num)}>
 | 
			
		||||
                    <Tooltip title="Remove the rule">
 | 
			
		||||
                      <DeleteIcon />
 | 
			
		||||
                    </Tooltip>
 | 
			
		||||
                  </IconButton>
 | 
			
		||||
                </TableCell>
 | 
			
		||||
              )}
 | 
			
		||||
            </TableRow>
 | 
			
		||||
          ))}
 | 
			
		||||
        </TableBody>
 | 
			
		||||
      </Table>
 | 
			
		||||
    </TableContainer>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										690
									
								
								virtweb_frontend/src/widgets/tokens/TokenRightsEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										690
									
								
								virtweb_frontend/src/widgets/tokens/TokenRightsEditor.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,690 @@
 | 
			
		||||
import {
 | 
			
		||||
  Checkbox,
 | 
			
		||||
  FormControlLabel,
 | 
			
		||||
  Paper,
 | 
			
		||||
  Table,
 | 
			
		||||
  TableBody,
 | 
			
		||||
  TableCell,
 | 
			
		||||
  TableHead,
 | 
			
		||||
  TableRow,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Typography,
 | 
			
		||||
} from "@mui/material";
 | 
			
		||||
import React from "react";
 | 
			
		||||
import { NWFilter } from "../../api/NWFilterApi";
 | 
			
		||||
import { NetworkInfo } from "../../api/NetworksApi";
 | 
			
		||||
import { ServerApi } from "../../api/ServerApi";
 | 
			
		||||
import { APIToken, TokenRight } from "../../api/TokensApi";
 | 
			
		||||
import { VMInfo } from "../../api/VMApi";
 | 
			
		||||
 | 
			
		||||
export function TokenRightsEditor(p: {
 | 
			
		||||
  token: APIToken;
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  onChange?: () => void;
 | 
			
		||||
  vms: VMInfo[];
 | 
			
		||||
  networks: NetworkInfo[];
 | 
			
		||||
  nwFilters: NWFilter[];
 | 
			
		||||
  tokens: APIToken[];
 | 
			
		||||
}): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {/* Virtual machines */}
 | 
			
		||||
      <RightsSection label="Virtual machines">
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "POST", path: "/api/vm/create" }}
 | 
			
		||||
          label="Create a new virtual machine"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/vm/list" }}
 | 
			
		||||
          label="Get list of virtual machines"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/vnc" }}
 | 
			
		||||
          label="Establish VNC connection"
 | 
			
		||||
        />
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
 | 
			
		||||
      <RightsSection label="VM configuration management">
 | 
			
		||||
        <Table size="small">
 | 
			
		||||
          <TableHead>
 | 
			
		||||
            <TableRow>
 | 
			
		||||
              <TableCell>VM name</TableCell>
 | 
			
		||||
              <TableCell align="center">Get definition</TableCell>
 | 
			
		||||
              <TableCell align="center">Update</TableCell>
 | 
			
		||||
              <TableCell align="center">Delete</TableCell>
 | 
			
		||||
              <TableCell align="center">Get XML definition</TableCell>
 | 
			
		||||
              <TableCell align="center">Get autostart</TableCell>
 | 
			
		||||
              <TableCell align="center">Set autostart</TableCell>
 | 
			
		||||
            </TableRow>
 | 
			
		||||
          </TableHead>
 | 
			
		||||
          <TableBody>
 | 
			
		||||
            {/* All VM operations */}
 | 
			
		||||
            <TableRow hover>
 | 
			
		||||
              <TableCell>
 | 
			
		||||
                <i>All</i>
 | 
			
		||||
              </TableCell>
 | 
			
		||||
              <CellRight {...p} right={{ verb: "GET", path: "/api/vm/*" }} />
 | 
			
		||||
              <CellRight {...p} right={{ verb: "PUT", path: "/api/vm/*" }} />
 | 
			
		||||
              <CellRight {...p} right={{ verb: "DELETE", path: "/api/vm/*" }} />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/vm/*/src" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/vm/*/autostart" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "PUT", path: "/api/vm/*/autostart" }}
 | 
			
		||||
              />
 | 
			
		||||
            </TableRow>
 | 
			
		||||
 | 
			
		||||
            {/* Per VM operations */}
 | 
			
		||||
            {p.vms.map((v, n) => (
 | 
			
		||||
              <TableRow hover key={n}>
 | 
			
		||||
                <TableCell>{v.name}</TableCell>
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "PUT", path: `/api/vm/${v.uuid}` }}
 | 
			
		||||
                  parent={{ verb: "PUT", path: "/api/vm/*" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "DELETE", path: `/api/vm/${v.uuid}` }}
 | 
			
		||||
                  parent={{ verb: "DELETE", path: "/api/vm/*" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}/src` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*/src" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}/autostart` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*/autostart" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "PUT", path: `/api/vm/${v.uuid}/autostart` }}
 | 
			
		||||
                  parent={{ verb: "PUT", path: "/api/vm/*/autostart" }}
 | 
			
		||||
                />
 | 
			
		||||
              </TableRow>
 | 
			
		||||
            ))}
 | 
			
		||||
          </TableBody>
 | 
			
		||||
        </Table>
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
 | 
			
		||||
      <RightsSection label="VM maintenance">
 | 
			
		||||
        <Table size="small">
 | 
			
		||||
          <TableHead>
 | 
			
		||||
            <TableRow>
 | 
			
		||||
              <TableCell>VM name</TableCell>
 | 
			
		||||
              <TableCell align="center">Get state</TableCell>
 | 
			
		||||
              <TableCell align="center">Start</TableCell>
 | 
			
		||||
              <TableCell align="center">Shutdown</TableCell>
 | 
			
		||||
              <TableCell align="center">Kill</TableCell>
 | 
			
		||||
              <TableCell align="center">Reset</TableCell>
 | 
			
		||||
              <TableCell align="center">Suspend</TableCell>
 | 
			
		||||
              <TableCell align="center">Resume</TableCell>
 | 
			
		||||
              <TableCell align="center">Screenshot</TableCell>
 | 
			
		||||
              <TableCell align="center">VNC token</TableCell>
 | 
			
		||||
            </TableRow>
 | 
			
		||||
          </TableHead>
 | 
			
		||||
          <TableBody>
 | 
			
		||||
            {/* All VM operations */}
 | 
			
		||||
            <TableRow hover>
 | 
			
		||||
              <TableCell>
 | 
			
		||||
                <i>All</i>
 | 
			
		||||
              </TableCell>
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/vm/*/state" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/vm/*/start" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/vm/*/shutdown" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/vm/*/kill" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/vm/*/reset" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/vm/*/suspend" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/vm/*/resume" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/vm/*/screenshot" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/vm/*/vnc" }}
 | 
			
		||||
              />
 | 
			
		||||
            </TableRow>
 | 
			
		||||
 | 
			
		||||
            {/* Per VM operations */}
 | 
			
		||||
            {p.vms.map((v, n) => (
 | 
			
		||||
              <TableRow hover key={n}>
 | 
			
		||||
                <TableCell>{v.name}</TableCell>
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}/state` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*/state" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}/start` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*/start" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}/shutdown` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*/shutdown" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}/kill` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*/kill" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}/reset` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*/reset" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}/suspend` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*/suspend" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}/resume` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*/resume" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}/screenshot` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*/screenshot" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/vm/${v.uuid}/vnc` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/vm/*/vnc" }}
 | 
			
		||||
                />
 | 
			
		||||
              </TableRow>
 | 
			
		||||
            ))}
 | 
			
		||||
          </TableBody>
 | 
			
		||||
        </Table>
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
 | 
			
		||||
      {/* Networks */}
 | 
			
		||||
      <RightsSection label="Networks">
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "POST", path: "/api/network/create" }}
 | 
			
		||||
          label="Create a new network"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/network/list" }}
 | 
			
		||||
          label="Get list of networks"
 | 
			
		||||
        />
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
 | 
			
		||||
      {/* Networks management */}
 | 
			
		||||
      <RightsSection label="Networks management">
 | 
			
		||||
        <Table size="small">
 | 
			
		||||
          <TableHead>
 | 
			
		||||
            <TableRow>
 | 
			
		||||
              <TableCell>Network name</TableCell>
 | 
			
		||||
              <TableCell align="center">Get definition</TableCell>
 | 
			
		||||
              <TableCell align="center">Update</TableCell>
 | 
			
		||||
              <TableCell align="center">Delete</TableCell>
 | 
			
		||||
              <TableCell align="center">Get XML definition</TableCell>
 | 
			
		||||
              <TableCell align="center">Get autostart</TableCell>
 | 
			
		||||
              <TableCell align="center">Set autostart</TableCell>
 | 
			
		||||
              <TableCell align="center">Get status</TableCell>
 | 
			
		||||
              <TableCell align="center">Start</TableCell>
 | 
			
		||||
              <TableCell align="center">Stop</TableCell>
 | 
			
		||||
            </TableRow>
 | 
			
		||||
          </TableHead>
 | 
			
		||||
          <TableBody>
 | 
			
		||||
            {/* All networks operations */}
 | 
			
		||||
            <TableRow hover>
 | 
			
		||||
              <TableCell>
 | 
			
		||||
                <i>All</i>
 | 
			
		||||
              </TableCell>
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/network/*" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "PUT", path: "/api/network/*" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "DELETE", path: "/api/network/*" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/network/*/src" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/network/*/autostart" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "PUT", path: "/api/network/*/autostart" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/network/*/status" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/network/*/start" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/network/*/stop" }}
 | 
			
		||||
              />
 | 
			
		||||
            </TableRow>
 | 
			
		||||
 | 
			
		||||
            {/* Per network operations */}
 | 
			
		||||
            {p.networks.map((v, n) => (
 | 
			
		||||
              <TableRow hover key={n}>
 | 
			
		||||
                <TableCell>{v.name}</TableCell>
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/network/${v.uuid}` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/network/*" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "PUT", path: `/api/network/${v.uuid}` }}
 | 
			
		||||
                  parent={{ verb: "PUT", path: "/api/network/*" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "DELETE", path: `/api/network/${v.uuid}` }}
 | 
			
		||||
                  parent={{ verb: "DELETE", path: "/api/network/*" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/network/${v.uuid}/src` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/network/*/src" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{
 | 
			
		||||
                    verb: "GET",
 | 
			
		||||
                    path: `/api/network/${v.uuid}/autostart`,
 | 
			
		||||
                  }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/network/*/autostart" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{
 | 
			
		||||
                    verb: "PUT",
 | 
			
		||||
                    path: `/api/network/${v.uuid}/autostart`,
 | 
			
		||||
                  }}
 | 
			
		||||
                  parent={{ verb: "PUT", path: "/api/network/*/autostart" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{
 | 
			
		||||
                    verb: "GET",
 | 
			
		||||
                    path: `/api/network/${v.uuid}/status`,
 | 
			
		||||
                  }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/network/*/status" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{
 | 
			
		||||
                    verb: "GET",
 | 
			
		||||
                    path: `/api/network/${v.uuid}/start`,
 | 
			
		||||
                  }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/network/*/start" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{
 | 
			
		||||
                    verb: "GET",
 | 
			
		||||
                    path: `/api/network/${v.uuid}/stop`,
 | 
			
		||||
                  }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/network/*/stop" }}
 | 
			
		||||
                />
 | 
			
		||||
              </TableRow>
 | 
			
		||||
            ))}
 | 
			
		||||
          </TableBody>
 | 
			
		||||
        </Table>
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
 | 
			
		||||
      {/* Network filters */}
 | 
			
		||||
      <RightsSection label="Network filters">
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "POST", path: "/api/nwfilter/create" }}
 | 
			
		||||
          label="Create a new network filter"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/nwfilter/list" }}
 | 
			
		||||
          label="Get list of network filters"
 | 
			
		||||
        />
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
 | 
			
		||||
      {/* Networks filters management */}
 | 
			
		||||
      <RightsSection label="Networks filters management">
 | 
			
		||||
        <Table size="small">
 | 
			
		||||
          <TableHead>
 | 
			
		||||
            <TableRow>
 | 
			
		||||
              <TableCell>Network filter name</TableCell>
 | 
			
		||||
              <TableCell align="center">Get definition</TableCell>
 | 
			
		||||
              <TableCell align="center">Update</TableCell>
 | 
			
		||||
              <TableCell align="center">Delete</TableCell>
 | 
			
		||||
              <TableCell align="center">Get XML definition</TableCell>
 | 
			
		||||
            </TableRow>
 | 
			
		||||
          </TableHead>
 | 
			
		||||
          <TableBody>
 | 
			
		||||
            {/* All networks filters operations */}
 | 
			
		||||
            <TableRow hover>
 | 
			
		||||
              <TableCell>
 | 
			
		||||
                <i>All</i>
 | 
			
		||||
              </TableCell>
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/nwfilter/*" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "PUT", path: "/api/nwfilter/*" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "DELETE", path: "/api/nwfilter/*" }}
 | 
			
		||||
              />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "GET", path: "/api/nwfilter/*/src" }}
 | 
			
		||||
              />
 | 
			
		||||
            </TableRow>
 | 
			
		||||
 | 
			
		||||
            {/* Per network filter operations */}
 | 
			
		||||
            {p.nwFilters.map((v, n) => (
 | 
			
		||||
              <TableRow hover key={n}>
 | 
			
		||||
                <TableCell>{v.name}</TableCell>
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/nwfilter/${v.uuid}` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/nwfilter/*" }}
 | 
			
		||||
                />
 | 
			
		||||
                {ServerApi.Config.builtin_nwfilter_rules.includes(v.name!) ? (
 | 
			
		||||
                  <TableCell></TableCell>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <CellRight
 | 
			
		||||
                    {...p}
 | 
			
		||||
                    right={{ verb: "PUT", path: `/api/nwfilter/${v.uuid}` }}
 | 
			
		||||
                    parent={{ verb: "PUT", path: "/api/nwfilter/*" }}
 | 
			
		||||
                  />
 | 
			
		||||
                )}
 | 
			
		||||
                {ServerApi.Config.builtin_nwfilter_rules.includes(v.name!) ? (
 | 
			
		||||
                  <TableCell></TableCell>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <CellRight
 | 
			
		||||
                    {...p}
 | 
			
		||||
                    right={{ verb: "DELETE", path: `/api/nwfilter/${v.uuid}` }}
 | 
			
		||||
                    parent={{ verb: "DELETE", path: "/api/nwfilter/*" }}
 | 
			
		||||
                  />
 | 
			
		||||
                )}
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/nwfilter/${v.uuid}/src` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/nwfilter/*/src" }}
 | 
			
		||||
                />
 | 
			
		||||
              </TableRow>
 | 
			
		||||
            ))}
 | 
			
		||||
          </TableBody>
 | 
			
		||||
        </Table>
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
 | 
			
		||||
      {/* API tokens */}
 | 
			
		||||
      <RightsSection label="API tokens">
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "POST", path: "/api/token/create" }}
 | 
			
		||||
          label="Create a new API token"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/token/list" }}
 | 
			
		||||
          label="Get list of API tokens"
 | 
			
		||||
        />
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
 | 
			
		||||
      {/* API tokens management */}
 | 
			
		||||
      <RightsSection label="API tokens management">
 | 
			
		||||
        <Table size="small">
 | 
			
		||||
          <TableHead>
 | 
			
		||||
            <TableRow>
 | 
			
		||||
              <TableCell>API token name</TableCell>
 | 
			
		||||
              <TableCell align="center">Get</TableCell>
 | 
			
		||||
              <TableCell align="center">Update</TableCell>
 | 
			
		||||
              <TableCell align="center">Delete</TableCell>
 | 
			
		||||
            </TableRow>
 | 
			
		||||
          </TableHead>
 | 
			
		||||
          <TableBody>
 | 
			
		||||
            {/* All API tokens operations */}
 | 
			
		||||
            <TableRow hover>
 | 
			
		||||
              <TableCell>
 | 
			
		||||
                <i>All</i>
 | 
			
		||||
              </TableCell>
 | 
			
		||||
              <CellRight {...p} right={{ verb: "GET", path: "/api/token/*" }} />
 | 
			
		||||
              <CellRight {...p} right={{ verb: "PUT", path: "/api/token/*" }} />
 | 
			
		||||
              <CellRight
 | 
			
		||||
                {...p}
 | 
			
		||||
                right={{ verb: "DELETE", path: "/api/token/*" }}
 | 
			
		||||
              />
 | 
			
		||||
            </TableRow>
 | 
			
		||||
 | 
			
		||||
            {/* Per API token operations */}
 | 
			
		||||
            {p.tokens.map((v, n) => (
 | 
			
		||||
              <TableRow hover key={n}>
 | 
			
		||||
                <TableCell>{v.name}</TableCell>
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "GET", path: `/api/token/${v.id}` }}
 | 
			
		||||
                  parent={{ verb: "GET", path: "/api/token/*" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "PUT", path: `/api/token/${v.id}` }}
 | 
			
		||||
                  parent={{ verb: "PUT", path: "/api/token/*" }}
 | 
			
		||||
                />
 | 
			
		||||
                <CellRight
 | 
			
		||||
                  {...p}
 | 
			
		||||
                  right={{ verb: "DELETE", path: `/api/token/${v.id}` }}
 | 
			
		||||
                  parent={{ verb: "DELETE", path: "/api/token/*" }}
 | 
			
		||||
                />
 | 
			
		||||
              </TableRow>
 | 
			
		||||
            ))}
 | 
			
		||||
          </TableBody>
 | 
			
		||||
        </Table>
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
 | 
			
		||||
      {/* ISO files */}
 | 
			
		||||
      <RightsSection label="ISO files">
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "POST", path: "/api/iso/upload" }}
 | 
			
		||||
          label="Upload a new ISO file"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "POST", path: "/api/iso/upload_from_url" }}
 | 
			
		||||
          label="Upload a new ISO file from a given URL"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/iso/list" }}
 | 
			
		||||
          label="Get the list of ISO files"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/iso/*" }}
 | 
			
		||||
          label="Download ISO files"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "DELETE", path: "/api/iso/*" }}
 | 
			
		||||
          label="Delete ISO files"
 | 
			
		||||
        />
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
 | 
			
		||||
      {/* Server general information */}
 | 
			
		||||
      <RightsSection label="Server">
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/server/static_config" }}
 | 
			
		||||
          label="Get static server configuration"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/server/info" }}
 | 
			
		||||
          label="Get server information"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/server/network_hook_status" }}
 | 
			
		||||
          label="Get network hook status"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/server/number_vcpus" }}
 | 
			
		||||
          label="Get number of vCPU"
 | 
			
		||||
        />
 | 
			
		||||
        <RouteRight
 | 
			
		||||
          {...p}
 | 
			
		||||
          right={{ verb: "GET", path: "/api/server/networks" }}
 | 
			
		||||
          label="Get list of network cards"
 | 
			
		||||
        />
 | 
			
		||||
      </RightsSection>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function RightsSection(
 | 
			
		||||
  p: React.PropsWithChildren<{ label: string }>
 | 
			
		||||
): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <Paper style={{ padding: "20px", margin: "10px" }}>
 | 
			
		||||
      <Typography variant="h5">{p.label}</Typography>
 | 
			
		||||
      {p.children}
 | 
			
		||||
    </Paper>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface RightOpts {
 | 
			
		||||
  right: TokenRight;
 | 
			
		||||
  label?: string;
 | 
			
		||||
  editable: boolean;
 | 
			
		||||
  token: APIToken;
 | 
			
		||||
  onChange?: () => void;
 | 
			
		||||
  parent?: TokenRight;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function CellRight(p: RightOpts): React.ReactElement {
 | 
			
		||||
  return (
 | 
			
		||||
    <TableCell align="center">
 | 
			
		||||
      <RouteRight {...p} />
 | 
			
		||||
    </TableCell>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function RouteRight(p: RightOpts): React.ReactElement {
 | 
			
		||||
  const rightIndex = p.token.rights.findIndex(
 | 
			
		||||
    (r) => r.verb === p.right.verb && r.path === p.right.path
 | 
			
		||||
  );
 | 
			
		||||
  const activated = rightIndex !== -1;
 | 
			
		||||
 | 
			
		||||
  const parentActivated =
 | 
			
		||||
    !!p.parent &&
 | 
			
		||||
    p.token.rights.findIndex(
 | 
			
		||||
      (r) => r.verb === p.parent?.verb && r.path === p.parent?.path
 | 
			
		||||
    ) !== -1;
 | 
			
		||||
 | 
			
		||||
  const toggle = (a: boolean) => {
 | 
			
		||||
    if (a) {
 | 
			
		||||
      p.token.rights.push(p.right);
 | 
			
		||||
    } else {
 | 
			
		||||
      p.token.rights.splice(rightIndex, 1);
 | 
			
		||||
    }
 | 
			
		||||
    p.onChange?.();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Tooltip
 | 
			
		||||
        title={`${p.right.verb} ${p.right.path}`}
 | 
			
		||||
        arrow
 | 
			
		||||
        placement="left"
 | 
			
		||||
        slotProps={{
 | 
			
		||||
          popper: {
 | 
			
		||||
            modifiers: [
 | 
			
		||||
              {
 | 
			
		||||
                name: "offset",
 | 
			
		||||
                options: {
 | 
			
		||||
                  offset: [0, -14],
 | 
			
		||||
                },
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        {p.label ? (
 | 
			
		||||
          <FormControlLabel
 | 
			
		||||
            control={
 | 
			
		||||
              <Checkbox
 | 
			
		||||
                checked={activated || parentActivated}
 | 
			
		||||
                disabled={!p.editable || parentActivated}
 | 
			
		||||
                onChange={(_e, a) => toggle(a)}
 | 
			
		||||
              />
 | 
			
		||||
            }
 | 
			
		||||
            label={p.label}
 | 
			
		||||
          />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <span>
 | 
			
		||||
            <Checkbox
 | 
			
		||||
              checked={activated || parentActivated}
 | 
			
		||||
              disabled={!p.editable || parentActivated}
 | 
			
		||||
              onChange={(_e, a) => toggle(a)}
 | 
			
		||||
            />
 | 
			
		||||
          </span>
 | 
			
		||||
        )}
 | 
			
		||||
      </Tooltip>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user