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 (
- );
-};
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 (
-
-
+
);
};
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 (
-
-
+
);
};
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 (
-
-
+
);
};
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 (
-
-
- );
-};
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 (
-
-
- );
-};
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({