diff --git a/ui/CHANGELOG.md b/ui/CHANGELOG.md index dddb9a555a..8fc322b22d 100644 --- a/ui/CHANGELOG.md +++ b/ui/CHANGELOG.md @@ -30,6 +30,7 @@ All notable changes to the **Prowler UI** are documented in this file. - Add `Provider UID` filter to scans page. [(#7820)](https://github.com/prowler-cloud/prowler/pull/7820) - Aligned Next.js version to `v14.2.29` across Prowler and Cloud environments for consistency and improved maintainability. [(#7962)](https://github.com/prowler-cloud/prowler/pull/7962) +- Refactor credentials forms with reusable components and error handling. [(#7988)](https://github.com/prowler-cloud/prowler/pull/7988) --- diff --git a/ui/actions/providers/providers.ts b/ui/actions/providers/providers.ts index 9bf9826c47..922041d487 100644 --- a/ui/actions/providers/providers.ts +++ b/ui/actions/providers/providers.ts @@ -7,9 +7,17 @@ import { apiBaseUrl, getAuthHeaders, getErrorMessage, + getFormValue, parseStringify, wait, } from "@/lib"; +import { + buildSecretConfig, + buildUpdateSecretConfig, + handleApiError, + handleApiResponse, +} from "@/lib/provider-credentials/build-crendentials"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; import { ProvidersApiResponse, ProviderType } from "@/types/providers"; export const getProviders = async ({ @@ -74,10 +82,8 @@ export const getProvider = async (formData: FormData) => { export const updateProvider = async (formData: FormData) => { const headers = await getAuthHeaders({ contentType: true }); - - const providerId = formData.get("providerId"); - const providerAlias = formData.get("alias"); - + const providerId = formData.get(ProviderCredentialFields.PROVIDER_ID); + const providerAlias = formData.get(ProviderCredentialFields.PROVIDER_ALIAS); const url = new URL(`${apiBaseUrl}/providers/${providerId}`); try { @@ -88,22 +94,14 @@ export const updateProvider = async (formData: FormData) => { data: { type: "providers", id: providerId, - attributes: { - alias: providerAlias, - }, + attributes: { alias: providerAlias }, }, }), }); - const data = await response.json(); - revalidatePath("/providers"); - return parseStringify(data); + return handleApiResponse(response, "/providers"); } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - return { - error: getErrorMessage(error), - }; + return handleApiError(error); } }; @@ -150,120 +148,37 @@ export const addCredentialsProvider = async (formData: FormData) => { const headers = await getAuthHeaders({ contentType: true }); const url = new URL(`${apiBaseUrl}/providers/secrets`); - const secretName = formData.get("secretName"); - const providerId = formData.get("providerId"); - const providerType = formData.get("providerType") as ProviderType; - - const isRole = formData.get("role_arn") !== null; - const isServiceAccount = formData.get("service_account_key") !== null; - - let secret = {}; - let secretType = "static"; // Default to static credentials - - if (providerType === "aws") { - if (isRole) { - // Role-based configuration for AWS - secretType = "role"; - secret = { - role_arn: formData.get("role_arn"), - external_id: formData.get("external_id"), - aws_access_key_id: formData.get("aws_access_key_id") || undefined, - aws_secret_access_key: - formData.get("aws_secret_access_key") || undefined, - aws_session_token: formData.get("aws_session_token") || undefined, - session_duration: - parseInt(formData.get("session_duration") as string, 10) || 3600, - role_session_name: formData.get("role_session_name") || undefined, - }; - } else { - // Static credentials configuration for AWS - secret = { - aws_access_key_id: formData.get("aws_access_key_id"), - aws_secret_access_key: formData.get("aws_secret_access_key"), - aws_session_token: formData.get("aws_session_token") || undefined, - }; - } - } else if (providerType === "azure") { - // Static credentials configuration for Azure - secret = { - client_id: formData.get("client_id"), - client_secret: formData.get("client_secret"), - tenant_id: formData.get("tenant_id"), - }; - } else if (providerType === "m365") { - // Static credentials configuration for M365 - secret = { - client_id: formData.get("client_id"), - client_secret: formData.get("client_secret"), - tenant_id: formData.get("tenant_id"), - user: formData.get("user"), - password: formData.get("password"), - }; - } else if (providerType === "gcp") { - if (isServiceAccount) { - // Service account configuration for GCP - secretType = "service_account"; - const serviceAccountKeyRaw = formData.get( - "service_account_key", - ) as string; - - try { - const serviceAccountKey = JSON.parse(serviceAccountKeyRaw); - secret = { - service_account_key: serviceAccountKey, - }; - } catch (error) { - // eslint-disable-next-line no-console - console.error("error", error); - } - } else { - // Static credentials configuration for GCP - secret = { - client_id: formData.get("client_id"), - client_secret: formData.get("client_secret"), - refresh_token: formData.get("refresh_token"), - }; - } - } else if (providerType === "kubernetes") { - // Static credentials configuration for Kubernetes - secret = { - kubeconfig_content: formData.get("kubeconfig_content"), - }; - } - const bodyData = { - data: { - type: "provider-secrets", - attributes: { - secret_type: secretType, - secret, - name: secretName, - }, - relationships: { - provider: { - data: { - id: providerId, - type: "providers", - }, - }, - }, - }, - }; + const providerId = getFormValue( + formData, + ProviderCredentialFields.PROVIDER_ID, + ); + const providerType = getFormValue( + formData, + ProviderCredentialFields.PROVIDER_TYPE, + ) as ProviderType; try { + const { secretType, secret } = buildSecretConfig(formData, providerType); + const response = await fetch(url.toString(), { method: "POST", headers, - body: JSON.stringify(bodyData), + body: JSON.stringify({ + data: { + type: "provider-secrets", + attributes: { secret_type: secretType, secret }, + relationships: { + provider: { + data: { id: providerId, type: "providers" }, + }, + }, + }, + }), }); - const data = await response.json(); - revalidatePath("/providers"); - return parseStringify(data); + + return handleApiResponse(response, "/providers"); } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - return { - error: getErrorMessage(error), - }; + return handleApiError(error); } }; @@ -273,139 +188,48 @@ export const updateCredentialsProvider = async ( ) => { const headers = await getAuthHeaders({ contentType: true }); const url = new URL(`${apiBaseUrl}/providers/secrets/${credentialsId}`); - - const secretName = formData.get("secretName"); - const providerType = formData.get("providerType") as ProviderType; - - const isRole = formData.get("role_arn") !== null; - const isServiceAccount = formData.get("service_account_key") !== null; - - let secret = {}; - - if (providerType === "aws") { - if (isRole) { - // Role-based configuration for AWS - secret = { - role_arn: formData.get("role_arn"), - aws_access_key_id: formData.get("aws_access_key_id") || undefined, - aws_secret_access_key: - formData.get("aws_secret_access_key") || undefined, - aws_session_token: formData.get("aws_session_token") || undefined, - session_duration: - parseInt(formData.get("session_duration") as string, 10) || 3600, - external_id: formData.get("external_id") || undefined, - role_session_name: formData.get("role_session_name") || undefined, - }; - } else { - // Static credentials configuration for AWS - secret = { - aws_access_key_id: formData.get("aws_access_key_id"), - aws_secret_access_key: formData.get("aws_secret_access_key"), - aws_session_token: formData.get("aws_session_token") || undefined, - }; - } - } else if (providerType === "azure") { - // Static credentials configuration for Azure - secret = { - client_id: formData.get("client_id"), - client_secret: formData.get("client_secret"), - tenant_id: formData.get("tenant_id"), - }; - } else if (providerType === "m365") { - // Static credentials configuration for M365 - secret = { - client_id: formData.get("client_id"), - client_secret: formData.get("client_secret"), - tenant_id: formData.get("tenant_id"), - user: formData.get("user"), - password: formData.get("password"), - }; - } else if (providerType === "gcp") { - if (isServiceAccount) { - // Service account configuration for GCP - const serviceAccountKeyRaw = formData.get( - "service_account_key", - ) as string; - - try { - // Parse the service account key as JSON - const serviceAccountKey = JSON.parse(serviceAccountKeyRaw); - secret = { - service_account_key: serviceAccountKey, - }; - } catch (error) { - // eslint-disable-next-line no-console - console.error("error", error); - } - } else { - // Static credentials configuration for GCP - secret = { - client_id: formData.get("client_id"), - client_secret: formData.get("client_secret"), - refresh_token: formData.get("refresh_token"), - }; - } - } else if (providerType === "kubernetes") { - // Static credentials configuration for Kubernetes - secret = { - kubeconfig_content: formData.get("kubeconfig_content"), - }; - } - - const bodyData = { - data: { - type: "provider-secrets", - id: credentialsId, - attributes: { - name: secretName, - secret, - }, - }, - }; + const providerType = getFormValue( + formData, + ProviderCredentialFields.PROVIDER_TYPE, + ) as ProviderType; try { + const secret = buildUpdateSecretConfig(formData, providerType); + const response = await fetch(url.toString(), { method: "PATCH", headers, - body: JSON.stringify(bodyData), + body: JSON.stringify({ + data: { + type: "provider-secrets", + id: credentialsId, + attributes: { secret }, + }, + }), }); if (!response.ok) { - throw new Error(`Failed to update credentials: ${response.statusText}`); + const data = await response.json(); + return parseStringify(data); // Return API errors for UI handling } - const data = await response.json(); - revalidatePath("/providers"); - return parseStringify(data); + return handleApiResponse(response, "/providers"); } catch (error) { - // eslint-disable-next-line no-console - console.error(error); - return { - error: getErrorMessage(error), - }; + return handleApiError(error); } }; export const checkConnectionProvider = async (formData: FormData) => { const headers = await getAuthHeaders({ contentType: false }); - - const providerId = formData.get("providerId"); - + const providerId = formData.get(ProviderCredentialFields.PROVIDER_ID); const url = new URL(`${apiBaseUrl}/providers/${providerId}/connection`); try { - const response = await fetch(url.toString(), { - method: "POST", - headers, - }); - const data = await response.json(); + const response = await fetch(url.toString(), { method: "POST", headers }); await wait(2000); - revalidatePath("/providers"); - return parseStringify(data); + return handleApiResponse(response, "/providers"); } catch (error) { - return { - error: getErrorMessage(error), - }; + return handleApiError(error); } }; @@ -451,7 +275,7 @@ export const deleteCredentials = async (secretId: string) => { export const deleteProvider = async (formData: FormData) => { const headers = await getAuthHeaders({ contentType: false }); - const providerId = formData.get("id"); + const providerId = formData.get(ProviderCredentialFields.PROVIDER_ID); if (!providerId) { return { error: "Provider ID is required" }; diff --git a/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx b/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx index 7f579c67ff..b867e919d0 100644 --- a/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx +++ b/ui/app/(prowler)/providers/(set-up-provider)/add-credentials/page.tsx @@ -1,13 +1,13 @@ import React from "react"; import { - ViaCredentialsForm, - ViaRoleForm, + AddViaCredentialsForm, + AddViaRoleForm, } from "@/components/providers/workflow/forms"; import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws"; import { + AddViaServiceAccountForm, SelectViaGCP, - ViaServiceAccountForm, } from "@/components/providers/workflow/forms/select-credentials-type/gcp"; import { ProviderType } from "@/types/providers"; @@ -29,16 +29,16 @@ export default function AddCredentialsPage({ searchParams }: Props) { {((searchParams.type === "aws" && searchParams.via === "credentials") || (searchParams.type === "gcp" && searchParams.via === "credentials") || (searchParams.type !== "aws" && searchParams.type !== "gcp")) && ( - + )} {searchParams.type === "aws" && searchParams.via === "role" && ( - + )} {searchParams.type === "gcp" && searchParams.via === "service-account" && ( - + )} ); diff --git a/ui/components/providers/forms/delete-form.tsx b/ui/components/providers/forms/delete-form.tsx index 2f80b3294b..056a6be2c4 100644 --- a/ui/components/providers/forms/delete-form.tsx +++ b/ui/components/providers/forms/delete-form.tsx @@ -10,9 +10,10 @@ import { DeleteIcon } from "@/components/icons"; import { useToast } from "@/components/ui"; import { CustomButton } from "@/components/ui/custom"; import { Form } from "@/components/ui/form"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; const formSchema = z.object({ - providerId: z.string(), + [ProviderCredentialFields.PROVIDER_ID]: z.string(), }); export const DeleteForm = ({ @@ -53,7 +54,11 @@ export const DeleteForm = ({ return (
- +
>({ resolver: zodResolver(formSchema), defaultValues: { - providerId: providerId, - alias: providerAlias, + [ProviderCredentialFields.PROVIDER_ID]: providerId, + [ProviderCredentialFields.PROVIDER_ALIAS]: providerAlias, }, }); @@ -74,14 +75,16 @@ export const EditForm = ({
diff --git a/ui/components/providers/workflow/forms/add-via-credentials-form.tsx b/ui/components/providers/workflow/forms/add-via-credentials-form.tsx new file mode 100644 index 0000000000..511b0aaf2d --- /dev/null +++ b/ui/components/providers/workflow/forms/add-via-credentials-form.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { addCredentialsProvider } from "@/actions/providers/providers"; +import { ProviderType } from "@/types"; + +import { BaseCredentialsForm } from "./base-credentials-form"; + +export const AddViaCredentialsForm = ({ + searchParams, +}: { + searchParams: { type: string; id: string }; +}) => { + const providerType = searchParams.type as ProviderType; + const providerId = searchParams.id; + + const handleAddCredentials = async (formData: FormData) => { + return await addCredentialsProvider(formData); + }; + + const successNavigationUrl = `/providers/test-connection?type=${providerType}&id=${providerId}`; + + return ( + + ); +}; diff --git a/ui/components/providers/workflow/forms/add-via-role-form.tsx b/ui/components/providers/workflow/forms/add-via-role-form.tsx new file mode 100644 index 0000000000..e7945450de --- /dev/null +++ b/ui/components/providers/workflow/forms/add-via-role-form.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { addCredentialsProvider } from "@/actions/providers/providers"; +import { ProviderType } from "@/types"; + +import { BaseCredentialsForm } from "./base-credentials-form"; + +export const AddViaRoleForm = ({ + searchParams, +}: { + searchParams: { type: string; id: string }; +}) => { + const providerType = searchParams.type as ProviderType; + const providerId = searchParams.id; + + const handleAddCredentials = async (formData: FormData) => { + return await addCredentialsProvider(formData); + }; + + const successNavigationUrl = `/providers/test-connection?type=${providerType}&id=${providerId}`; + + return ( + + ); +}; diff --git a/ui/components/providers/workflow/forms/base-credentials-form.tsx b/ui/components/providers/workflow/forms/base-credentials-form.tsx new file mode 100644 index 0000000000..39ab8ff6b3 --- /dev/null +++ b/ui/components/providers/workflow/forms/base-credentials-form.tsx @@ -0,0 +1,160 @@ +"use client"; + +import { Divider } from "@nextui-org/react"; +import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; +import { Control } from "react-hook-form"; + +import { CustomButton } from "@/components/ui/custom"; +import { Form } from "@/components/ui/form"; +import { useCredentialsForm } from "@/hooks/use-credentials-form"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; +import { + AWSCredentials, + AWSCredentialsRole, + AzureCredentials, + GCPDefaultCredentials, + GCPServiceAccountKey, + KubernetesCredentials, + M365Credentials, + ProviderType, +} from "@/types"; + +import { ProviderTitleDocs } from "../provider-title-docs"; +import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credentials-type"; +import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credentials-type/aws-role-credentials-form"; +import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type"; +import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type/gcp-service-account-key-form"; +import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form"; +import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form"; +import { M365CredentialsForm } from "./via-credentials/m365-credentials-form"; + +type BaseCredentialsFormProps = { + providerType: ProviderType; + providerId: string; + onSubmit: (formData: FormData) => Promise; + successNavigationUrl: string; + submitButtonText?: string; + showBackButton?: boolean; +}; + +export const BaseCredentialsForm = ({ + providerType, + providerId, + onSubmit, + successNavigationUrl, + submitButtonText = "Next", + showBackButton = true, +}: BaseCredentialsFormProps) => { + const { + form, + isLoading, + handleSubmit, + handleBackStep, + searchParamsObj, + externalId, + } = useCredentialsForm({ + providerType, + providerId, + onSubmit, + successNavigationUrl, + }); + + return ( + + + + + + + + + + {providerType === "aws" && searchParamsObj.get("via") === "role" && ( + } + setValue={form.setValue as any} + externalId={externalId} + /> + )} + {providerType === "aws" && searchParamsObj.get("via") !== "role" && ( + } + /> + )} + {providerType === "azure" && ( + } + /> + )} + {providerType === "m365" && ( + } + /> + )} + {providerType === "gcp" && + searchParamsObj.get("via") === "service-account" && ( + } + /> + )} + {providerType === "gcp" && + searchParamsObj.get("via") !== "service-account" && ( + + } + /> + )} + {providerType === "kubernetes" && ( + } + /> + )} + +
+ {showBackButton && + (searchParamsObj.get("via") === "credentials" || + searchParamsObj.get("via") === "role" || + searchParamsObj.get("via") === "service-account") && ( + } + isDisabled={isLoading} + > + Back + + )} + } + > + {isLoading ? <>Loading : {submitButtonText}} + +
+ + + ); +}; diff --git a/ui/components/providers/workflow/forms/index.ts b/ui/components/providers/workflow/forms/index.ts index fb8abdb8b4..66ecf382d8 100644 --- a/ui/components/providers/workflow/forms/index.ts +++ b/ui/components/providers/workflow/forms/index.ts @@ -1,6 +1,6 @@ +export * from "./add-via-credentials-form"; +export * from "./add-via-role-form"; export * from "./connect-account-form"; export * from "./test-connection-form"; export * from "./update-via-credentials-form"; export * from "./update-via-role-form"; -export * from "./via-credentials-form"; -export * from "./via-role-form"; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx index 89c1fef8c2..ebc6c35982 100644 --- a/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/select-credentials-type/aws/credentials-type/aws-role-credentials-form.tsx @@ -3,6 +3,7 @@ import { Control, UseFormSetValue, useWatch } from "react-hook-form"; import { CredentialsRoleHelper } from "@/components/providers/workflow"; import { CustomInput } from "@/components/ui/custom"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; import { AWSCredentialsRole } from "@/types"; export const AWSRoleCredentialsForm = ({ @@ -16,7 +17,7 @@ export const AWSRoleCredentialsForm = ({ }) => { const credentialsType = useWatch({ control, - name: "credentials_type" as const, + name: ProviderCredentialFields.CREDENTIALS_TYPE, defaultValue: "aws-sdk-default", }); @@ -34,7 +35,7 @@ export const AWSRoleCredentialsForm = ({ Authentication - - - - - - - {providerType === "gcp" && ( - } - /> - )} - -
- {searchParamsObj.get("via") === "service-account" && ( - } - isDisabled={isLoading} - > - Back - - )} - } - > - {isLoading ? <>Loading : Next} - -
- - - ); -}; diff --git a/ui/components/providers/workflow/forms/update-via-credentials-form.tsx b/ui/components/providers/workflow/forms/update-via-credentials-form.tsx index 76da05efcb..613882b381 100644 --- a/ui/components/providers/workflow/forms/update-via-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/update-via-credentials-form.tsx @@ -1,266 +1,32 @@ "use client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Divider } from "@nextui-org/react"; -import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Control, useForm } from "react-hook-form"; -import * as z from "zod"; - import { updateCredentialsProvider } from "@/actions/providers/providers"; -import { useToast } from "@/components/ui"; -import { CustomButton } from "@/components/ui/custom"; -import { Form } from "@/components/ui/form"; import { ProviderType } from "@/types"; -import { - addCredentialsFormSchema, - ApiError, - AWSCredentials, - AzureCredentials, - GCPDefaultCredentials, - KubernetesCredentials, - M365Credentials, -} from "@/types"; -import { ProviderTitleDocs } from "../provider-title-docs"; -import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credentials-type"; -import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type"; -import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form"; -import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form"; -import { M365CredentialsForm } from "./via-credentials/m365-credentials-form"; - -type CredentialsFormSchema = z.infer< - ReturnType ->; - -// Add this type intersection to include all fields -type FormType = CredentialsFormSchema & - AWSCredentials & - AzureCredentials & - M365Credentials & - GCPDefaultCredentials & - KubernetesCredentials; +import { BaseCredentialsForm } from "./base-credentials-form"; export const UpdateViaCredentialsForm = ({ searchParams, }: { searchParams: { type: string; id: string; secretId?: string }; }) => { - const router = useRouter(); - const { toast } = useToast(); - - const searchParamsObj = useSearchParams(); - - // Handler for back button - const handleBackStep = () => { - const currentParams = new URLSearchParams(window.location.search); - currentParams.delete("via"); - router.push(`?${currentParams.toString()}`); - }; - const providerType = searchParams.type as ProviderType; const providerId = searchParams.id; const providerSecretId = searchParams.secretId || ""; - const formSchema = addCredentialsFormSchema(providerType); - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - providerId, - providerType, - ...(providerType === "aws" - ? { - aws_access_key_id: "", - aws_secret_access_key: "", - aws_session_token: "", - } - : providerType === "azure" - ? { - client_id: "", - client_secret: "", - tenant_id: "", - } - : providerType === "m365" - ? { - client_id: "", - client_secret: "", - tenant_id: "", - user: "", - password: "", - } - : providerType === "gcp" - ? { - client_id: "", - client_secret: "", - refresh_token: "", - } - : providerType === "kubernetes" - ? { - kubeconfig_content: "", - } - : {}), - }, - }); - - const isLoading = form.formState.isSubmitting; - - const onSubmitClient = async (values: FormType) => { - const formData = new FormData(); - - Object.entries(values).forEach( - ([key, value]) => value !== undefined && formData.append(key, value), - ); - - const data = await updateCredentialsProvider(providerSecretId, formData); - - if (data?.errors && data.errors.length > 0) { - data.errors.forEach((error: ApiError) => { - const errorMessage = error.detail; - switch (error.source.pointer) { - case "/data/attributes/secret/aws_access_key_id": - form.setError("aws_access_key_id", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/aws_secret_access_key": - form.setError("aws_secret_access_key", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/aws_session_token": - form.setError("aws_session_token", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/client_id": - form.setError("client_id", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/client_secret": - form.setError("client_secret", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/user": - form.setError("user", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/password": - form.setError("password", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/tenant_id": - form.setError("tenant_id", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/kubeconfig_content": - form.setError("kubeconfig_content", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/name": - form.setError("secretName", { - type: "server", - message: errorMessage, - }); - break; - default: - toast({ - variant: "destructive", - title: "Oops! Something went wrong", - description: errorMessage, - }); - } - }); - } else { - router.push( - `/providers/test-connection?type=${providerType}&id=${providerId}&updated=true`, - ); - } + const handleUpdateCredentials = async (formData: FormData) => { + return await updateCredentialsProvider(providerSecretId, formData); }; + const successNavigationUrl = `/providers/test-connection?type=${providerType}&id=${providerId}&updated=true`; + return ( -
- - - - - - - - - {providerType === "aws" && ( - } - /> - )} - {providerType === "azure" && ( - } - /> - )} - {providerType === "m365" && ( - } - /> - )} - {providerType === "gcp" && ( - } - /> - )} - {providerType === "kubernetes" && ( - } - /> - )} - -
- {searchParamsObj.get("via") === "credentials" && ( - } - isDisabled={isLoading} - > - Back - - )} - } - > - {isLoading ? <>Loading : Next} - -
- - + ); }; diff --git a/ui/components/providers/workflow/forms/update-via-role-form.tsx b/ui/components/providers/workflow/forms/update-via-role-form.tsx index b207a27bbc..6385cac7f8 100644 --- a/ui/components/providers/workflow/forms/update-via-role-form.tsx +++ b/ui/components/providers/workflow/forms/update-via-role-form.tsx @@ -1,195 +1,32 @@ "use client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useSession } from "next-auth/react"; -import { Control, useForm, UseFormSetValue } from "react-hook-form"; -import * as z from "zod"; - import { updateCredentialsProvider } from "@/actions/providers/providers"; -import { useToast } from "@/components/ui"; -import { CustomButton } from "@/components/ui/custom"; -import { Form } from "@/components/ui/form"; -import { - addCredentialsRoleFormSchema, - ApiError, - AWSCredentialsRole, -} from "@/types"; +import { ProviderType } from "@/types"; -import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credentials-type"; +import { BaseCredentialsForm } from "./base-credentials-form"; export const UpdateViaRoleForm = ({ searchParams, }: { searchParams: { type: string; id: string; secretId?: string }; }) => { - const router = useRouter(); - const { toast } = useToast(); - const { data: session } = useSession(); - - const searchParamsObj = useSearchParams(); - - // Extract values from searchParams - const providerType = searchParams.type; + const providerType = searchParams.type as ProviderType; const providerId = searchParams.id; const providerSecretId = searchParams.secretId || ""; - const externalId = session?.tenantId; - const formSchema = addCredentialsRoleFormSchema(providerType); - type FormSchemaType = z.infer & { - credentials_type: "aws-sdk-default" | "access-secret-key"; + const handleUpdateCredentials = async (formData: FormData) => { + return await updateCredentialsProvider(providerSecretId, formData); }; - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - providerId, - providerType, - credentials_type: "aws-sdk-default", - ...(providerType === "aws" && { - role_arn: "", - external_id: externalId, - aws_access_key_id: "", - aws_secret_access_key: "", - aws_session_token: "", - role_session_name: "", - session_duration: "3600", - }), - }, - }); - - const isLoading = form.formState.isSubmitting; - - // Handle form submission - const onSubmitClient = async (values: FormSchemaType) => { - try { - const formData = new FormData(); - - Object.entries(values).forEach(([key, value]) => { - if (key === "credentials_type") return; - - if ( - values.credentials_type === "access-secret-key" && - [ - "aws_access_key_id", - "aws_secret_access_key", - "aws_session_token", - ].includes(key) - ) { - if (value !== undefined && value !== "") { - formData.append(key, String(value)); - } - return; - } - - if (value !== undefined && value !== "") { - formData.append(key, String(value)); - } - }); - - const data = await updateCredentialsProvider(providerSecretId, formData); - - // Handle errors - if (data?.errors?.length) { - data.errors.forEach((error: ApiError) => { - const errorMessage = error.detail; - switch (error.source.pointer) { - case "/data/attributes/secret/role_arn": - form.setError("role_arn" as keyof FormSchemaType, { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/external_id": - form.setError("external_id" as keyof FormSchemaType, { - type: "server", - message: errorMessage, - }); - break; - default: - toast({ - variant: "destructive", - title: "Oops! Something went wrong", - description: errorMessage, - }); - } - }); - } else { - // Redirect on success - router.push( - `/providers/test-connection?type=${providerType}&id=${providerId}&updated=true`, - ); - } - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error during submission:", error); - toast({ - variant: "destructive", - title: "Submission failed", - description: "An error occurred while processing your request.", - }); - } - }; - - // Handle back navigation - const handleBackStep = () => { - const currentParams = new URLSearchParams(window.location.search); - currentParams.delete("via"); - router.push(`?${currentParams.toString()}`); - }; + const successNavigationUrl = `/providers/test-connection?type=${providerType}&id=${providerId}&updated=true`; return ( -
- - - - - {/* Conditional AWS Form */} - {providerType === "aws" && ( - } - setValue={ - form.setValue as unknown as UseFormSetValue - } - externalId={externalId || ""} - /> - )} - - {/* Action Buttons */} -
- {searchParamsObj.get("via") === "role" && ( - } - isDisabled={isLoading} - > - Back - - )} - } - > - {isLoading ? <>Loading : Next} - -
- - + ); }; diff --git a/ui/components/providers/workflow/forms/update-via-service-account-key-form.tsx b/ui/components/providers/workflow/forms/update-via-service-account-key-form.tsx index 17991cb90c..a1db92bbdd 100644 --- a/ui/components/providers/workflow/forms/update-via-service-account-key-form.tsx +++ b/ui/components/providers/workflow/forms/update-via-service-account-key-form.tsx @@ -1,169 +1,32 @@ "use client"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Divider } from "@nextui-org/react"; -import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Control, useForm } from "react-hook-form"; -import * as z from "zod"; - import { updateCredentialsProvider } from "@/actions/providers/providers"; -import { ProviderTitleDocs } from "@/components/providers/workflow"; -import { useToast } from "@/components/ui"; -import { CustomButton } from "@/components/ui/custom"; -import { Form } from "@/components/ui/form"; -import { - addCredentialsServiceAccountFormSchema, - ApiError, - GCPServiceAccountKey, - ProviderType, -} from "@/types"; +import { ProviderType } from "@/types"; -import { GCPServiceAccountKeyForm } from "./select-credentials-type/gcp/credentials-type"; +import { BaseCredentialsForm } from "./base-credentials-form"; export const UpdateViaServiceAccountForm = ({ searchParams, }: { searchParams: { type: string; id: string; secretId?: string }; }) => { - const router = useRouter(); - const { toast } = useToast(); - const searchParamsObj = useSearchParams(); - - // Handler for back button - const handleBackStep = () => { - const currentParams = new URLSearchParams(window.location.search); - currentParams.delete("via"); - router.push(`?${currentParams.toString()}`); - }; - const providerType = searchParams.type as ProviderType; const providerId = searchParams.id; const providerSecretId = searchParams.secretId || ""; - const formSchema = addCredentialsServiceAccountFormSchema(providerType); - type FormSchemaType = z.infer; - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - providerId, - providerType, - ...(providerType === "gcp" - ? { - service_account_key: "", - secretName: "", - } - : {}), - }, - }); - - const isLoading = form.formState.isSubmitting; - - const onSubmitClient = async (values: FormSchemaType) => { - if (!providerSecretId) { - toast({ - variant: "destructive", - title: "Missing Secret ID", - description: "Cannot update credentials without a valid secret ID.", - }); - return; - } - - const formData = new FormData(); - - Object.entries(values).forEach(([key, value]) => { - if (value !== undefined && value !== "") { - formData.append(key, String(value)); - } - }); - - try { - const data = await updateCredentialsProvider(providerSecretId, formData); - if (data?.errors && data.errors.length > 0) { - data.errors.forEach((error: ApiError) => { - const errorMessage = error.detail; - - switch (error.source.pointer) { - case "/data/attributes/secret/service_account_key": - form.setError("service_account_key" as keyof FormSchemaType, { - type: "server", - message: errorMessage, - }); - break; - default: - toast({ - variant: "destructive", - title: "Oops! Something went wrong", - description: errorMessage, - }); - } - }); - } else { - router.push( - `/providers/test-connection?type=${providerType}&id=${providerId}&updated=true`, - ); - } - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error during submission:", error); - toast({ - variant: "destructive", - title: "Submission failed", - description: "An error occurred while processing your request.", - }); - } + const handleUpdateCredentials = async (formData: FormData) => { + return await updateCredentialsProvider(providerSecretId, formData); }; + const successNavigationUrl = `/providers/test-connection?type=${providerType}&id=${providerId}&updated=true`; + return ( -
- - - - - - - - - {providerType === "gcp" && ( - } - /> - )} - -
- {searchParamsObj.get("via") === "service-account" && ( - } - isDisabled={isLoading} - > - Back - - )} - } - > - {isLoading ? <>Loading : Next} - -
- - + ); }; diff --git a/ui/components/providers/workflow/forms/via-credentials-form.tsx b/ui/components/providers/workflow/forms/via-credentials-form.tsx deleted file mode 100644 index d73a875872..0000000000 --- a/ui/components/providers/workflow/forms/via-credentials-form.tsx +++ /dev/null @@ -1,265 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { Divider } from "@nextui-org/divider"; -import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { Control, useForm } from "react-hook-form"; -import * as z from "zod"; - -import { addCredentialsProvider } from "@/actions/providers/providers"; -import { useToast } from "@/components/ui"; -import { CustomButton } from "@/components/ui/custom"; -import { Form } from "@/components/ui/form"; -import { - addCredentialsFormSchema, - ApiError, - AWSCredentials, - AzureCredentials, - GCPDefaultCredentials, - KubernetesCredentials, - M365Credentials, - ProviderType, -} from "@/types"; - -import { ProviderTitleDocs } from "../provider-title-docs"; -import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credentials-type"; -import { GCPDefaultCredentialsForm } from "./select-credentials-type/gcp/credentials-type"; -import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form"; -import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form"; -import { M365CredentialsForm } from "./via-credentials/m365-credentials-form"; - -type CredentialsFormSchema = z.infer< - ReturnType ->; - -// Add this type intersection to include all fields -type FormType = CredentialsFormSchema & - AWSCredentials & - AzureCredentials & - GCPDefaultCredentials & - KubernetesCredentials & - M365Credentials; - -export const ViaCredentialsForm = ({ - searchParams, -}: { - searchParams: { type: string; id: string }; -}) => { - const router = useRouter(); - const { toast } = useToast(); - - const searchParamsObj = useSearchParams(); - - // Handler for back button - const handleBackStep = () => { - const currentParams = new URLSearchParams(window.location.search); - currentParams.delete("via"); - router.push(`?${currentParams.toString()}`); - }; - - const providerType = searchParams.type as ProviderType; - const providerId = searchParams.id; - const formSchema = addCredentialsFormSchema(providerType); - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - providerId, - providerType, - ...(providerType === "aws" - ? { - aws_access_key_id: "", - aws_secret_access_key: "", - aws_session_token: "", - } - : providerType === "azure" - ? { - client_id: "", - client_secret: "", - tenant_id: "", - } - : providerType === "m365" - ? { - client_id: "", - client_secret: "", - tenant_id: "", - user: "", - password: "", - } - : providerType === "gcp" - ? { - client_id: "", - client_secret: "", - refresh_token: "", - } - : providerType === "kubernetes" - ? { - kubeconfig_content: "", - } - : {}), - }, - }); - - const isLoading = form.formState.isSubmitting; - - const onSubmitClient = async (values: FormType) => { - const formData = new FormData(); - - Object.entries(values).forEach( - ([key, value]) => value !== undefined && formData.append(key, value), - ); - - const data = await addCredentialsProvider(formData); - - if (data?.errors && data.errors.length > 0) { - data.errors.forEach((error: ApiError) => { - const errorMessage = error.detail; - switch (error.source.pointer) { - case "/data/attributes/secret/aws_access_key_id": - form.setError("aws_access_key_id", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/aws_secret_access_key": - form.setError("aws_secret_access_key", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/aws_session_token": - form.setError("aws_session_token", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/client_id": - form.setError("client_id", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/client_secret": - form.setError("client_secret", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/user": - form.setError("user", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/password": - form.setError("password", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/tenant_id": - form.setError("tenant_id", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/kubeconfig_content": - form.setError("kubeconfig_content", { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/name": - form.setError("secretName", { - type: "server", - message: errorMessage, - }); - break; - default: - toast({ - variant: "destructive", - title: "Oops! Something went wrong", - description: errorMessage, - }); - } - }); - } else { - router.push( - `/providers/test-connection?type=${providerType}&id=${providerId}`, - ); - } - }; - - return ( -
- - - - - - - - - {providerType === "aws" && ( - } - /> - )} - {providerType === "azure" && ( - } - /> - )} - {providerType === "m365" && ( - } - /> - )} - {providerType === "gcp" && ( - } - /> - )} - {providerType === "kubernetes" && ( - } - /> - )} - -
- {searchParamsObj.get("via") === "credentials" && ( - } - isDisabled={isLoading} - > - Back - - )} - } - > - {isLoading ? <>Loading : Next} - -
- - - ); -}; diff --git a/ui/components/providers/workflow/forms/via-role-form.tsx b/ui/components/providers/workflow/forms/via-role-form.tsx deleted file mode 100644 index 84c6bfc3e7..0000000000 --- a/ui/components/providers/workflow/forms/via-role-form.tsx +++ /dev/null @@ -1,193 +0,0 @@ -"use client"; - -import { zodResolver } from "@hookform/resolvers/zod"; -import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"; -import { useRouter, useSearchParams } from "next/navigation"; -import { useSession } from "next-auth/react"; -import { Control, useForm, UseFormSetValue } from "react-hook-form"; -import * as z from "zod"; - -import { addCredentialsProvider } from "@/actions/providers/providers"; -import { useToast } from "@/components/ui"; -import { CustomButton } from "@/components/ui/custom"; -import { Form } from "@/components/ui/form"; -import { - addCredentialsRoleFormSchema, - ApiError, - AWSCredentialsRole, -} from "@/types"; - -import { AWSRoleCredentialsForm } from "./select-credentials-type/aws/credentials-type"; - -export const ViaRoleForm = ({ - searchParams, -}: { - searchParams: { type: string; id: string }; -}) => { - const router = useRouter(); - const { toast } = useToast(); - const { data: session } = useSession(); - const searchParamsObj = useSearchParams(); - const externalId = session?.tenantId; - - // Handler for back button - const handleBackStep = () => { - const currentParams = new URLSearchParams(window.location.search); - currentParams.delete("via"); - router.push(`?${currentParams.toString()}`); - }; - - const providerType = searchParams.type; - const providerId = searchParams.id; - - const formSchema = addCredentialsRoleFormSchema(providerType); - type FormSchemaType = z.infer & { - credentials_type: "aws-sdk-default" | "access-secret-key"; - }; - - const form = useForm({ - resolver: zodResolver(formSchema), - defaultValues: { - providerId, - providerType, - credentials_type: "aws-sdk-default", - ...(providerType === "aws" - ? { - role_arn: "", - external_id: externalId, - aws_access_key_id: "", - aws_secret_access_key: "", - aws_session_token: "", - role_session_name: "", - session_duration: "3600", - } - : {}), - }, - }); - - const isLoading = form.formState.isSubmitting; - - const onSubmitClient = async (values: FormSchemaType) => { - const formData = new FormData(); - - Object.entries(values).forEach(([key, value]) => { - // Do not include credentials_type - if (key === "credentials_type") return; - - // If credentials_type is "access-secret-key", include the relevant fields - if ( - values.credentials_type === "access-secret-key" && - [ - "aws_access_key_id", - "aws_secret_access_key", - "aws_session_token", - ].includes(key) - ) { - if (value !== undefined && value !== "") { - formData.append(key, String(value)); - } - return; - } - - // Add any other valid field - if (value !== undefined && value !== "") { - formData.append(key, String(value)); - } - }); - - try { - const data = await addCredentialsProvider(formData); - - if (data?.errors && data.errors.length > 0) { - data.errors.forEach((error: ApiError) => { - const errorMessage = error.detail; - - switch (error.source.pointer) { - case "/data/attributes/secret/role_arn": - form.setError("role_arn" as keyof FormSchemaType, { - type: "server", - message: errorMessage, - }); - break; - case "/data/attributes/secret/external_id": - form.setError("external_id" as keyof FormSchemaType, { - type: "server", - message: errorMessage, - }); - break; - default: - toast({ - variant: "destructive", - title: "Oops! Something went wrong", - description: errorMessage, - }); - } - }); - } else { - router.push( - `/providers/test-connection?type=${providerType}&id=${providerId}`, - ); - } - } catch (error) { - // eslint-disable-next-line no-console - console.error("Error during submission:", error); - toast({ - variant: "destructive", - title: "Submission failed", - description: "An error occurred while processing your request.", - }); - } - }; - - return ( -
- - - - - {providerType === "aws" && ( - } - setValue={ - form.setValue as unknown as UseFormSetValue - } - externalId={externalId || ""} - /> - )} - -
- {searchParamsObj.get("via") === "role" && ( - } - isDisabled={isLoading} - > - Back - - )} - } - > - {isLoading ? <>Loading : Next} - -
- - - ); -}; diff --git a/ui/hooks/index.ts b/ui/hooks/index.ts index 2d03f3ba70..17921cb82a 100644 --- a/ui/hooks/index.ts +++ b/ui/hooks/index.ts @@ -1,3 +1,5 @@ +export * from "./use-credentials-form"; +export * from "./use-form-server-errors"; +export * from "./use-local-storage"; export * from "./use-sidebar"; export * from "./use-store"; -export * from "./useLocalStorage"; diff --git a/ui/hooks/use-credentials-form.ts b/ui/hooks/use-credentials-form.ts new file mode 100644 index 0000000000..df6ffa9517 --- /dev/null +++ b/ui/hooks/use-credentials-form.ts @@ -0,0 +1,168 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter, useSearchParams } from "next/navigation"; +import { useSession } from "next-auth/react"; +import { useForm } from "react-hook-form"; + +import { useFormServerErrors } from "@/hooks/use-form-server-errors"; +import { filterEmptyValues } from "@/lib"; +import { PROVIDER_CREDENTIALS_ERROR_MAPPING } from "@/lib/error-mappings"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; +import { + addCredentialsFormSchema, + addCredentialsRoleFormSchema, + addCredentialsServiceAccountFormSchema, + ProviderType, +} from "@/types"; + +type CredentialsFormData = { + providerId: string; + providerType: ProviderType; + [key: string]: any; +}; + +type UseCredentialsFormProps = { + providerType: ProviderType; + providerId: string; + onSubmit: (formData: FormData) => Promise; + successNavigationUrl: string; +}; + +export const useCredentialsForm = ({ + providerType, + providerId, + onSubmit, + successNavigationUrl, +}: UseCredentialsFormProps) => { + const router = useRouter(); + const searchParamsObj = useSearchParams(); + const { data: session } = useSession(); + const via = searchParamsObj.get("via"); + + // Select the appropriate schema based on provider type and via parameter + const getFormSchema = () => { + if (providerType === "aws" && via === "role") { + return addCredentialsRoleFormSchema(providerType); + } + if (providerType === "gcp" && via === "service-account") { + return addCredentialsServiceAccountFormSchema(providerType); + } + return addCredentialsFormSchema(providerType); + }; + + const formSchema = getFormSchema(); + + // Get default values based on provider type and via parameter + const getDefaultValues = (): CredentialsFormData => { + const baseDefaults = { + [ProviderCredentialFields.PROVIDER_ID]: providerId, + [ProviderCredentialFields.PROVIDER_TYPE]: providerType, + }; + + // AWS Role credentials + if (providerType === "aws" && via === "role") { + return { + ...baseDefaults, + [ProviderCredentialFields.CREDENTIALS_TYPE]: "aws-sdk-default", + [ProviderCredentialFields.ROLE_ARN]: "", + [ProviderCredentialFields.EXTERNAL_ID]: session?.tenantId || "", + [ProviderCredentialFields.AWS_ACCESS_KEY_ID]: "", + [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]: "", + [ProviderCredentialFields.AWS_SESSION_TOKEN]: "", + [ProviderCredentialFields.ROLE_SESSION_NAME]: "", + [ProviderCredentialFields.SESSION_DURATION]: "3600", + }; + } + + // GCP Service Account + if (providerType === "gcp" && via === "service-account") { + return { + ...baseDefaults, + [ProviderCredentialFields.SERVICE_ACCOUNT_KEY]: "", + }; + } + + switch (providerType) { + case "aws": + return { + ...baseDefaults, + [ProviderCredentialFields.AWS_ACCESS_KEY_ID]: "", + [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]: "", + [ProviderCredentialFields.AWS_SESSION_TOKEN]: "", + }; + case "azure": + return { + ...baseDefaults, + [ProviderCredentialFields.CLIENT_ID]: "", + [ProviderCredentialFields.CLIENT_SECRET]: "", + [ProviderCredentialFields.TENANT_ID]: "", + }; + case "m365": + return { + ...baseDefaults, + [ProviderCredentialFields.CLIENT_ID]: "", + [ProviderCredentialFields.CLIENT_SECRET]: "", + [ProviderCredentialFields.TENANT_ID]: "", + [ProviderCredentialFields.USER]: "", + [ProviderCredentialFields.PASSWORD]: "", + }; + case "gcp": + return { + ...baseDefaults, + [ProviderCredentialFields.CLIENT_ID]: "", + [ProviderCredentialFields.CLIENT_SECRET]: "", + [ProviderCredentialFields.REFRESH_TOKEN]: "", + }; + case "kubernetes": + return { + ...baseDefaults, + [ProviderCredentialFields.KUBECONFIG_CONTENT]: "", + }; + default: + return baseDefaults; + } + }; + + const form = useForm({ + resolver: zodResolver(formSchema), + defaultValues: getDefaultValues(), + }); + + const { handleServerResponse } = useFormServerErrors( + form, + PROVIDER_CREDENTIALS_ERROR_MAPPING, + ); + + // Handler for back button + const handleBackStep = () => { + const currentParams = new URLSearchParams(window.location.search); + currentParams.delete("via"); + router.push(`?${currentParams.toString()}`); + }; + + // Form submit handler + const handleSubmit = async (values: CredentialsFormData) => { + const formData = new FormData(); + + // Filter out empty values first, then append all remaining values + const filteredValues = filterEmptyValues(values); + Object.entries(filteredValues).forEach(([key, value]) => { + formData.append(key, value); + }); + + const data = await onSubmit(formData); + + const isSuccess = handleServerResponse(data); + if (isSuccess) { + router.push(successNavigationUrl); + } + }; + + return { + form, + isLoading: form.formState.isSubmitting, + handleSubmit, + handleBackStep, + searchParamsObj, + externalId: session?.tenantId || "", + }; +}; diff --git a/ui/hooks/use-form-server-errors.ts b/ui/hooks/use-form-server-errors.ts new file mode 100644 index 0000000000..d8d662bac7 --- /dev/null +++ b/ui/hooks/use-form-server-errors.ts @@ -0,0 +1,61 @@ +import { UseFormReturn } from "react-hook-form"; + +import { useToast } from "@/components/ui"; +import { ApiError } from "@/types"; + +/** + * Generic hook for handling server errors in forms + * Can be used across different types of forms, not just credential forms + */ +export const useFormServerErrors = >( + form: UseFormReturn, + customErrorMapping?: Record, +) => { + const { toast } = useToast(); + + const handleServerErrors = ( + errors: ApiError[], + errorMapping?: Record, + ) => { + errors.forEach((error: ApiError) => { + const errorMessage = error.detail; + const fieldName = errorMapping?.[error.source.pointer]; + + if (fieldName && fieldName in form.formState.defaultValues!) { + form.setError(fieldName as any, { + type: "server", + message: errorMessage, + }); + } else { + // Handle unknown error pointers with toast + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: errorMessage, + }); + } + }); + }; + + const handleServerResponse = ( + data: any, + errorMapping?: Record, + ) => { + // Check for both error (singular) and errors (plural) from server responses + if (data?.error) { + // Handle single error from server + toast({ + variant: "destructive", + title: "Oops! Something went wrong", + description: data.error, + }); + return false; // Indicates error occurred + } else if (data?.errors && data.errors.length > 0) { + handleServerErrors(data.errors, errorMapping || customErrorMapping); + return false; // Indicates error occurred + } + return true; // Indicates success + }; + + return { handleServerResponse, handleServerErrors }; +}; diff --git a/ui/hooks/useLocalStorage.ts b/ui/hooks/use-local-storage.ts similarity index 100% rename from ui/hooks/useLocalStorage.ts rename to ui/hooks/use-local-storage.ts diff --git a/ui/lib/error-mappings.ts b/ui/lib/error-mappings.ts new file mode 100644 index 0000000000..582dfd5696 --- /dev/null +++ b/ui/lib/error-mappings.ts @@ -0,0 +1,31 @@ +import { + ErrorPointers, + ProviderCredentialFields, +} from "./provider-credentials/provider-credential-fields"; + +/** + * Error pointer to field name mappings for different types of forms + * These can be imported and used with the useFormServerErrors hook + */ + +// Mapping for provider credentials forms +export const PROVIDER_CREDENTIALS_ERROR_MAPPING: Record = { + [ErrorPointers.AWS_ACCESS_KEY_ID]: ProviderCredentialFields.AWS_ACCESS_KEY_ID, + [ErrorPointers.AWS_SECRET_ACCESS_KEY]: + ProviderCredentialFields.AWS_SECRET_ACCESS_KEY, + [ErrorPointers.AWS_SESSION_TOKEN]: ProviderCredentialFields.AWS_SESSION_TOKEN, + [ErrorPointers.CLIENT_ID]: ProviderCredentialFields.CLIENT_ID, + [ErrorPointers.CLIENT_SECRET]: ProviderCredentialFields.CLIENT_SECRET, + [ErrorPointers.USER]: ProviderCredentialFields.USER, + [ErrorPointers.PASSWORD]: ProviderCredentialFields.PASSWORD, + [ErrorPointers.TENANT_ID]: ProviderCredentialFields.TENANT_ID, + [ErrorPointers.KUBECONFIG_CONTENT]: + ProviderCredentialFields.KUBECONFIG_CONTENT, + [ErrorPointers.REFRESH_TOKEN]: ProviderCredentialFields.REFRESH_TOKEN, + [ErrorPointers.ROLE_ARN]: ProviderCredentialFields.ROLE_ARN, + [ErrorPointers.EXTERNAL_ID]: ProviderCredentialFields.EXTERNAL_ID, + [ErrorPointers.SESSION_DURATION]: ProviderCredentialFields.SESSION_DURATION, + [ErrorPointers.ROLE_SESSION_NAME]: ProviderCredentialFields.ROLE_SESSION_NAME, + [ErrorPointers.SERVICE_ACCOUNT_KEY]: + ProviderCredentialFields.SERVICE_ACCOUNT_KEY, +}; diff --git a/ui/lib/helper.ts b/ui/lib/helper.ts index 7a13fb266f..69ba6d36b4 100644 --- a/ui/lib/helper.ts +++ b/ui/lib/helper.ts @@ -7,6 +7,44 @@ import { AuthSocialProvider, MetaDataProps, PermissionInfo } from "@/types"; export const baseUrl = process.env.AUTH_URL || "http://localhost:3000"; export const apiBaseUrl = process.env.API_BASE_URL; +/** + * Extracts a form value from a FormData object + * @param formData - The FormData object to extract from + * @param field - The name of the field to extract + * @returns The value of the field + */ +export const getFormValue = (formData: FormData, field: string) => + formData.get(field); + +/** + * Filters out empty values from an object + * @param obj - Object to filter + * @returns New object with empty values removed + * Avoids sending empty values to the API + */ +export function filterEmptyValues( + obj: Record, +): Record { + return Object.fromEntries( + Object.entries(obj).filter(([_, value]) => { + // Keep number 0 and boolean false as they are valid values + if (value === 0 || value === false) return true; + + // Filter out null, undefined, empty strings, and empty arrays + if (value === null || value === undefined) return false; + if (typeof value === "string" && value.trim() === "") return false; + if (Array.isArray(value) && value.length === 0) return false; + + return true; + }), + ); +} + +/** + * Returns the authentication headers for API requests + * @param options - Optional configuration options + * @returns Authentication headers with Accept and Authorization + */ export const getAuthHeaders = async (options?: { contentType?: boolean }) => { const session = await auth(); diff --git a/ui/lib/index.ts b/ui/lib/index.ts index 6e17065079..8d50d049be 100644 --- a/ui/lib/index.ts +++ b/ui/lib/index.ts @@ -1,3 +1,4 @@ +export * from "./error-mappings"; export * from "./external-urls"; export * from "./helper"; export * from "./helper-filters"; diff --git a/ui/lib/provider-credentials/build-crendentials.ts b/ui/lib/provider-credentials/build-crendentials.ts new file mode 100644 index 0000000000..2c0f6ee3de --- /dev/null +++ b/ui/lib/provider-credentials/build-crendentials.ts @@ -0,0 +1,229 @@ +import { revalidatePath } from "next/cache"; + +import { + filterEmptyValues, + getErrorMessage, + getFormValue, + parseStringify, +} from "@/lib"; +import { ProviderType } from "@/types"; + +import { ProviderCredentialFields } from "./provider-credential-fields"; + +// Helper functions for each provider type +export const buildAWSSecret = (formData: FormData, isRole: boolean) => { + if (isRole) { + const secret = { + [ProviderCredentialFields.ROLE_ARN]: getFormValue( + formData, + ProviderCredentialFields.ROLE_ARN, + ), + [ProviderCredentialFields.EXTERNAL_ID]: getFormValue( + formData, + ProviderCredentialFields.EXTERNAL_ID, + ), + [ProviderCredentialFields.AWS_ACCESS_KEY_ID]: getFormValue( + formData, + ProviderCredentialFields.AWS_ACCESS_KEY_ID, + ), + [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]: getFormValue( + formData, + ProviderCredentialFields.AWS_SECRET_ACCESS_KEY, + ), + [ProviderCredentialFields.AWS_SESSION_TOKEN]: getFormValue( + formData, + ProviderCredentialFields.AWS_SESSION_TOKEN, + ), + session_duration: + parseInt( + getFormValue( + formData, + ProviderCredentialFields.SESSION_DURATION, + ) as string, + 10, + ) || 3600, + [ProviderCredentialFields.ROLE_SESSION_NAME]: getFormValue( + formData, + ProviderCredentialFields.ROLE_SESSION_NAME, + ), + }; + return filterEmptyValues(secret); + } + + const secret = { + [ProviderCredentialFields.AWS_ACCESS_KEY_ID]: getFormValue( + formData, + ProviderCredentialFields.AWS_ACCESS_KEY_ID, + ), + [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]: getFormValue( + formData, + ProviderCredentialFields.AWS_SECRET_ACCESS_KEY, + ), + [ProviderCredentialFields.AWS_SESSION_TOKEN]: getFormValue( + formData, + ProviderCredentialFields.AWS_SESSION_TOKEN, + ), + }; + return filterEmptyValues(secret); +}; + +export const buildAzureSecret = (formData: FormData) => { + const secret = { + [ProviderCredentialFields.CLIENT_ID]: getFormValue( + formData, + ProviderCredentialFields.CLIENT_ID, + ), + [ProviderCredentialFields.CLIENT_SECRET]: getFormValue( + formData, + ProviderCredentialFields.CLIENT_SECRET, + ), + [ProviderCredentialFields.TENANT_ID]: getFormValue( + formData, + ProviderCredentialFields.TENANT_ID, + ), + }; + return filterEmptyValues(secret); +}; + +export const buildM365Secret = (formData: FormData) => { + const secret = { + ...buildAzureSecret(formData), + [ProviderCredentialFields.USER]: getFormValue( + formData, + ProviderCredentialFields.USER, + ), + [ProviderCredentialFields.PASSWORD]: getFormValue( + formData, + ProviderCredentialFields.PASSWORD, + ), + }; + return filterEmptyValues(secret); +}; + +export const buildGCPSecret = ( + formData: FormData, + isServiceAccount: boolean, +) => { + if (isServiceAccount) { + const serviceAccountKeyRaw = getFormValue( + formData, + ProviderCredentialFields.SERVICE_ACCOUNT_KEY, + ) as string; + + try { + return { + service_account_key: JSON.parse(serviceAccountKeyRaw), + }; + } catch (error) { + console.error("Invalid service account key JSON:", error); + throw new Error("Invalid service account key format"); + } + } + + const secret = { + [ProviderCredentialFields.CLIENT_ID]: getFormValue( + formData, + ProviderCredentialFields.CLIENT_ID, + ), + [ProviderCredentialFields.CLIENT_SECRET]: getFormValue( + formData, + ProviderCredentialFields.CLIENT_SECRET, + ), + [ProviderCredentialFields.REFRESH_TOKEN]: getFormValue( + formData, + ProviderCredentialFields.REFRESH_TOKEN, + ), + }; + return filterEmptyValues(secret); +}; + +export const buildKubernetesSecret = (formData: FormData) => { + const secret = { + [ProviderCredentialFields.KUBECONFIG_CONTENT]: getFormValue( + formData, + ProviderCredentialFields.KUBECONFIG_CONTENT, + ), + }; + return filterEmptyValues(secret); +}; + +// Main function to build secret configuration +export const buildSecretConfig = ( + formData: FormData, + providerType: ProviderType, +) => { + const isRole = formData.get(ProviderCredentialFields.ROLE_ARN) !== null; + const isServiceAccount = + formData.get(ProviderCredentialFields.SERVICE_ACCOUNT_KEY) !== null; + + const secretBuilders = { + aws: () => ({ + secretType: isRole ? "role" : "static", + secret: buildAWSSecret(formData, isRole), + }), + azure: () => ({ + secretType: "static", + secret: buildAzureSecret(formData), + }), + m365: () => ({ + secretType: "static", + secret: buildM365Secret(formData), + }), + gcp: () => ({ + secretType: isServiceAccount ? "service_account" : "static", + secret: buildGCPSecret(formData, isServiceAccount), + }), + kubernetes: () => ({ + secretType: "static", + secret: buildKubernetesSecret(formData), + }), + }; + + const builder = secretBuilders[providerType]; + if (!builder) { + throw new Error(`Unsupported provider type: ${providerType}`); + } + + return builder(); +}; + +// Helper function to build secret for update (reuses existing logic) +export const buildUpdateSecretConfig = ( + formData: FormData, + providerType: ProviderType, +) => { + // Reuse the same secret building logic as add, but only return the secret + const { secret } = buildSecretConfig(formData, providerType); + + // Handle special case for M365 password field inconsistency + if (providerType === "m365") { + return { + ...secret, + password: formData.get(ProviderCredentialFields.PASSWORD), + }; + } + + return secret; +}; + +// Helper function to handle API responses consistently +export const handleApiResponse = async ( + response: Response, + pathToRevalidate?: string, +) => { + const data = await response.json(); + + if (pathToRevalidate) { + revalidatePath(pathToRevalidate); + } + + return parseStringify(data); +}; + +// Helper function to handle API errors consistently +export const handleApiError = (error: unknown) => { + console.error(error); + return { + error: getErrorMessage(error), + }; +}; diff --git a/ui/lib/provider-credentials/provider-credential-fields.ts b/ui/lib/provider-credentials/provider-credential-fields.ts new file mode 100644 index 0000000000..5cbd6605b9 --- /dev/null +++ b/ui/lib/provider-credentials/provider-credential-fields.ts @@ -0,0 +1,64 @@ +/** + * Centralized credential field names to avoid hardcoded strings + * and provide type safety across the application + */ + +// Provider credential field names +export const ProviderCredentialFields = { + CREDENTIALS_TYPE: "credentials_type", + CREDENTIALS_TYPE_AWS: "aws-sdk-default", + CREDENTIALS_TYPE_ACCESS_SECRET_KEY: "access-secret-key", + // Base fields for all providers + PROVIDER_ID: "providerId", + PROVIDER_TYPE: "providerType", + PROVIDER_ALIAS: "providerAlias", + + // AWS fields + AWS_ACCESS_KEY_ID: "aws_access_key_id", + AWS_SECRET_ACCESS_KEY: "aws_secret_access_key", + AWS_SESSION_TOKEN: "aws_session_token", + ROLE_ARN: "role_arn", + EXTERNAL_ID: "external_id", + SESSION_DURATION: "session_duration", + ROLE_SESSION_NAME: "role_session_name", + + // Azure/M365 fields + CLIENT_ID: "client_id", + CLIENT_SECRET: "client_secret", + TENANT_ID: "tenant_id", + USER: "user", + PASSWORD: "password", + + // GCP fields + REFRESH_TOKEN: "refresh_token", + SERVICE_ACCOUNT_KEY: "service_account_key", + + // Kubernetes fields + KUBECONFIG_CONTENT: "kubeconfig_content", +} as const; + +// Type for credential field values +export type ProviderCredentialField = + (typeof ProviderCredentialFields)[keyof typeof ProviderCredentialFields]; + +// API error pointer paths +export const ErrorPointers = { + // Secret fields + AWS_ACCESS_KEY_ID: "/data/attributes/secret/aws_access_key_id", + AWS_SECRET_ACCESS_KEY: "/data/attributes/secret/aws_secret_access_key", + AWS_SESSION_TOKEN: "/data/attributes/secret/aws_session_token", + CLIENT_ID: "/data/attributes/secret/client_id", + CLIENT_SECRET: "/data/attributes/secret/client_secret", + USER: "/data/attributes/secret/user", + PASSWORD: "/data/attributes/secret/password", + TENANT_ID: "/data/attributes/secret/tenant_id", + KUBECONFIG_CONTENT: "/data/attributes/secret/kubeconfig_content", + REFRESH_TOKEN: "/data/attributes/secret/refresh_token", + ROLE_ARN: "/data/attributes/secret/role_arn", + EXTERNAL_ID: "/data/attributes/secret/external_id", + SESSION_DURATION: "/data/attributes/secret/session_duration", + ROLE_SESSION_NAME: "/data/attributes/secret/role_session_name", + SERVICE_ACCOUNT_KEY: "/data/attributes/secret/service_account_key", +} as const; + +export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers]; diff --git a/ui/types/components.ts b/ui/types/components.ts index c8a8054f80..6d9512f65e 100644 --- a/ui/types/components.ts +++ b/ui/types/components.ts @@ -1,6 +1,8 @@ import { LucideIcon } from "lucide-react"; import { SVGProps } from "react"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; + export type IconSvgProps = SVGProps & { size?: number; }; @@ -179,39 +181,38 @@ export interface TaskDetails { }; } export type AWSCredentials = { - aws_access_key_id: string; - aws_secret_access_key: string; - aws_session_token: string; - secretName: string; - providerId: string; + [ProviderCredentialFields.AWS_ACCESS_KEY_ID]: string; + [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]: string; + [ProviderCredentialFields.AWS_SESSION_TOKEN]: string; + [ProviderCredentialFields.PROVIDER_ID]: string; }; export type AWSCredentialsRole = { - role_arn: string; - aws_access_key_id?: string; - aws_secret_access_key?: string; - aws_session_token?: string; - external_id?: string; - role_session_name?: string; - session_duration?: number; - credentials_type?: "aws-sdk-default" | "access-secret-key"; + [ProviderCredentialFields.ROLE_ARN]: string; + [ProviderCredentialFields.AWS_ACCESS_KEY_ID]?: string; + [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]?: string; + [ProviderCredentialFields.AWS_SESSION_TOKEN]?: string; + [ProviderCredentialFields.EXTERNAL_ID]?: string; + [ProviderCredentialFields.ROLE_SESSION_NAME]?: string; + [ProviderCredentialFields.SESSION_DURATION]?: number; + [ProviderCredentialFields.CREDENTIALS_TYPE]?: + | "aws-sdk-default" + | "access-secret-key"; }; export type AzureCredentials = { - client_id: string; - client_secret: string; - tenant_id: string; - secretName: string; - providerId: string; + [ProviderCredentialFields.CLIENT_ID]: string; + [ProviderCredentialFields.CLIENT_SECRET]: string; + [ProviderCredentialFields.TENANT_ID]: string; + [ProviderCredentialFields.PROVIDER_ID]: string; }; export type M365Credentials = { - client_id: string; - client_secret: string; - tenant_id: string; - user: string; - password: string; - secretName: string; + [ProviderCredentialFields.CLIENT_ID]: string; + [ProviderCredentialFields.CLIENT_SECRET]: string; + [ProviderCredentialFields.TENANT_ID]: string; + [ProviderCredentialFields.USER]: string; + [ProviderCredentialFields.PASSWORD]: string; providerId: string; }; @@ -219,20 +220,17 @@ export type GCPDefaultCredentials = { client_id: string; client_secret: string; refresh_token: string; - secretName: string; providerId: string; }; export type GCPServiceAccountKey = { - service_account_key: string; - secretName: string; - providerId: string; + [ProviderCredentialFields.SERVICE_ACCOUNT_KEY]: string; + [ProviderCredentialFields.PROVIDER_ID]: string; }; export type KubernetesCredentials = { - kubeconfig_content: string; - secretName: string; - providerId: string; + [ProviderCredentialFields.KUBECONFIG_CONTENT]: string; + [ProviderCredentialFields.PROVIDER_ID]: string; }; export type CredentialsFormSchema = diff --git a/ui/types/formSchemas.ts b/ui/types/formSchemas.ts index a2fbf1e3a4..5feca84f37 100644 --- a/ui/types/formSchemas.ts +++ b/ui/types/formSchemas.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields"; + import { ProviderType } from "./providers"; export const addRoleFormSchema = z.object({ @@ -42,7 +44,7 @@ export const editScanFormSchema = (currentName: string) => export const onDemandScanFormSchema = () => z.object({ - providerId: z.string(), + [ProviderCredentialFields.PROVIDER_ID]: z.string(), scanName: z.string().optional(), scannerArgs: z .object({ @@ -73,30 +75,30 @@ export const addProviderFormSchema = z z.discriminatedUnion("providerType", [ z.object({ providerType: z.literal("aws"), - providerAlias: z.string(), + [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), providerUid: z.string(), }), z.object({ providerType: z.literal("azure"), - providerAlias: z.string(), + [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), providerUid: z.string(), awsCredentialsType: z.string().optional(), }), z.object({ providerType: z.literal("m365"), - providerAlias: z.string(), + [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), providerUid: z.string(), awsCredentialsType: z.string().optional(), }), z.object({ providerType: z.literal("gcp"), - providerAlias: z.string(), + [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), providerUid: z.string(), awsCredentialsType: z.string().optional(), }), z.object({ providerType: z.literal("kubernetes"), - providerAlias: z.string(), + [ProviderCredentialFields.PROVIDER_ALIAS]: z.string(), providerUid: z.string(), awsCredentialsType: z.string().optional(), }), @@ -105,46 +107,65 @@ export const addProviderFormSchema = z export const addCredentialsFormSchema = (providerType: string) => z.object({ - secretName: z.string().optional(), - providerId: z.string(), - providerType: z.string(), + [ProviderCredentialFields.PROVIDER_ID]: z.string(), + [ProviderCredentialFields.PROVIDER_TYPE]: z.string(), ...(providerType === "aws" ? { - aws_access_key_id: z + [ProviderCredentialFields.AWS_ACCESS_KEY_ID]: z .string() .nonempty("AWS Access Key ID is required"), - aws_secret_access_key: z + [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]: z .string() .nonempty("AWS Secret Access Key is required"), - aws_session_token: z.string().optional(), + [ProviderCredentialFields.AWS_SESSION_TOKEN]: z.string().optional(), } : providerType === "azure" ? { - client_id: z.string().nonempty("Client ID is required"), - client_secret: z.string().nonempty("Client Secret is required"), - tenant_id: z.string().nonempty("Tenant ID is required"), + [ProviderCredentialFields.CLIENT_ID]: z + .string() + .nonempty("Client ID is required"), + [ProviderCredentialFields.CLIENT_SECRET]: z + .string() + .nonempty("Client Secret is required"), + [ProviderCredentialFields.TENANT_ID]: z + .string() + .nonempty("Tenant ID is required"), } : providerType === "gcp" ? { - client_id: z.string().nonempty("Client ID is required"), - client_secret: z.string().nonempty("Client Secret is required"), - refresh_token: z.string().nonempty("Refresh Token is required"), + [ProviderCredentialFields.CLIENT_ID]: z + .string() + .nonempty("Client ID is required"), + [ProviderCredentialFields.CLIENT_SECRET]: z + .string() + .nonempty("Client Secret is required"), + [ProviderCredentialFields.REFRESH_TOKEN]: z + .string() + .nonempty("Refresh Token is required"), } : providerType === "kubernetes" ? { - kubeconfig_content: z + [ProviderCredentialFields.KUBECONFIG_CONTENT]: z .string() .nonempty("Kubeconfig Content is required"), } : providerType === "m365" ? { - client_id: z.string().nonempty("Client ID is required"), - client_secret: z + [ProviderCredentialFields.CLIENT_ID]: z + .string() + .nonempty("Client ID is required"), + [ProviderCredentialFields.CLIENT_SECRET]: z .string() .nonempty("Client Secret is required"), - tenant_id: z.string().nonempty("Tenant ID is required"), - user: z.string().nonempty("User is required"), - password: z.string().nonempty("Password is required"), + [ProviderCredentialFields.TENANT_ID]: z + .string() + .nonempty("Tenant ID is required"), + [ProviderCredentialFields.USER]: z + .string() + .nonempty("User is required"), + [ProviderCredentialFields.PASSWORD]: z + .string() + .nonempty("Password is required"), } : {}), }); @@ -153,24 +174,30 @@ export const addCredentialsRoleFormSchema = (providerType: string) => providerType === "aws" ? z .object({ - providerId: z.string(), - providerType: z.string(), - role_arn: z.string().nonempty("AWS Role ARN is required"), - external_id: z.string().optional(), - aws_access_key_id: z.string().optional(), - aws_secret_access_key: z.string().optional(), - aws_session_token: z.string().optional(), - session_duration: z.string().optional(), - role_session_name: z.string().optional(), - credentials_type: z.string().optional(), + [ProviderCredentialFields.PROVIDER_ID]: z.string(), + [ProviderCredentialFields.PROVIDER_TYPE]: z.string(), + [ProviderCredentialFields.ROLE_ARN]: z + .string() + .nonempty("AWS Role ARN is required"), + [ProviderCredentialFields.EXTERNAL_ID]: z.string().optional(), + [ProviderCredentialFields.AWS_ACCESS_KEY_ID]: z.string().optional(), + [ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]: z + .string() + .optional(), + [ProviderCredentialFields.AWS_SESSION_TOKEN]: z.string().optional(), + [ProviderCredentialFields.SESSION_DURATION]: z.string().optional(), + [ProviderCredentialFields.ROLE_SESSION_NAME]: z.string().optional(), + [ProviderCredentialFields.CREDENTIALS_TYPE]: z.string().optional(), }) .refine( (data) => - data.credentials_type !== "access-secret-key" || - (data.aws_access_key_id && data.aws_secret_access_key), + data[ProviderCredentialFields.CREDENTIALS_TYPE] !== + "access-secret-key" || + (data[ProviderCredentialFields.AWS_ACCESS_KEY_ID] && + data[ProviderCredentialFields.AWS_SECRET_ACCESS_KEY]), { message: "AWS Access Key ID and Secret Access Key are required.", - path: ["aws_access_key_id"], + path: [ProviderCredentialFields.AWS_ACCESS_KEY_ID], }, ) : z.object({ @@ -183,9 +210,9 @@ export const addCredentialsServiceAccountFormSchema = ( ) => providerType === "gcp" ? z.object({ - providerId: z.string(), - providerType: z.string(), - service_account_key: z.string().refine( + [ProviderCredentialFields.PROVIDER_ID]: z.string(), + [ProviderCredentialFields.PROVIDER_TYPE]: z.string(), + [ProviderCredentialFields.SERVICE_ACCOUNT_KEY]: z.string().refine( (val) => { try { const parsed = JSON.parse(val); @@ -202,22 +229,21 @@ export const addCredentialsServiceAccountFormSchema = ( message: "Invalid JSON format. Please provide a valid JSON object.", }, ), - secretName: z.string().optional(), }) : z.object({ - providerId: z.string(), - providerType: z.string(), + [ProviderCredentialFields.PROVIDER_ID]: z.string(), + [ProviderCredentialFields.PROVIDER_TYPE]: z.string(), }); export const testConnectionFormSchema = z.object({ - providerId: z.string(), + [ProviderCredentialFields.PROVIDER_ID]: z.string(), runOnce: z.boolean().default(false), }); export const launchScanFormSchema = () => z.object({ - providerId: z.string(), - providerType: z.string(), + [ProviderCredentialFields.PROVIDER_ID]: z.string(), + [ProviderCredentialFields.PROVIDER_TYPE]: z.string(), scannerArgs: z .object({ checksToExecute: z.array(z.string()).optional(), @@ -227,7 +253,7 @@ export const launchScanFormSchema = () => export const editProviderFormSchema = (currentAlias: string) => z.object({ - alias: z + [ProviderCredentialFields.PROVIDER_ALIAS]: z .string() .refine((val) => val === "" || val.length >= 3, { message: "The alias must be empty or have at least 3 characters.", @@ -236,7 +262,7 @@ export const editProviderFormSchema = (currentAlias: string) => message: "The new alias must be different from the current one.", }) .optional(), - providerId: z.string(), + [ProviderCredentialFields.PROVIDER_ID]: z.string(), }); export const editInviteFormSchema = z.object({