[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;
|
brand: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SecretCredentialResponse = {
|
||||||
|
secret_label?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type CredentialApiResponse = {
|
export type CredentialApiResponse = {
|
||||||
credential_id: string;
|
credential_id: string;
|
||||||
credential: PasswordCredentialApiResponse | CreditCardCredentialApiResponse;
|
credential:
|
||||||
credential_type: "password" | "credit_card";
|
| PasswordCredentialApiResponse
|
||||||
|
| CreditCardCredentialApiResponse
|
||||||
|
| SecretCredentialResponse;
|
||||||
|
credential_type: "password" | "credit_card" | "secret";
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function isPasswordCredential(
|
export function isPasswordCredential(
|
||||||
credential: PasswordCredentialApiResponse | CreditCardCredentialApiResponse,
|
credential:
|
||||||
|
| PasswordCredentialApiResponse
|
||||||
|
| CreditCardCredentialApiResponse
|
||||||
|
| SecretCredentialResponse,
|
||||||
): credential is PasswordCredentialApiResponse {
|
): credential is PasswordCredentialApiResponse {
|
||||||
return "username" in credential;
|
return "username" in credential;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isCreditCardCredential(
|
export function isCreditCardCredential(
|
||||||
credential: PasswordCredentialApiResponse | CreditCardCredentialApiResponse,
|
credential:
|
||||||
|
| PasswordCredentialApiResponse
|
||||||
|
| CreditCardCredentialApiResponse
|
||||||
|
| SecretCredentialResponse,
|
||||||
): credential is CreditCardCredentialApiResponse {
|
): credential is CreditCardCredentialApiResponse {
|
||||||
return "last_four" in credential;
|
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 = {
|
export type CreateCredentialRequest = {
|
||||||
name: string;
|
name: string;
|
||||||
credential_type: "password" | "credit_card";
|
credential_type: "password" | "credit_card" | "secret";
|
||||||
credential: PasswordCredential | CreditCardCredential;
|
credential: PasswordCredential | CreditCardCredential | SecretCredential;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PasswordCredential = {
|
export type PasswordCredential = {
|
||||||
@@ -542,6 +564,11 @@ export type CreditCardCredential = {
|
|||||||
card_holder_name: string;
|
card_holder_name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SecretCredential = {
|
||||||
|
secret_value: string;
|
||||||
|
secret_label?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export const OtpType = {
|
export const OtpType = {
|
||||||
Totp: "totp",
|
Totp: "totp",
|
||||||
MagicLink: "magic_link",
|
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 { DeleteCredentialButton } from "./DeleteCredentialButton";
|
||||||
import { CredentialApiResponse } from "@/api/types";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
credential: CredentialApiResponse;
|
credential: CredentialApiResponse;
|
||||||
};
|
};
|
||||||
|
|
||||||
function CredentialItem({ credential }: Props) {
|
function CredentialItem({ credential }: Props) {
|
||||||
|
const credentialData = credential.credential;
|
||||||
const getTotpTypeDisplay = (totpType: string) => {
|
const getTotpTypeDisplay = (totpType: string) => {
|
||||||
switch (totpType) {
|
switch (totpType) {
|
||||||
case "authenticator":
|
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 (
|
return (
|
||||||
<div className="flex gap-5 rounded-lg bg-slate-elevation2 p-4">
|
<div className="flex gap-5 rounded-lg bg-slate-elevation2 p-4">
|
||||||
<div className="w-48 space-y-2">
|
<div className="w-48 space-y-2">
|
||||||
@@ -29,45 +97,7 @@ function CredentialItem({ credential }: Props) {
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-slate-400">{credential.credential_id}</p>
|
<p className="text-sm text-slate-400">{credential.credential_id}</p>
|
||||||
</div>
|
</div>
|
||||||
{isPasswordCredential(credential.credential) ? (
|
{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>
|
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
<div className="ml-auto">
|
<div className="ml-auto">
|
||||||
<DeleteCredentialButton credential={credential} />
|
<DeleteCredentialButton credential={credential} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||||||
import { CredentialItem } from "./CredentialItem";
|
import { CredentialItem } from "./CredentialItem";
|
||||||
import { useCredentialsQuery } from "@/routes/workflows/hooks/useCredentialsQuery";
|
import { useCredentialsQuery } from "@/routes/workflows/hooks/useCredentialsQuery";
|
||||||
|
|
||||||
type CredentialFilter = "password" | "credit_card";
|
type CredentialFilter = "password" | "credit_card" | "secret";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
filter?: CredentialFilter;
|
filter?: CredentialFilter;
|
||||||
@@ -11,6 +11,7 @@ type Props = {
|
|||||||
const EMPTY_MESSAGE: Record<CredentialFilter, string> = {
|
const EMPTY_MESSAGE: Record<CredentialFilter, string> = {
|
||||||
password: "No password credentials stored yet.",
|
password: "No password credentials stored yet.",
|
||||||
credit_card: "No credit cards stored yet.",
|
credit_card: "No credit cards stored yet.",
|
||||||
|
secret: "No secrets stored yet.",
|
||||||
};
|
};
|
||||||
|
|
||||||
function CredentialsList({ filter }: Props = {}) {
|
function CredentialsList({ filter }: Props = {}) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
CredentialModalTypes,
|
CredentialModalTypes,
|
||||||
} from "./useCredentialModalState";
|
} from "./useCredentialModalState";
|
||||||
import { PasswordCredentialContent } from "./PasswordCredentialContent";
|
import { PasswordCredentialContent } from "./PasswordCredentialContent";
|
||||||
|
import { SecretCredentialContent } from "./SecretCredentialContent";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CreditCardCredentialContent } from "./CreditCardCredentialContent";
|
import { CreditCardCredentialContent } from "./CreditCardCredentialContent";
|
||||||
@@ -40,6 +41,12 @@ const CREDIT_CARD_CREDENTIAL_INITIAL_VALUES = {
|
|||||||
cardHolderName: "",
|
cardHolderName: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SECRET_CREDENTIAL_INITIAL_VALUES = {
|
||||||
|
name: "",
|
||||||
|
secretLabel: "",
|
||||||
|
secretValue: "",
|
||||||
|
};
|
||||||
|
|
||||||
// Function to generate a unique credential name
|
// Function to generate a unique credential name
|
||||||
function generateDefaultCredentialName(existingNames: string[]): string {
|
function generateDefaultCredentialName(existingNames: string[]): string {
|
||||||
const baseName = "credentials";
|
const baseName = "credentials";
|
||||||
@@ -73,6 +80,9 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
|||||||
const [creditCardCredentialValues, setCreditCardCredentialValues] = useState(
|
const [creditCardCredentialValues, setCreditCardCredentialValues] = useState(
|
||||||
CREDIT_CARD_CREDENTIAL_INITIAL_VALUES,
|
CREDIT_CARD_CREDENTIAL_INITIAL_VALUES,
|
||||||
);
|
);
|
||||||
|
const [secretCredentialValues, setSecretCredentialValues] = useState(
|
||||||
|
SECRET_CREDENTIAL_INITIAL_VALUES,
|
||||||
|
);
|
||||||
|
|
||||||
// Set default name when modal opens
|
// Set default name when modal opens
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -88,12 +98,17 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
|||||||
...prev,
|
...prev,
|
||||||
name: defaultName,
|
name: defaultName,
|
||||||
}));
|
}));
|
||||||
|
setSecretCredentialValues((prev) => ({
|
||||||
|
...prev,
|
||||||
|
name: defaultName,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}, [isOpen, credentials]);
|
}, [isOpen, credentials]);
|
||||||
|
|
||||||
function reset() {
|
function reset() {
|
||||||
setPasswordCredentialValues(PASSWORD_CREDENTIAL_INITIAL_VALUES);
|
setPasswordCredentialValues(PASSWORD_CREDENTIAL_INITIAL_VALUES);
|
||||||
setCreditCardCredentialValues(CREDIT_CARD_CREDENTIAL_INITIAL_VALUES);
|
setCreditCardCredentialValues(CREDIT_CARD_CREDENTIAL_INITIAL_VALUES);
|
||||||
|
setSecretCredentialValues(SECRET_CREDENTIAL_INITIAL_VALUES);
|
||||||
}
|
}
|
||||||
|
|
||||||
const createCredentialMutation = useMutation({
|
const createCredentialMutation = useMutation({
|
||||||
@@ -128,7 +143,9 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
|||||||
const name =
|
const name =
|
||||||
type === CredentialModalTypes.PASSWORD
|
type === CredentialModalTypes.PASSWORD
|
||||||
? passwordCredentialValues.name.trim()
|
? passwordCredentialValues.name.trim()
|
||||||
: creditCardCredentialValues.name.trim();
|
: type === CredentialModalTypes.CREDIT_CARD
|
||||||
|
? creditCardCredentialValues.name.trim()
|
||||||
|
: secretCredentialValues.name.trim();
|
||||||
if (name === "") {
|
if (name === "") {
|
||||||
toast({
|
toast({
|
||||||
title: "Error",
|
title: "Error",
|
||||||
@@ -219,9 +236,55 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
|||||||
card_holder_name: cardHolderName,
|
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 (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={isOpen}
|
open={isOpen}
|
||||||
@@ -236,17 +299,7 @@ function CredentialsModal({ onCredentialCreated }: Props) {
|
|||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="font-bold">Add Credential</DialogTitle>
|
<DialogTitle className="font-bold">Add Credential</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
{type === CredentialModalTypes.PASSWORD ? (
|
{credentialContent}
|
||||||
<PasswordCredentialContent
|
|
||||||
values={passwordCredentialValues}
|
|
||||||
onChange={setPasswordCredentialValues}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<CreditCardCredentialContent
|
|
||||||
values={creditCardCredentialValues}
|
|
||||||
onChange={setCreditCardCredentialValues}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { CardStackIcon, PlusIcon } from "@radix-ui/react-icons";
|
import { CardStackIcon, LockClosedIcon, PlusIcon } from "@radix-ui/react-icons";
|
||||||
import {
|
import {
|
||||||
CredentialModalTypes,
|
CredentialModalTypes,
|
||||||
useCredentialModalState,
|
useCredentialModalState,
|
||||||
@@ -19,9 +19,14 @@ import { CredentialsTotpTab } from "./CredentialsTotpTab";
|
|||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
const subHeaderText =
|
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];
|
type TabValue = (typeof TAB_VALUES)[number];
|
||||||
const DEFAULT_TAB: TabValue = "passwords";
|
const DEFAULT_TAB: TabValue = "passwords";
|
||||||
|
|
||||||
@@ -79,6 +84,16 @@ function CredentialsPage() {
|
|||||||
<CardStackIcon className="mr-2 size-4" />
|
<CardStackIcon className="mr-2 size-4" />
|
||||||
Credit Card
|
Credit Card
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={() => {
|
||||||
|
setIsOpen(true);
|
||||||
|
setType(CredentialModalTypes.SECRET);
|
||||||
|
}}
|
||||||
|
className="cursor-pointer"
|
||||||
|
>
|
||||||
|
<LockClosedIcon className="mr-2 size-4" />
|
||||||
|
Secret
|
||||||
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</div>
|
</div>
|
||||||
@@ -90,6 +105,7 @@ function CredentialsPage() {
|
|||||||
<TabsList className="bg-slate-elevation1">
|
<TabsList className="bg-slate-elevation1">
|
||||||
<TabsTrigger value="passwords">Passwords</TabsTrigger>
|
<TabsTrigger value="passwords">Passwords</TabsTrigger>
|
||||||
<TabsTrigger value="creditCards">Credit Cards</TabsTrigger>
|
<TabsTrigger value="creditCards">Credit Cards</TabsTrigger>
|
||||||
|
<TabsTrigger value="secrets">Secrets</TabsTrigger>
|
||||||
<TabsTrigger value="twoFactor">2FA</TabsTrigger>
|
<TabsTrigger value="twoFactor">2FA</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
@@ -101,6 +117,10 @@ function CredentialsPage() {
|
|||||||
<CredentialsList filter="credit_card" />
|
<CredentialsList filter="credit_card" />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="secrets" className="space-y-4">
|
||||||
|
<CredentialsList filter="secret" />
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="twoFactor" className="space-y-4">
|
<TabsContent value="twoFactor" className="space-y-4">
|
||||||
<CredentialsTotpTab />
|
<CredentialsTotpTab />
|
||||||
</TabsContent>
|
</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 = {
|
export const CredentialModalTypes = {
|
||||||
PASSWORD: "password",
|
PASSWORD: "password",
|
||||||
CREDIT_CARD: "credit-card",
|
CREDIT_CARD: "credit-card",
|
||||||
|
SECRET: "secret",
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type CredentialModalType =
|
export type CredentialModalType =
|
||||||
|
|||||||
@@ -62,7 +62,9 @@ function CredentialSelector({ value, onChange }: Props) {
|
|||||||
<p className="text-xs text-slate-400">
|
<p className="text-xs text-slate-400">
|
||||||
{credential.credential_type === "password"
|
{credential.credential_type === "password"
|
||||||
? "Password"
|
? "Password"
|
||||||
: "Credit Card"}
|
: credential.credential_type === "credit_card"
|
||||||
|
? "Credit Card"
|
||||||
|
: "Secret"}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CustomSelectItem>
|
</CustomSelectItem>
|
||||||
|
|||||||
@@ -399,9 +399,10 @@ function HttpRequestNode({ id, data }: NodeProps<HttpRequestNodeType>) {
|
|||||||
authentication and content headers
|
authentication and content headers
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
Pass a credential/secret parameter and reference it in headers
|
Password credential: {"{{ my_credential.username }}"} /{" "}
|
||||||
or body with {"{{ my_credential.password }}"}
|
{"{{ my_credential.password }}"}
|
||||||
</li>
|
</li>
|
||||||
|
<li>Secret credential: {"{{ my_secret.secret_value }}"}</li>
|
||||||
<li>
|
<li>
|
||||||
The request will return response data including status, headers,
|
The request will return response data including status, headers,
|
||||||
and body
|
and body
|
||||||
|
|||||||
Reference in New Issue
Block a user