diff --git a/frontend/src/helpers/string.ts b/frontend/src/helpers/string.ts index 8f581a280..bd97d8a3c 100644 --- a/frontend/src/helpers/string.ts +++ b/frontend/src/helpers/string.ts @@ -3,3 +3,12 @@ export const removeTrailingSlash = (str: string) => { return str.endsWith("/") ? str.slice(0, -1) : str; }; + +export const isValidPath = (val: string): boolean => { + if (val.length === 0) return false; + if (val === "/") return true; + + // Check for valid characters and no consecutive slashes + const validPathRegex = /^[a-zA-Z0-9-_.:]+(?:\/[a-zA-Z0-9-_.:]+)*$/; + return validPathRegex.test(val); +} \ No newline at end of file diff --git a/frontend/src/pages/integrations/hashicorp-vault/authorize.tsx b/frontend/src/pages/integrations/hashicorp-vault/authorize.tsx index 41ba3571b..a68a68b61 100644 --- a/frontend/src/pages/integrations/hashicorp-vault/authorize.tsx +++ b/frontend/src/pages/integrations/hashicorp-vault/authorize.tsx @@ -1,83 +1,69 @@ -import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; import { faArrowUpRightFromSquare, faBookOpen } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import axios from "axios"; +import { z } from "zod"; +import { createNotification } from "@app/components/notifications"; import { useSaveIntegrationAccessToken } from "@app/hooks/api"; -import { Button, Card, CardTitle, FormControl, Input } from "../../../components/v2"; +import { Button, Card, CardBody, CardTitle, FormControl, Input } from "../../../components/v2"; + +const formSchema = z.object({ + vaultURL: z.string().url({ message: "Invalid Hashicorp Vault URL" }), + vaultNamespace: z.string().optional(), + vaultRoleID: z.string().uuid({ message: "Role ID has be a valid UUID" }), + vaultSecretID: z.string().uuid({ message: "Role ID has be a valid UUID" }) +}); + +type TForm = z.infer; export default function HashiCorpVaultAuthorizeIntegrationPage() { const router = useRouter(); const { mutateAsync } = useSaveIntegrationAccessToken(); - const [vaultURL, setVaultURL] = useState(""); - const [vaultURLErrorText, setVaultURLErrorText] = useState(""); + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + vaultURL: "", + vaultNamespace: "", + vaultRoleID: "", + vaultSecretID: "" + } + }); - const [vaultNamespace, setVaultNamespace] = useState(""); - const [vaultNamespaceErrorText, setVaultNamespaceErrorText] = useState(""); - - const [vaultRoleID, setVaultRoleID] = useState(""); - const [vaultRoleIDErrorText, setVaultRoleIDErrorText] = useState(""); - - const [vaultSecretID, setVaultSecretID] = useState(""); - const [vaultSecretIDErrorText, setVaultSecretIDErrorText] = useState(""); - - const [isLoading, setIsLoading] = useState(false); - - const handleButtonClick = async () => { + const handleFormSubmit = async (formData: TForm) => { try { - if (vaultURL.length === 0) { - setVaultURLErrorText("Vault Cluster URL cannot be blank"); - } else { - setVaultURLErrorText(""); - } - - if (vaultNamespace.length === 0) { - setVaultNamespaceErrorText("Vault Namespace cannot be blank"); - } else { - setVaultNamespaceErrorText(""); - } - - if (vaultRoleID.length === 0) { - setVaultRoleIDErrorText("Vault Role ID cannot be blank"); - } else { - setVaultRoleIDErrorText(""); - } - - if (vaultSecretID.length === 0) { - setVaultSecretIDErrorText("Vault Secret ID cannot be blank"); - } else { - setVaultSecretIDErrorText(""); - } - if ( - vaultURL.length === 0 || - vaultNamespace.length === 0 || - vaultRoleID.length === 0 || - vaultSecretID.length === 0 - ) { - return; - } - - setIsLoading(true); - const integrationAuth = await mutateAsync({ workspaceId: localStorage.getItem("projectData.id"), integration: "hashicorp-vault", - accessId: vaultRoleID, - accessToken: vaultSecretID, - url: vaultURL, - namespace: vaultNamespace + accessId: formData.vaultRoleID, + accessToken: formData.vaultSecretID, + url: formData.vaultURL, + namespace: formData.vaultNamespace }); - - setIsLoading(false); - router.push(`/integrations/hashicorp-vault/create?integrationAuthId=${integrationAuth.id}`); } catch (err) { console.error(err); + let errorMessage: string = "Something went wrong!"; + if (axios.isAxiosError(err)) { + const { message } = err?.response?.data as { message: string }; + errorMessage = message; + } + + createNotification({ + text: errorMessage, + type: "error" + }); } }; @@ -93,7 +79,7 @@ export default function HashiCorpVaultAuthorizeIntegrationPage() { subTitle="After connecting to Vault, you will be prompted to set up an integration for a particular Infisical project and environment." >
-
+
- - setVaultURL(e.target.value)} /> - - - setVaultNamespace(e.target.value)} - /> - - - setVaultRoleID(e.target.value)} - /> - - - setVaultSecretID(e.target.value)} - /> - - + +
+ ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + ( + + + + )} + /> + + +
); diff --git a/frontend/src/pages/integrations/hashicorp-vault/create.tsx b/frontend/src/pages/integrations/hashicorp-vault/create.tsx index 20e916c39..12837be8f 100644 --- a/frontend/src/pages/integrations/hashicorp-vault/create.tsx +++ b/frontend/src/pages/integrations/hashicorp-vault/create.tsx @@ -1,4 +1,5 @@ -import { useState } from "react"; +import { useMemo } from "react"; +import { Controller, useForm } from "react-hook-form"; import Head from "next/head"; import Image from "next/image"; import Link from "next/link"; @@ -10,13 +11,19 @@ import { faCircleInfo } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { zodResolver } from "@hookform/resolvers/zod"; +import axios from "axios"; import queryString from "query-string"; +import { z } from "zod"; +import { createNotification } from "@app/components/notifications"; +import { isValidPath } from "@app/helpers/string"; import { useCreateIntegration } from "@app/hooks/api"; import { Button, Card, + CardBody, CardTitle, FormControl, Input, @@ -26,6 +33,29 @@ import { import { useGetIntegrationAuthById } from "../../../hooks/api/integrationAuth"; import { useGetWorkspaceById } from "../../../hooks/api/workspace"; +const generateFormSchema = (availableEnvironmentNames: string[]) => { + return z.object({ + secretPath: z.string().min(1).refine((val) => isValidPath(val), { + message: "Vault secret path has to be a valid path" + }), + vaultEnginePath: z + .string() + .min(1) + .refine((val) => isValidPath(val), { + message: "Vault engine path has to be a valid path" + }), + vaultSecretPath: z + .string() + .min(1) + .refine((val) => isValidPath(val), { + message: "Vault secret path has to be a valid path" + }), + selectedSourceEnvironment: z.enum(availableEnvironmentNames as any as [string, ...string[]]) + }); +}; + +type TForm = z.infer>; + export default function HashiCorpVaultCreateIntegrationPage() { const router = useRouter(); const { mutateAsync } = useCreateIntegration(); @@ -37,54 +67,48 @@ export default function HashiCorpVaultCreateIntegrationPage() { (integrationAuthId as string) ?? "" ); - const [vaultEnginePath, setVaultEnginePath] = useState(""); - const [vaultEnginePathErrorText, setVaultEnginePathErrorText] = useState(""); + const formSchema = useMemo(() => { + return generateFormSchema(workspace?.environments.map((env) => env.slug) ?? []); + }, [workspace?.environments]); - const [vaultSecretPath, setVaultSecretPath] = useState(""); - const [vaultSecretPathErrorText, setVaultSecretPathErrorText] = useState(""); + const { + control, + handleSubmit, + formState: { isSubmitting } + } = useForm({ + resolver: zodResolver(formSchema), + defaultValues: { + secretPath: "/", + vaultEnginePath: "", + vaultSecretPath: "", + selectedSourceEnvironment: "" + } + }); - const [selectedSourceEnvironment, setSelectedSourceEnvironment] = useState(""); - const [secretPath, setSecretPath] = useState("/"); - const [isLoading, setIsLoading] = useState(false); - - const isValidVaultPath = (vaultPath: string) => { - return !(vaultPath.length === 0 || vaultPath.startsWith("/") || vaultPath.endsWith("/")); - }; - - const handleButtonClick = async () => { + const handleFormSubmit = async (formData: TForm) => { try { if (!integrationAuth?.id) return; - - if (!isValidVaultPath(vaultEnginePath)) { - setVaultEnginePathErrorText("Vault KV Secrets Engine Path must be valid like kv"); - } else { - setVaultEnginePathErrorText(""); - } - - if (!isValidVaultPath(vaultSecretPath)) { - setVaultSecretPathErrorText("Vault Secret(s) Path must be valid like machine/dev"); - } else { - setVaultSecretPathErrorText(""); - } - - if (!isValidVaultPath || !isValidVaultPath(vaultSecretPath)) return; - - setIsLoading(true); - await mutateAsync({ integrationAuthId: integrationAuth?.id, isActive: true, - app: vaultEnginePath, - sourceEnvironment: selectedSourceEnvironment, - path: vaultSecretPath, - secretPath + app: formData.vaultEnginePath, + sourceEnvironment: formData.selectedSourceEnvironment, + path: formData.vaultSecretPath, + secretPath: formData.secretPath }); - - setIsLoading(false); - router.push(`/integrations/${localStorage.getItem("projectData.id")}`); } catch (err) { console.error(err); + let errorMessage: string = "Something went wrong!"; + if (axios.isAxiosError(err)) { + const { message } = err?.response?.data as { message: string }; + errorMessage = message; + } + + createNotification({ + text: errorMessage, + type: "error" + }); } }; @@ -100,7 +124,7 @@ export default function HashiCorpVaultCreateIntegrationPage() { subTitle="Select which environment or folder in Infisical you want to sync to which path in HashiCorp Vault." >
-
+
- - - - - setSecretPath(evt.target.value)} - placeholder="Provide a path, default is /" - /> - - - setVaultEnginePath(e.target.value)} - /> - - - setVaultSecretPath(e.target.value)} - /> - - + +
+ ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + ( + + + + )} + /> + + + +
diff --git a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx index 1b6473bfe..c01332874 100644 --- a/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx +++ b/frontend/src/views/IntegrationsPage/components/IntegrationsSection/IntegrationsSection.tsx @@ -238,7 +238,7 @@ export const IntegrationsSection = ({ } >
-
Sync Status
+
{integration.isSynced ? "Synced" : "Not synced"}
{!integration.isSynced && }