308 lines · 8.6 KB · TypeScript
RawDownload
import { useEffect, useState } from "react"; import {  getExecutorApiBaseUrl,  useExecutorMutation,  useWorkspaceRequestContext,} from "@executor/react";import type {  SecretStoreCreateFormProps,} from "@executor/react/plugins";import {  Alert,  Button,  Input,  Label,  Select,} from "@executor/react/plugins";import {  type OnePasswordDiscoverVaultsInput,  type OnePasswordDiscoverVaultsResult,  type OnePasswordStoreAuth,  type OnePasswordVault,} from "@executor/plugin-onepassword-shared";  export function OnePasswordSecretStoreCreateForm(  props: SecretStoreCreateFormProps,) {  const workspace = useWorkspaceRequestContext();  const [name, setName] = useState("");  const [authKind, setAuthKind] = useState<"desktop-app" | "service-account">(    "desktop-app",  );  const [accountName, setAccountName] = useState("");  const [vaultId, setVaultId] = useState("");  const [tokenSecretId, setTokenSecretId] = useState("");  const [discoveredVaults, setDiscoveredVaults] = useState<ReadonlyArray<OnePasswordVault>>(    [],  );  const [error, setError] = useState<string | null>(null);  const discoverVaultsMutation = useExecutorMutation<    OnePasswordDiscoverVaultsInput,    OnePasswordDiscoverVaultsResult  >(async (payload) => {    if (!workspace.enabled) {      throw new Error("Workspace is still loading.");    }     const response = await fetch(      `${getExecutorApiBaseUrl()}/v1/workspaces/${workspace.workspaceId}/plugins/onepassword/vaults/discover`,      {        method: "POST",        headers: {          "content-type": "application/json",        },        body: JSON.stringify(payload),      },    );     if (!response.ok) {      let message = "Failed loading 1Password vaults.";      try {        const responseError = await response.json() as {          message?: string;          details?: string;        };        message = responseError.message ?? responseError.details ?? message;      } catch {        // ignore response parsing errors      }      throw new Error(message);    }     return await response.json() as OnePasswordDiscoverVaultsResult;  });   useEffect(() => {    setDiscoveredVaults([]);    discoverVaultsMutation.reset();  }, [accountName, authKind, discoverVaultsMutation.reset, tokenSecretId]);   const onePasswordAuth = (): OnePasswordStoreAuth | null => {    if (authKind === "desktop-app") {      const trimmedAccountName = accountName.trim();      if (!trimmedAccountName) {        return null;      }       return {        kind: "desktop-app",        accountName: trimmedAccountName,      };    }     if (!tokenSecretId) {      return null;    }     return {      kind: "service-account",      tokenSecretRef: {        secretId: tokenSecretId as Extract<          OnePasswordStoreAuth,          { kind: "service-account" }        >["tokenSecretRef"]["secretId"],      },    };  };   const handleDiscoverVaults = async () => {    setError(null);     const auth = onePasswordAuth();    if (!auth) {      setError(        authKind === "desktop-app"          ? "Enter your 1Password account name before loading vaults."          : "Select a service-account token secret before loading vaults.",      );      return;    }     try {      const result = await discoverVaultsMutation.mutateAsync({ auth });      setDiscoveredVaults(result.vaults);      if (!vaultId.trim() && result.vaults.length > 0) {        setVaultId(result.vaults[0]!.id);      }      if (result.vaults.length === 0) {        setError("No accessible vaults were returned for this 1Password account.");      }    } catch (cause) {      setDiscoveredVaults([]);      setError(        cause instanceof Error ? cause.message : "Failed loading 1Password vaults.",      );    }  };   const handleSubmit = async () => {    setError(null);     const trimmedName = name.trim();    if (!trimmedName) {      setError("Store name is required.");      return;    }    if (authKind === "desktop-app" && !accountName.trim()) {      setError("Account name is required for desktop app auth.");      return;    }    if (!vaultId.trim()) {      setError("Vault ID is required.");      return;    }    if (authKind === "service-account" && !tokenSecretId) {      setError("Select a secret that contains the 1Password service account token.");      return;    }     try {      await props.onSubmit({        name: trimmedName,        config: {          vaultId: vaultId.trim(),          auth: onePasswordAuth()!,        },      });    } catch (cause) {      setError(        cause instanceof Error ? cause.message : "Failed creating secret store.",      );    }  };   return (    <div className="space-y-4">      {error && (        <Alert variant="destructive">          {error}        </Alert>      )}       <div className="grid gap-2">        <Label>Name</Label>        <Input          value={name}          onChange={(event) => setName(event.target.value)}          placeholder="Team 1Password"          autoFocus        />      </div>       <div className="grid gap-4 sm:grid-cols-2">        <div className="grid gap-2">          <Label>Auth method</Label>          <Select            value={authKind}            onChange={(event) =>              setAuthKind(event.target.value as "desktop-app" | "service-account")}          >            <option value="desktop-app">Desktop app</option>            <option value="service-account">Service account</option>          </Select>        </div>        <div className="flex items-end">          <Button            variant="outline"            type="button"            onClick={() => {              void handleDiscoverVaults();            }}            disabled={discoverVaultsMutation.status === "pending"}          >            {discoverVaultsMutation.status === "pending" ? "Loading..." : "Load vaults"}          </Button>        </div>      </div>       {authKind === "desktop-app" ? (        <div className="grid gap-2">          <Label>Account name or UUID</Label>          <Input            value={accountName}            onChange={(event) => setAccountName(event.target.value)}            placeholder="my.1password.com"          />          <div className="text-[11px] text-muted-foreground">            Use the account shown in the 1Password desktop app sidebar or the            account UUID from <code>op account list --format json</code>.          </div>        </div>      ) : (        <div className="grid gap-2">          <Label>Service Account Token Secret</Label>          <Select            value={tokenSecretId}            onChange={(event) => setTokenSecretId(event.target.value)}          >            <option value="">              {props.secrets.status === "ready"                ? "Select a secret"                : "Secrets are loading"}            </option>            {props.secrets.status === "ready" &&              props.secrets.data.map((secret) => (                <option key={secret.id} value={secret.id}>                  {secret.name ?? secret.id}                </option>              ))}          </Select>          <div className="text-[11px] text-muted-foreground">            Use this for remote or headless automation. Desktop app auth is better for            local Executor use.          </div>        </div>      )}       {discoveredVaults.length > 0 && (        <div className="grid gap-2">          <Label>Discovered vaults</Label>          <Select            value={vaultId}            onChange={(event) => setVaultId(event.target.value)}          >            {discoveredVaults.map((vault) => (              <option key={vault.id} value={vault.id}>                {vault.name}              </option>            ))}          </Select>          <div className="text-[11px] text-muted-foreground">            Pick from the vaults visible to this account, or override the id below.          </div>        </div>      )}       <div className="grid gap-2">        <Label>Vault ID</Label>        <Input          value={vaultId}          onChange={(event) => setVaultId(event.target.value)}          placeholder="vlt_..."          className="font-mono text-[12px]"        />      </div>       <div className="flex justify-end gap-2">        <Button          variant="outline"          type="button"          onClick={props.onCancel}        >          Cancel        </Button>        <Button          type="button"          onClick={() => {            void handleSubmit();          }}          disabled={props.isSubmitting}        >          {props.isSubmitting ? "Creating..." : "Create store"}        </Button>      </div>    </div>  );}