import { useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { useForm } from "react-hook-form"; import { z } from "zod"; import { zodResolver } from "@hookform/resolvers/zod"; import { useAddSecretMutation, useSecretQuery, useUpdateSecretMutation, } from "../../../hooks/useSecret"; import { useSodium } from "../../../hooks/useSodium"; import { PUBLIC_KEY } from "../../../consts"; import { useNotyf } from "../../../hooks/useNotyf"; const UPPER_SNAKE_CASE_REGEX = /^[A-Z][A-Z0-9_]*$/; const schema = z.object({ name: z .string() .min(1, "Name is required") .regex( UPPER_SNAKE_CASE_REGEX, "Name must be in UPPER_SNAKE_CASE (e.g. MY_SECRET)", ), value: z.string().min(1, "Value is required"), }); type FormValues = z.infer; export type AddSecretModalProps = { isOpen: boolean; onClose: () => void; sandboxId: string; secretId?: string; }; function AddSecretModal({ isOpen, onClose, sandboxId, secretId, }: AddSecretModalProps) { const sodium = useSodium(); const notyf = useNotyf(); const [isLoading, setIsLoading] = useState(false); const { mutateAsync: addSecret } = useAddSecretMutation(); const { mutateAsync: updateSecret } = useUpdateSecretMutation(); const { data } = useSecretQuery(secretId!); const { register, handleSubmit, reset, setValue, formState: { errors }, } = useForm({ resolver: zodResolver(schema), }); const handleNameChange = (e: React.ChangeEvent) => { const transformed = e.target.value .toUpperCase() .replace(/\s+/g, "_") .replace(/[^A-Z0-9_]/g, ""); setValue("name", transformed, { shouldValidate: true }); }; useEffect(() => { if (data) { setValue("name", data.secret?.name); } }, [data, setValue]); useEffect(() => { const handleEscapeKey = (event: KeyboardEvent) => { if (event.key === "Escape" && isOpen) { reset(); onClose(); } }; document.addEventListener("keydown", handleEscapeKey); return () => { document.removeEventListener("keydown", handleEscapeKey); }; }, [isOpen, onClose, reset]); const handleBackdropClick = (e: React.MouseEvent) => { e.stopPropagation(); if (e.target === e.currentTarget) { reset(); onClose(); } }; const handleContentClick = (e: React.MouseEvent) => { e.stopPropagation(); }; const handleCloseButton = (e: React.MouseEvent) => { e.stopPropagation(); reset(); onClose(); }; const onSubmit = async (data: FormValues) => { setIsLoading(true); const sealed = sodium.cryptoBoxSeal( sodium.fromString(data.value), sodium.fromHex(PUBLIC_KEY), ); try { if (secretId) { await updateSecret({ id: secretId, name: data.name, value: sodium.toBase64( sealed, sodium.base64Variants.URLSAFE_NO_PADDING, ), sandboxId, }); setIsLoading(false); reset(); onClose(); notyf.open("primary", "Secret updated successfully!"); return; } await addSecret({ sandboxId, name: data.name, value: sodium.toBase64( sealed, sodium.base64Variants.URLSAFE_NO_PADDING, ), }); setIsLoading(false); reset(); onClose(); notyf.open("primary", "Secret added successfully!"); } catch { notyf.open("error", "Failed to add secret!"); setIsLoading(false); reset(); onClose(); } }; if (!isOpen) return null; return createPortal( <>
{secretId ? "Edit Secret" : "Add Secret"}
{errors.name && ( {errors.name.message} )}
{errors.value && ( {errors.value.message} )}
e.stopPropagation()} >
, document.body, ); } export default AddSecretModal;