2025-03-11 11:18:26 -07:00
|
|
|
import {
|
|
|
|
|
Dialog,
|
|
|
|
|
DialogTitle,
|
|
|
|
|
DialogContent,
|
|
|
|
|
DialogHeader,
|
|
|
|
|
DialogFooter,
|
|
|
|
|
} from "@/components/ui/dialog";
|
|
|
|
|
import {
|
|
|
|
|
useCredentialModalState,
|
|
|
|
|
CredentialModalTypes,
|
|
|
|
|
} from "./useCredentialModalState";
|
|
|
|
|
import { PasswordCredentialContent } from "./PasswordCredentialContent";
|
2025-06-23 21:52:38 -04:00
|
|
|
import { useState, useEffect } from "react";
|
2025-03-11 11:18:26 -07:00
|
|
|
import { Button } from "@/components/ui/button";
|
|
|
|
|
import { CreditCardCredentialContent } from "./CreditCardCredentialContent";
|
|
|
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
|
|
|
import { CreateCredentialRequest } from "@/api/types";
|
|
|
|
|
import { getClient } from "@/api/AxiosClient";
|
|
|
|
|
import { useCredentialGetter } from "@/hooks/useCredentialGetter";
|
|
|
|
|
import { toast } from "@/components/ui/use-toast";
|
|
|
|
|
import { AxiosError } from "axios";
|
|
|
|
|
import { ReloadIcon } from "@radix-ui/react-icons";
|
2025-06-23 21:52:38 -04:00
|
|
|
import { useCredentialsQuery } from "@/routes/workflows/hooks/useCredentialsQuery";
|
2025-03-11 11:18:26 -07:00
|
|
|
|
|
|
|
|
const PASSWORD_CREDENTIAL_INITIAL_VALUES = {
|
|
|
|
|
name: "",
|
|
|
|
|
username: "",
|
|
|
|
|
password: "",
|
|
|
|
|
totp: "",
|
2025-10-08 11:38:34 -07:00
|
|
|
totp_type: "none" as "none" | "authenticator" | "email" | "text",
|
2025-12-04 13:26:51 -08:00
|
|
|
totp_identifier: "",
|
2025-03-11 11:18:26 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const CREDIT_CARD_CREDENTIAL_INITIAL_VALUES = {
|
|
|
|
|
name: "",
|
|
|
|
|
cardNumber: "",
|
|
|
|
|
cardExpirationDate: "",
|
|
|
|
|
cardCode: "",
|
|
|
|
|
cardBrand: "",
|
|
|
|
|
cardHolderName: "",
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-23 21:52:38 -04:00
|
|
|
// Function to generate a unique credential name
|
|
|
|
|
function generateDefaultCredentialName(existingNames: string[]): string {
|
|
|
|
|
const baseName = "credentials";
|
|
|
|
|
|
|
|
|
|
// Check if "credentials" is available
|
|
|
|
|
if (!existingNames.includes(baseName)) {
|
|
|
|
|
return baseName;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Find the next available number
|
|
|
|
|
let counter = 1;
|
|
|
|
|
while (existingNames.includes(`${baseName}_${counter}`)) {
|
|
|
|
|
counter++;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `${baseName}_${counter}`;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-11 11:18:26 -07:00
|
|
|
type Props = {
|
|
|
|
|
onCredentialCreated?: (id: string) => void;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
function CredentialsModal({ onCredentialCreated }: Props) {
|
|
|
|
|
const credentialGetter = useCredentialGetter();
|
|
|
|
|
const queryClient = useQueryClient();
|
|
|
|
|
const { isOpen, type, setIsOpen } = useCredentialModalState();
|
2025-06-23 21:52:38 -04:00
|
|
|
const { data: credentials } = useCredentialsQuery();
|
2025-03-11 11:18:26 -07:00
|
|
|
const [passwordCredentialValues, setPasswordCredentialValues] = useState(
|
|
|
|
|
PASSWORD_CREDENTIAL_INITIAL_VALUES,
|
|
|
|
|
);
|
|
|
|
|
const [creditCardCredentialValues, setCreditCardCredentialValues] = useState(
|
|
|
|
|
CREDIT_CARD_CREDENTIAL_INITIAL_VALUES,
|
|
|
|
|
);
|
|
|
|
|
|
2025-06-23 21:52:38 -04:00
|
|
|
// Set default name when modal opens
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (isOpen && credentials) {
|
|
|
|
|
const existingNames = credentials.map((cred) => cred.name);
|
|
|
|
|
const defaultName = generateDefaultCredentialName(existingNames);
|
|
|
|
|
|
|
|
|
|
setPasswordCredentialValues((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
name: defaultName,
|
|
|
|
|
}));
|
|
|
|
|
setCreditCardCredentialValues((prev) => ({
|
|
|
|
|
...prev,
|
|
|
|
|
name: defaultName,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}, [isOpen, credentials]);
|
|
|
|
|
|
2025-03-11 11:18:26 -07:00
|
|
|
function reset() {
|
|
|
|
|
setPasswordCredentialValues(PASSWORD_CREDENTIAL_INITIAL_VALUES);
|
|
|
|
|
setCreditCardCredentialValues(CREDIT_CARD_CREDENTIAL_INITIAL_VALUES);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const createCredentialMutation = useMutation({
|
|
|
|
|
mutationFn: async (request: CreateCredentialRequest) => {
|
|
|
|
|
const client = await getClient(credentialGetter);
|
|
|
|
|
const response = await client.post("/credentials", request);
|
|
|
|
|
return response.data;
|
|
|
|
|
},
|
|
|
|
|
onSuccess: (data) => {
|
|
|
|
|
setIsOpen(false);
|
|
|
|
|
queryClient.invalidateQueries({
|
|
|
|
|
queryKey: ["credentials"],
|
|
|
|
|
});
|
|
|
|
|
toast({
|
|
|
|
|
title: "Credential created",
|
|
|
|
|
description: "Your credential has been created successfully",
|
|
|
|
|
variant: "success",
|
|
|
|
|
});
|
|
|
|
|
onCredentialCreated?.(data.credential_id);
|
|
|
|
|
},
|
|
|
|
|
onError: (error: AxiosError) => {
|
|
|
|
|
const detail = (error.response?.data as { detail?: string })?.detail;
|
|
|
|
|
toast({
|
|
|
|
|
title: "Error",
|
|
|
|
|
description: detail ? detail : error.message,
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handleSave = () => {
|
2025-03-13 12:35:43 -07:00
|
|
|
const name =
|
|
|
|
|
type === CredentialModalTypes.PASSWORD
|
|
|
|
|
? passwordCredentialValues.name.trim()
|
|
|
|
|
: creditCardCredentialValues.name.trim();
|
|
|
|
|
if (name === "") {
|
|
|
|
|
toast({
|
|
|
|
|
title: "Error",
|
|
|
|
|
description: "Name is required",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-11 11:18:26 -07:00
|
|
|
if (type === CredentialModalTypes.PASSWORD) {
|
2025-03-13 12:35:43 -07:00
|
|
|
const username = passwordCredentialValues.username.trim();
|
|
|
|
|
const password = passwordCredentialValues.password.trim();
|
|
|
|
|
const totp = passwordCredentialValues.totp.trim();
|
2025-12-04 13:26:51 -08:00
|
|
|
const totpIdentifier = passwordCredentialValues.totp_identifier.trim();
|
2025-03-13 12:35:43 -07:00
|
|
|
|
|
|
|
|
if (username === "" || password === "") {
|
|
|
|
|
toast({
|
|
|
|
|
title: "Error",
|
|
|
|
|
description: "Username and password are required",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
2025-03-11 11:18:26 -07:00
|
|
|
createCredentialMutation.mutate({
|
2025-03-13 12:35:43 -07:00
|
|
|
name,
|
2025-03-11 11:18:26 -07:00
|
|
|
credential_type: "password",
|
|
|
|
|
credential: {
|
2025-03-13 12:35:43 -07:00
|
|
|
username,
|
|
|
|
|
password,
|
|
|
|
|
totp: totp === "" ? null : totp,
|
2025-10-08 11:38:34 -07:00
|
|
|
totp_type: passwordCredentialValues.totp_type,
|
2025-12-04 13:26:51 -08:00
|
|
|
totp_identifier: totpIdentifier === "" ? null : totpIdentifier,
|
2025-03-11 11:18:26 -07:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} else if (type === CredentialModalTypes.CREDIT_CARD) {
|
2025-03-13 12:35:43 -07:00
|
|
|
const cardNumber = creditCardCredentialValues.cardNumber.trim();
|
|
|
|
|
const cardCode = creditCardCredentialValues.cardCode.trim();
|
|
|
|
|
const cardExpirationDate =
|
|
|
|
|
creditCardCredentialValues.cardExpirationDate.trim();
|
|
|
|
|
const cardBrand = creditCardCredentialValues.cardBrand.trim();
|
|
|
|
|
const cardHolderName = creditCardCredentialValues.cardHolderName.trim();
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
cardNumber === "" ||
|
|
|
|
|
cardCode === "" ||
|
|
|
|
|
cardExpirationDate === "" ||
|
|
|
|
|
cardBrand === "" ||
|
|
|
|
|
cardHolderName === ""
|
|
|
|
|
) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "Error",
|
|
|
|
|
description: "All credit card fields are required",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2025-03-11 11:18:26 -07:00
|
|
|
const cardExpirationDateParts = cardExpirationDate.split("/");
|
|
|
|
|
if (cardExpirationDateParts.length !== 2) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "Error",
|
|
|
|
|
description: "Invalid card expiration date",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const cardExpirationMonth = cardExpirationDateParts[0];
|
|
|
|
|
const cardExpirationYear = cardExpirationDateParts[1];
|
|
|
|
|
if (!cardExpirationMonth || !cardExpirationYear) {
|
|
|
|
|
toast({
|
|
|
|
|
title: "Error",
|
|
|
|
|
description: "Invalid card expiration date",
|
|
|
|
|
variant: "destructive",
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// remove all spaces from the card number
|
|
|
|
|
const number = creditCardCredentialValues.cardNumber.replace(/\s/g, "");
|
|
|
|
|
createCredentialMutation.mutate({
|
2025-03-13 12:35:43 -07:00
|
|
|
name,
|
2025-03-11 11:18:26 -07:00
|
|
|
credential_type: "credit_card",
|
|
|
|
|
credential: {
|
|
|
|
|
card_number: number,
|
2025-03-13 12:35:43 -07:00
|
|
|
card_cvv: cardCode,
|
2025-03-11 11:18:26 -07:00
|
|
|
card_exp_month: cardExpirationMonth,
|
|
|
|
|
card_exp_year: cardExpirationYear,
|
2025-03-13 12:35:43 -07:00
|
|
|
card_brand: cardBrand,
|
|
|
|
|
card_holder_name: cardHolderName,
|
2025-03-11 11:18:26 -07:00
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Dialog
|
|
|
|
|
open={isOpen}
|
|
|
|
|
onOpenChange={(open) => {
|
|
|
|
|
if (!open) {
|
|
|
|
|
reset();
|
|
|
|
|
}
|
|
|
|
|
setIsOpen(open);
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<DialogContent className="w-[700px] max-w-[700px]">
|
|
|
|
|
<DialogHeader>
|
|
|
|
|
<DialogTitle className="font-bold">Add Credential</DialogTitle>
|
|
|
|
|
</DialogHeader>
|
|
|
|
|
{type === CredentialModalTypes.PASSWORD ? (
|
|
|
|
|
<PasswordCredentialContent
|
|
|
|
|
values={passwordCredentialValues}
|
|
|
|
|
onChange={setPasswordCredentialValues}
|
|
|
|
|
/>
|
|
|
|
|
) : (
|
|
|
|
|
<CreditCardCredentialContent
|
|
|
|
|
values={creditCardCredentialValues}
|
|
|
|
|
onChange={setCreditCardCredentialValues}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
<DialogFooter>
|
|
|
|
|
<Button
|
|
|
|
|
onClick={handleSave}
|
|
|
|
|
disabled={createCredentialMutation.isPending}
|
|
|
|
|
>
|
|
|
|
|
{createCredentialMutation.isPending ? (
|
|
|
|
|
<ReloadIcon className="mr-2 size-4 animate-spin" />
|
|
|
|
|
) : null}
|
|
|
|
|
Save
|
|
|
|
|
</Button>
|
|
|
|
|
</DialogFooter>
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export { CredentialsModal };
|