[Frontend] Add SECRET credential type for storing generic sensitive values (#4258)
This commit is contained in:
@@ -500,29 +500,51 @@ export type CreditCardCredentialApiResponse = {
|
||||
brand: string;
|
||||
};
|
||||
|
||||
export type SecretCredentialResponse = {
|
||||
secret_label?: string | null;
|
||||
};
|
||||
|
||||
export type CredentialApiResponse = {
|
||||
credential_id: string;
|
||||
credential: PasswordCredentialApiResponse | CreditCardCredentialApiResponse;
|
||||
credential_type: "password" | "credit_card";
|
||||
credential:
|
||||
| PasswordCredentialApiResponse
|
||||
| CreditCardCredentialApiResponse
|
||||
| SecretCredentialResponse;
|
||||
credential_type: "password" | "credit_card" | "secret";
|
||||
name: string;
|
||||
};
|
||||
|
||||
export function isPasswordCredential(
|
||||
credential: PasswordCredentialApiResponse | CreditCardCredentialApiResponse,
|
||||
credential:
|
||||
| PasswordCredentialApiResponse
|
||||
| CreditCardCredentialApiResponse
|
||||
| SecretCredentialResponse,
|
||||
): credential is PasswordCredentialApiResponse {
|
||||
return "username" in credential;
|
||||
}
|
||||
|
||||
export function isCreditCardCredential(
|
||||
credential: PasswordCredentialApiResponse | CreditCardCredentialApiResponse,
|
||||
credential:
|
||||
| PasswordCredentialApiResponse
|
||||
| CreditCardCredentialApiResponse
|
||||
| SecretCredentialResponse,
|
||||
): credential is CreditCardCredentialApiResponse {
|
||||
return "last_four" in credential;
|
||||
}
|
||||
|
||||
export function isSecretCredential(
|
||||
credential:
|
||||
| PasswordCredentialApiResponse
|
||||
| CreditCardCredentialApiResponse
|
||||
| SecretCredentialResponse,
|
||||
): credential is SecretCredentialResponse {
|
||||
return !("username" in credential) && !("last_four" in credential);
|
||||
}
|
||||
|
||||
export type CreateCredentialRequest = {
|
||||
name: string;
|
||||
credential_type: "password" | "credit_card";
|
||||
credential: PasswordCredential | CreditCardCredential;
|
||||
credential_type: "password" | "credit_card" | "secret";
|
||||
credential: PasswordCredential | CreditCardCredential | SecretCredential;
|
||||
};
|
||||
|
||||
export type PasswordCredential = {
|
||||
@@ -542,6 +564,11 @@ export type CreditCardCredential = {
|
||||
card_holder_name: string;
|
||||
};
|
||||
|
||||
export type SecretCredential = {
|
||||
secret_value: string;
|
||||
secret_label?: string | null;
|
||||
};
|
||||
|
||||
export const OtpType = {
|
||||
Totp: "totp",
|
||||
MagicLink: "magic_link",
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { isPasswordCredential } from "@/api/types";
|
||||
import {
|
||||
CredentialApiResponse,
|
||||
isCreditCardCredential,
|
||||
isPasswordCredential,
|
||||
isSecretCredential,
|
||||
} from "@/api/types";
|
||||
import { DeleteCredentialButton } from "./DeleteCredentialButton";
|
||||
import { CredentialApiResponse } from "@/api/types";
|
||||
|
||||
type Props = {
|
||||
credential: CredentialApiResponse;
|
||||
};
|
||||
|
||||
function CredentialItem({ credential }: Props) {
|
||||
const credentialData = credential.credential;
|
||||
const getTotpTypeDisplay = (totpType: string) => {
|
||||
switch (totpType) {
|
||||
case "authenticator":
|
||||
@@ -21,6 +26,69 @@ function CredentialItem({ credential }: Props) {
|
||||
}
|
||||
};
|
||||
|
||||
let credentialDetails = null;
|
||||
|
||||
if (isPasswordCredential(credentialData)) {
|
||||
credentialDetails = (
|
||||
<div className="border-l pl-5">
|
||||
<div className="flex gap-5">
|
||||
<div className="shrink-0 space-y-2">
|
||||
<p className="text-sm text-slate-400">Username/Email</p>
|
||||
<p className="text-sm text-slate-400">Password</p>
|
||||
{credentialData.totp_type !== "none" && (
|
||||
<p className="text-sm text-slate-400">2FA Type</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">{credentialData.username}</p>
|
||||
<p className="text-sm">{"********"}</p>
|
||||
{credentialData.totp_type !== "none" && (
|
||||
<p className="text-sm text-blue-400">
|
||||
{getTotpTypeDisplay(credentialData.totp_type)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (isCreditCardCredential(credentialData)) {
|
||||
credentialDetails = (
|
||||
<div className="flex gap-5 border-l pl-5">
|
||||
<div className="flex gap-5">
|
||||
<div className="shrink-0 space-y-2">
|
||||
<p className="text-sm text-slate-400">Card Number</p>
|
||||
<p className="text-sm text-slate-400">Brand</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-5">
|
||||
<div className="shrink-0 space-y-2">
|
||||
<p className="text-sm">
|
||||
{"************" + credentialData.last_four}
|
||||
</p>
|
||||
<p className="text-sm">{credentialData.brand}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (isSecretCredential(credentialData)) {
|
||||
credentialDetails = (
|
||||
<div className="flex gap-5 border-l pl-5">
|
||||
<div className="shrink-0 space-y-2">
|
||||
<p className="text-sm text-slate-400">Secret Value</p>
|
||||
{credentialData.secret_label ? (
|
||||
<p className="text-sm text-slate-400">Type</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">{"************"}</p>
|
||||
{credentialData.secret_label ? (
|
||||
<p className="text-sm">{credentialData.secret_label}</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex gap-5 rounded-lg bg-slate-elevation2 p-4">
|
||||
<div className="w-48 space-y-2">
|
||||
@@ -29,45 +97,7 @@ function CredentialItem({ credential }: Props) {
|
||||
</p>
|
||||
<p className="text-sm text-slate-400">{credential.credential_id}</p>
|
||||
</div>
|
||||
{isPasswordCredential(credential.credential) ? (
|
||||
<div className="border-l pl-5">
|
||||
<div className="flex gap-5">
|
||||
<div className="shrink-0 space-y-2">
|
||||
<p className="text-sm text-slate-400">Username/Email</p>
|
||||
<p className="text-sm text-slate-400">Password</p>
|
||||
{credential.credential.totp_type !== "none" && (
|
||||
<p className="text-sm text-slate-400">2FA Type</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm">{credential.credential.username}</p>
|
||||
<p className="text-sm">{"********"}</p>
|
||||
{credential.credential.totp_type !== "none" && (
|
||||
<p className="text-sm text-blue-400">
|
||||
{getTotpTypeDisplay(credential.credential.totp_type)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-5 border-l pl-5">
|
||||
<div className="flex gap-5">
|
||||
<div className="shrink-0 space-y-2">
|
||||
<p className="text-sm text-slate-400">Card Number</p>
|
||||
<p className="text-sm text-slate-400">Brand</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-5">
|
||||
<div className="shrink-0 space-y-2">
|
||||
<p className="text-sm">
|
||||
{"************" + credential.credential.last_four}
|
||||
</p>
|
||||
<p className="text-sm">{credential.credential.brand}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{credentialDetails}
|
||||
<div className="ml-auto">
|
||||
<DeleteCredentialButton credential={credential} />
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { CredentialItem } from "./CredentialItem";
|
||||
import { useCredentialsQuery } from "@/routes/workflows/hooks/useCredentialsQuery";
|
||||
|
||||
type CredentialFilter = "password" | "credit_card";
|
||||
type CredentialFilter = "password" | "credit_card" | "secret";
|
||||
|
||||
type Props = {
|
||||
filter?: CredentialFilter;
|
||||
@@ -11,6 +11,7 @@ type Props = {
|
||||
const EMPTY_MESSAGE: Record<CredentialFilter, string> = {
|
||||
password: "No password credentials stored yet.",
|
||||
credit_card: "No credit cards stored yet.",
|
||||
secret: "No secrets stored yet.",
|
||||
};
|
||||
|
||||
function CredentialsList({ filter }: Props = {}) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CredentialModalTypes,
|
||||
} from "./useCredentialModalState";
|
||||
import { PasswordCredentialContent } from "./PasswordCredentialContent";
|
||||
import { SecretCredentialContent } from "./SecretCredentialContent";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CreditCardCredentialContent } from "./CreditCardCredentialContent";
|
||||
@@ -40,6 +41,12 @@ const CREDIT_CARD_CREDENTIAL_INITIAL_VALUES = {
|
||||
cardHolderName: "",
|
||||
};
|
||||
|
||||
const SECRET_CREDENTIAL_INITIAL_VALUES = {
|
||||
name: "",
|
||||
secretLabel: "",
|
||||
secretValue: "",
|
||||
};
|
||||
|
||||
// Function to generate a unique credential name
|
||||
function generateDefaultCredentialName(existingNames: string[]): string {
|
||||
const baseName = "credentials";
|
||||
@@ -73,6 +80,9 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
||||
const [creditCardCredentialValues, setCreditCardCredentialValues] = useState(
|
||||
CREDIT_CARD_CREDENTIAL_INITIAL_VALUES,
|
||||
);
|
||||
const [secretCredentialValues, setSecretCredentialValues] = useState(
|
||||
SECRET_CREDENTIAL_INITIAL_VALUES,
|
||||
);
|
||||
|
||||
// Set default name when modal opens
|
||||
useEffect(() => {
|
||||
@@ -88,12 +98,17 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
||||
...prev,
|
||||
name: defaultName,
|
||||
}));
|
||||
setSecretCredentialValues((prev) => ({
|
||||
...prev,
|
||||
name: defaultName,
|
||||
}));
|
||||
}
|
||||
}, [isOpen, credentials]);
|
||||
|
||||
function reset() {
|
||||
setPasswordCredentialValues(PASSWORD_CREDENTIAL_INITIAL_VALUES);
|
||||
setCreditCardCredentialValues(CREDIT_CARD_CREDENTIAL_INITIAL_VALUES);
|
||||
setSecretCredentialValues(SECRET_CREDENTIAL_INITIAL_VALUES);
|
||||
}
|
||||
|
||||
const createCredentialMutation = useMutation({
|
||||
@@ -128,7 +143,9 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
||||
const name =
|
||||
type === CredentialModalTypes.PASSWORD
|
||||
? passwordCredentialValues.name.trim()
|
||||
: creditCardCredentialValues.name.trim();
|
||||
: type === CredentialModalTypes.CREDIT_CARD
|
||||
? creditCardCredentialValues.name.trim()
|
||||
: secretCredentialValues.name.trim();
|
||||
if (name === "") {
|
||||
toast({
|
||||
title: "Error",
|
||||
@@ -219,9 +236,55 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
||||
card_holder_name: cardHolderName,
|
||||
},
|
||||
});
|
||||
} else if (type === CredentialModalTypes.SECRET) {
|
||||
const secretValue = secretCredentialValues.secretValue.trim();
|
||||
const secretLabel = secretCredentialValues.secretLabel.trim();
|
||||
|
||||
if (secretValue === "") {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Secret value is required",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
createCredentialMutation.mutate({
|
||||
name,
|
||||
credential_type: "secret",
|
||||
credential: {
|
||||
secret_value: secretValue,
|
||||
secret_label: secretLabel === "" ? null : secretLabel,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const credentialContent = (() => {
|
||||
if (type === CredentialModalTypes.PASSWORD) {
|
||||
return (
|
||||
<PasswordCredentialContent
|
||||
values={passwordCredentialValues}
|
||||
onChange={setPasswordCredentialValues}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (type === CredentialModalTypes.CREDIT_CARD) {
|
||||
return (
|
||||
<CreditCardCredentialContent
|
||||
values={creditCardCredentialValues}
|
||||
onChange={setCreditCardCredentialValues}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SecretCredentialContent
|
||||
values={secretCredentialValues}
|
||||
onChange={setSecretCredentialValues}
|
||||
/>
|
||||
);
|
||||
})();
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
@@ -236,17 +299,7 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
||||
<DialogHeader>
|
||||
<DialogTitle className="font-bold">Add Credential</DialogTitle>
|
||||
</DialogHeader>
|
||||
{type === CredentialModalTypes.PASSWORD ? (
|
||||
<PasswordCredentialContent
|
||||
values={passwordCredentialValues}
|
||||
onChange={setPasswordCredentialValues}
|
||||
/>
|
||||
) : (
|
||||
<CreditCardCredentialContent
|
||||
values={creditCardCredentialValues}
|
||||
onChange={setCreditCardCredentialValues}
|
||||
/>
|
||||
)}
|
||||
{credentialContent}
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CardStackIcon, PlusIcon } from "@radix-ui/react-icons";
|
||||
import { CardStackIcon, LockClosedIcon, PlusIcon } from "@radix-ui/react-icons";
|
||||
import {
|
||||
CredentialModalTypes,
|
||||
useCredentialModalState,
|
||||
@@ -19,9 +19,14 @@ import { CredentialsTotpTab } from "./CredentialsTotpTab";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
|
||||
const subHeaderText =
|
||||
"Securely store your passwords, credit cards, and manage incoming 2FA codes for your workflows.";
|
||||
"Securely store your passwords, credit cards, secrets, and manage incoming 2FA codes for your workflows.";
|
||||
|
||||
const TAB_VALUES = ["passwords", "creditCards", "twoFactor"] as const;
|
||||
const TAB_VALUES = [
|
||||
"passwords",
|
||||
"creditCards",
|
||||
"secrets",
|
||||
"twoFactor",
|
||||
] as const;
|
||||
type TabValue = (typeof TAB_VALUES)[number];
|
||||
const DEFAULT_TAB: TabValue = "passwords";
|
||||
|
||||
@@ -79,6 +84,16 @@ function CredentialsPage() {
|
||||
<CardStackIcon className="mr-2 size-4" />
|
||||
Credit Card
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => {
|
||||
setIsOpen(true);
|
||||
setType(CredentialModalTypes.SECRET);
|
||||
}}
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<LockClosedIcon className="mr-2 size-4" />
|
||||
Secret
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
@@ -90,6 +105,7 @@ function CredentialsPage() {
|
||||
<TabsList className="bg-slate-elevation1">
|
||||
<TabsTrigger value="passwords">Passwords</TabsTrigger>
|
||||
<TabsTrigger value="creditCards">Credit Cards</TabsTrigger>
|
||||
<TabsTrigger value="secrets">Secrets</TabsTrigger>
|
||||
<TabsTrigger value="twoFactor">2FA</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
@@ -101,6 +117,10 @@ function CredentialsPage() {
|
||||
<CredentialsList filter="credit_card" />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="secrets" className="space-y-4">
|
||||
<CredentialsList filter="secret" />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="twoFactor" className="space-y-4">
|
||||
<CredentialsTotpTab />
|
||||
</TabsContent>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { EyeNoneIcon, EyeOpenIcon } from "@radix-ui/react-icons";
|
||||
import { useState } from "react";
|
||||
|
||||
type Props = {
|
||||
values: {
|
||||
name: string;
|
||||
secretLabel: string;
|
||||
secretValue: string;
|
||||
};
|
||||
onChange: (values: {
|
||||
name: string;
|
||||
secretLabel: string;
|
||||
secretValue: string;
|
||||
}) => void;
|
||||
};
|
||||
|
||||
function SecretCredentialContent({ values, onChange }: Props) {
|
||||
const { name, secretLabel, secretValue } = values;
|
||||
const [showSecret, setShowSecret] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex">
|
||||
<div className="w-72 shrink-0 space-y-1">
|
||||
<Label>Name</Label>
|
||||
<div className="text-sm text-slate-400">
|
||||
The name of the credential
|
||||
</div>
|
||||
</div>
|
||||
<Input
|
||||
value={name}
|
||||
onChange={(e) => onChange({ ...values, name: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="space-y-2">
|
||||
<Label>Secret Label (optional)</Label>
|
||||
<Input
|
||||
placeholder="e.g., API Key, Bearer Token"
|
||||
value={secretLabel}
|
||||
onChange={(e) => onChange({ ...values, secretLabel: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Secret Value</Label>
|
||||
<div className="relative w-full">
|
||||
<Input
|
||||
className="pr-9"
|
||||
type={showSecret ? "text" : "password"}
|
||||
value={secretValue}
|
||||
onChange={(e) =>
|
||||
onChange({ ...values, secretValue: e.target.value })
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="absolute right-0 top-0 flex size-9 cursor-pointer items-center justify-center"
|
||||
onClick={() => {
|
||||
setShowSecret((value) => !value);
|
||||
}}
|
||||
aria-label="Toggle secret value visibility"
|
||||
>
|
||||
{showSecret ? (
|
||||
<EyeOpenIcon className="size-4" />
|
||||
) : (
|
||||
<EyeNoneIcon className="size-4" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-400">
|
||||
{
|
||||
"Use in HTTP Request blocks with: {{ credential_name.secret_value }}"
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { SecretCredentialContent };
|
||||
@@ -6,6 +6,7 @@ const typeParam = "type";
|
||||
export const CredentialModalTypes = {
|
||||
PASSWORD: "password",
|
||||
CREDIT_CARD: "credit-card",
|
||||
SECRET: "secret",
|
||||
} as const;
|
||||
|
||||
export type CredentialModalType =
|
||||
|
||||
@@ -62,7 +62,9 @@ function CredentialSelector({ value, onChange }: Props) {
|
||||
<p className="text-xs text-slate-400">
|
||||
{credential.credential_type === "password"
|
||||
? "Password"
|
||||
: "Credit Card"}
|
||||
: credential.credential_type === "credit_card"
|
||||
? "Credit Card"
|
||||
: "Secret"}
|
||||
</p>
|
||||
</div>
|
||||
</CustomSelectItem>
|
||||
|
||||
@@ -399,9 +399,10 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
||||
authentication and content headers
|
||||
</li>
|
||||
<li>
|
||||
Pass a credential/secret parameter and reference it in headers
|
||||
or body with {"{{ my_credential.password }}"}
|
||||
Password credential: {"{{ my_credential.username }}"} /{" "}
|
||||
{"{{ my_credential.password }}"}
|
||||
</li>
|
||||
<li>Secret credential: {"{{ my_secret.secret_value }}"}</li>
|
||||
<li>
|
||||
The request will return response data including status, headers,
|
||||
and body
|
||||
|
||||
Reference in New Issue
Block a user