Start to build routes to create, view and edit API token
This commit is contained in:
		@@ -9,30 +9,35 @@ import "./App.css";
 | 
				
			|||||||
import { AuthApi } from "./api/AuthApi";
 | 
					import { AuthApi } from "./api/AuthApi";
 | 
				
			||||||
import { ServerApi } from "./api/ServerApi";
 | 
					import { ServerApi } from "./api/ServerApi";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  CreateNetworkRoute,
 | 
					  CreateApiTokenRoute,
 | 
				
			||||||
  EditNetworkRoute,
 | 
					  EditApiTokenRoute,
 | 
				
			||||||
} from "./routes/EditNetworkRoute";
 | 
					} from "./routes/EditAPITokenRoute";
 | 
				
			||||||
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";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  CreateNWFilterRoute,
 | 
					  CreateNWFilterRoute,
 | 
				
			||||||
  EditNWFilterRoute,
 | 
					  EditNWFilterRoute,
 | 
				
			||||||
} from "./routes/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 { 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 {
 | 
					interface AuthContext {
 | 
				
			||||||
  signedIn: boolean;
 | 
					  signedIn: boolean;
 | 
				
			||||||
@@ -74,6 +79,9 @@ export function App() {
 | 
				
			|||||||
          <Route path="nwfilter/:uuid/edit" element={<EditNWFilterRoute />} />
 | 
					          <Route path="nwfilter/:uuid/edit" element={<EditNWFilterRoute />} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Route path="tokens" element={<TokensListRoute />} />
 | 
					          <Route path="tokens" element={<TokensListRoute />} />
 | 
				
			||||||
 | 
					          <Route path="tokens/new" element={<CreateApiTokenRoute />} />
 | 
				
			||||||
 | 
					          <Route path="tokens/:id" element={<ViewApiTokenRoute />} />
 | 
				
			||||||
 | 
					          <Route path="tokens/:id/edit" element={<EditApiTokenRoute />} />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          <Route path="sysinfo" element={<SysInfoRoute />} />
 | 
					          <Route path="sysinfo" element={<SysInfoRoute />} />
 | 
				
			||||||
          <Route path="*" element={<NotFoundRoute />} />
 | 
					          <Route path="*" element={<NotFoundRoute />} />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -27,6 +27,8 @@ export interface ServerConstraints {
 | 
				
			|||||||
  nwfilter_comment_size: LenConstraint;
 | 
					  nwfilter_comment_size: LenConstraint;
 | 
				
			||||||
  nwfilter_priority: LenConstraint;
 | 
					  nwfilter_priority: LenConstraint;
 | 
				
			||||||
  nwfilter_selectors_count: LenConstraint;
 | 
					  nwfilter_selectors_count: LenConstraint;
 | 
				
			||||||
 | 
					  api_token_name_size: LenConstraint;
 | 
				
			||||||
 | 
					  api_token_description_size: LenConstraint;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface LenConstraint {
 | 
					export interface LenConstraint {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,30 @@ export function APITokenURL(t: APIToken, edit: boolean = false): string {
 | 
				
			|||||||
  return `/tokens/${t.id}${edit ? "/edit" : ""}`;
 | 
					  return `/tokens/${t.id}${edit ? "/edit" : ""}`;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface APITokenPrivateKey {
 | 
				
			||||||
 | 
					  alg: string;
 | 
				
			||||||
 | 
					  priv: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface CreatedAPIToken {
 | 
				
			||||||
 | 
					  token: APIToken;
 | 
				
			||||||
 | 
					  priv_key: APITokenPrivateKey;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class TokensApi {
 | 
					export class TokensApi {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Create a new API token
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async Create(n: APIToken): Promise<CreatedAPIToken> {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      await APIClient.exec({
 | 
				
			||||||
 | 
					        method: "POST",
 | 
				
			||||||
 | 
					        uri: "/tokens/create",
 | 
				
			||||||
 | 
					        jsonData: n,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    ).data;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  /**
 | 
					  /**
 | 
				
			||||||
   * Get the full list of tokens
 | 
					   * Get the full list of tokens
 | 
				
			||||||
   */
 | 
					   */
 | 
				
			||||||
@@ -33,4 +56,39 @@ export class TokensApi {
 | 
				
			|||||||
      })
 | 
					      })
 | 
				
			||||||
    ).data;
 | 
					    ).data;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Get the information about a single token
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async GetSingle(uuid: string): Promise<APIToken> {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      await APIClient.exec({
 | 
				
			||||||
 | 
					        method: "GET",
 | 
				
			||||||
 | 
					        uri: `/tokens/${uuid}`,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    ).data;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Update an existing API token information
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async Update(n: APIToken): Promise<void> {
 | 
				
			||||||
 | 
					    return (
 | 
				
			||||||
 | 
					      await APIClient.exec({
 | 
				
			||||||
 | 
					        method: "PATCH",
 | 
				
			||||||
 | 
					        uri: `/tokens/${n.id}`,
 | 
				
			||||||
 | 
					        jsonData: n,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    ).data;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * Delete an API token
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  static async Delete(n: APIToken): Promise<void> {
 | 
				
			||||||
 | 
					    await APIClient.exec({
 | 
				
			||||||
 | 
					      method: "DELETE",
 | 
				
			||||||
 | 
					      uri: `/tokens/${n.id}`,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										148
									
								
								virtweb_frontend/src/routes/EditAPITokenRoute.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										148
									
								
								virtweb_frontend/src/routes/EditAPITokenRoute.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,148 @@
 | 
				
			|||||||
 | 
					import { Button } from "@mui/material";
 | 
				
			||||||
 | 
					import React from "react";
 | 
				
			||||||
 | 
					import { useNavigate, useParams } from "react-router-dom";
 | 
				
			||||||
 | 
					import { APIToken, APITokenURL, TokensApi } from "../api/TokensApi";
 | 
				
			||||||
 | 
					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 [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!");
 | 
				
			||||||
 | 
					      // TODO : show a modal to give token information
 | 
				
			||||||
 | 
					      navigate(APITokenURL(res.token));
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      console.error(e);
 | 
				
			||||||
 | 
					      alert(`Failed to create API token!\n${e}`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return (
 | 
				
			||||||
 | 
					    <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 VisibilityIcon from "@mui/icons-material/Visibility";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Button,
 | 
					  Button,
 | 
				
			||||||
@@ -13,13 +12,13 @@ import {
 | 
				
			|||||||
  Typography,
 | 
					  Typography,
 | 
				
			||||||
} from "@mui/material";
 | 
					} from "@mui/material";
 | 
				
			||||||
import React from "react";
 | 
					import React from "react";
 | 
				
			||||||
 | 
					import { useNavigate } from "react-router-dom";
 | 
				
			||||||
import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi";
 | 
					import { NetworkApi, NetworkInfo, NetworkURL } from "../api/NetworksApi";
 | 
				
			||||||
import { AsyncWidget } from "../widgets/AsyncWidget";
 | 
					import { AsyncWidget } from "../widgets/AsyncWidget";
 | 
				
			||||||
import { RouterLink } from "../widgets/RouterLink";
 | 
					import { RouterLink } from "../widgets/RouterLink";
 | 
				
			||||||
import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
 | 
					import { VirtWebRouteContainer } from "../widgets/VirtWebRouteContainer";
 | 
				
			||||||
import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
 | 
					 | 
				
			||||||
import { useNavigate } from "react-router-dom";
 | 
					 | 
				
			||||||
import { NetworkHookStatusWidget } from "../widgets/net/NetworkHookStatusWidget";
 | 
					import { NetworkHookStatusWidget } from "../widgets/net/NetworkHookStatusWidget";
 | 
				
			||||||
 | 
					import { NetworkStatusWidget } from "../widgets/net/NetworkStatusWidget";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function NetworksListRoute(): React.ReactElement {
 | 
					export function NetworksListRoute(): React.ReactElement {
 | 
				
			||||||
  const [list, setList] = React.useState<NetworkInfo[] | undefined>();
 | 
					  const [list, setList] = React.useState<NetworkInfo[] | undefined>();
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -22,14 +22,16 @@ export function IPInput(p: {
 | 
				
			|||||||
export function IPInputWithMask(p: {
 | 
					export function IPInputWithMask(p: {
 | 
				
			||||||
  label: string;
 | 
					  label: string;
 | 
				
			||||||
  editable: boolean;
 | 
					  editable: boolean;
 | 
				
			||||||
 | 
					  ipAndMask?: string;
 | 
				
			||||||
  ip?: string;
 | 
					  ip?: string;
 | 
				
			||||||
  mask?: number;
 | 
					  mask?: number;
 | 
				
			||||||
  onValueChange?: (ip?: string, mask?: number) => void;
 | 
					  onValueChange?: (ip?: string, mask?: number, ipAndMask?: string) => void;
 | 
				
			||||||
  version: 4 | 6;
 | 
					  version: 4 | 6;
 | 
				
			||||||
}): React.ReactElement {
 | 
					}): React.ReactElement {
 | 
				
			||||||
  const showSlash = React.useRef(!!p.mask);
 | 
					  const showSlash = React.useRef(!!p.mask);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const currValue =
 | 
					  const currValue =
 | 
				
			||||||
 | 
					    p.ipAndMask ??
 | 
				
			||||||
    (p.ip ?? "") + (p.mask || showSlash.current ? "/" : "") + (p.mask ?? "");
 | 
					    (p.ip ?? "") + (p.mask || showSlash.current ? "/" : "") + (p.mask ?? "");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const { onValueChange, ...props } = p;
 | 
					  const { onValueChange, ...props } = p;
 | 
				
			||||||
@@ -38,7 +40,7 @@ export function IPInputWithMask(p: {
 | 
				
			|||||||
      onValueChange={(v) => {
 | 
					      onValueChange={(v) => {
 | 
				
			||||||
        showSlash.current = false;
 | 
					        showSlash.current = false;
 | 
				
			||||||
        if (!v) {
 | 
					        if (!v) {
 | 
				
			||||||
          onValueChange?.(undefined, undefined);
 | 
					          onValueChange?.(undefined, undefined, undefined);
 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,7 +54,11 @@ export function IPInputWithMask(p: {
 | 
				
			|||||||
          mask = sanitizeMask(p.version, split[1]);
 | 
					          mask = sanitizeMask(p.version, split[1]);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        onValueChange?.(ip, mask);
 | 
					        onValueChange?.(
 | 
				
			||||||
 | 
					          ip,
 | 
				
			||||||
 | 
					          mask,
 | 
				
			||||||
 | 
					          mask || showSlash.current ? `${ip}/${mask ?? ""}` : ip
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
      }}
 | 
					      }}
 | 
				
			||||||
      value={currValue}
 | 
					      value={currValue}
 | 
				
			||||||
      {...props}
 | 
					      {...props}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										216
									
								
								virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								virtweb_frontend/src/widgets/tokens/APITokenDetails.tsx
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,216 @@
 | 
				
			|||||||
 | 
					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 { IPInput, IPInputWithMask } from "../forms/IPInput";
 | 
				
			||||||
 | 
					import { ResAutostartInput } from "../forms/ResAutostartInput";
 | 
				
			||||||
 | 
					import { SelectInput } from "../forms/SelectInput";
 | 
				
			||||||
 | 
					import { TextInput } from "../forms/TextInput";
 | 
				
			||||||
 | 
					import { RadioGroupInput } from "../forms/RadioGroupInput";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 && <NetworkDetailsTabGeneral {...p} />}
 | 
				
			||||||
 | 
					      {/* todo: rights */}
 | 
				
			||||||
 | 
					      {currTab === TokenTab.Danger && <APITokenTabDanger {...p} />}
 | 
				
			||||||
 | 
					    </>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function NetworkDetailsTabGeneral(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 || p.token.ip_restriction) && (
 | 
				
			||||||
 | 
					          <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);
 | 
				
			||||||
 | 
					            }}
 | 
				
			||||||
 | 
					          />
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					        <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?.();
 | 
				
			||||||
 | 
					          }}
 | 
				
			||||||
 | 
					        />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        {/* TODO : remaining */}
 | 
				
			||||||
 | 
					      </EditSection>
 | 
				
			||||||
 | 
					    </Grid>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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>
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Reference in New Issue
	
	Block a user