fix: improve error handling in UpdateViaCredentialsForm with early re… (#7988)

This commit is contained in:
Pablo Lara
2025-06-12 11:39:49 +02:00
committed by GitHub
parent 5ec34ad5e7
commit e218435b2f
30 changed files with 1133 additions and 1543 deletions
+1
View File
@@ -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)
---
+59 -235
View File
@@ -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" };
@@ -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")) && (
<ViaCredentialsForm searchParams={searchParams} />
<AddViaCredentialsForm searchParams={searchParams} />
)}
{searchParams.type === "aws" && searchParams.via === "role" && (
<ViaRoleForm searchParams={searchParams} />
<AddViaRoleForm searchParams={searchParams} />
)}
{searchParams.type === "gcp" &&
searchParams.via === "service-account" && (
<ViaServiceAccountForm searchParams={searchParams} />
<AddViaServiceAccountForm searchParams={searchParams} />
)}
</>
);
@@ -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 (
<Form {...form}>
<form action={onSubmitClient}>
<input type="hidden" name="id" value={providerId} />
<input
type="hidden"
name={ProviderCredentialFields.PROVIDER_ID}
value={providerId}
/>
<div className="flex w-full justify-center sm:space-x-6">
<CustomButton
type="button"
+7 -4
View File
@@ -10,6 +10,7 @@ import { SaveIcon } from "@/components/icons";
import { useToast } from "@/components/ui";
import { CustomButton, CustomInput } from "@/components/ui/custom";
import { Form } from "@/components/ui/form";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { editProviderFormSchema } from "@/types";
export const EditForm = ({
@@ -26,8 +27,8 @@ export const EditForm = ({
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
providerId: providerId,
alias: providerAlias,
[ProviderCredentialFields.PROVIDER_ID]: providerId,
[ProviderCredentialFields.PROVIDER_ALIAS]: providerAlias,
},
});
@@ -74,14 +75,16 @@ export const EditForm = ({
<div>
<CustomInput
control={form.control}
name="alias"
name={ProviderCredentialFields.PROVIDER_ALIAS}
type="text"
label="Alias"
labelPlacement="outside"
placeholder={providerAlias}
variant="bordered"
isRequired={false}
isInvalid={!!form.formState.errors.alias}
isInvalid={
!!form.formState.errors[ProviderCredentialFields.PROVIDER_ALIAS]
}
/>
</div>
<input type="hidden" name="providerId" value={providerId} />
@@ -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 (
<BaseCredentialsForm
providerType={providerType}
providerId={providerId}
onSubmit={handleAddCredentials}
successNavigationUrl={successNavigationUrl}
submitButtonText="Next"
/>
);
};
@@ -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 (
<BaseCredentialsForm
providerType={providerType}
providerId={providerId}
onSubmit={handleAddCredentials}
successNavigationUrl={successNavigationUrl}
submitButtonText="Next"
/>
);
};
@@ -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<any>;
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 (
<Form {...form}>
<form
onSubmit={form.handleSubmit(handleSubmit)}
className="flex flex-col space-y-4"
>
<input
type="hidden"
name={ProviderCredentialFields.PROVIDER_ID}
value={providerId}
/>
<input
type="hidden"
name={ProviderCredentialFields.PROVIDER_TYPE}
value={providerType}
/>
<ProviderTitleDocs providerType={providerType} />
<Divider />
{providerType === "aws" && searchParamsObj.get("via") === "role" && (
<AWSRoleCredentialsForm
control={form.control as unknown as Control<AWSCredentialsRole>}
setValue={form.setValue as any}
externalId={externalId}
/>
)}
{providerType === "aws" && searchParamsObj.get("via") !== "role" && (
<AWSStaticCredentialsForm
control={form.control as unknown as Control<AWSCredentials>}
/>
)}
{providerType === "azure" && (
<AzureCredentialsForm
control={form.control as unknown as Control<AzureCredentials>}
/>
)}
{providerType === "m365" && (
<M365CredentialsForm
control={form.control as unknown as Control<M365Credentials>}
/>
)}
{providerType === "gcp" &&
searchParamsObj.get("via") === "service-account" && (
<GCPServiceAccountKeyForm
control={form.control as unknown as Control<GCPServiceAccountKey>}
/>
)}
{providerType === "gcp" &&
searchParamsObj.get("via") !== "service-account" && (
<GCPDefaultCredentialsForm
control={
form.control as unknown as Control<GCPDefaultCredentials>
}
/>
)}
{providerType === "kubernetes" && (
<KubernetesCredentialsForm
control={form.control as unknown as Control<KubernetesCredentials>}
/>
)}
<div className="flex w-full justify-end sm:space-x-6">
{showBackButton &&
(searchParamsObj.get("via") === "credentials" ||
searchParamsObj.get("via") === "role" ||
searchParamsObj.get("via") === "service-account") && (
<CustomButton
type="button"
ariaLabel="Back"
className="w-1/2 bg-transparent"
variant="faded"
size="lg"
radius="lg"
onPress={handleBackStep}
startContent={!isLoading && <ChevronLeftIcon size={24} />}
isDisabled={isLoading}
>
<span>Back</span>
</CustomButton>
)}
<CustomButton
type="submit"
ariaLabel="Save"
className="w-1/2"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
endContent={!isLoading && <ChevronRightIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>{submitButtonText}</span>}
</CustomButton>
</div>
</form>
</Form>
);
};
@@ -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";
@@ -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 = ({
<span className="text-xs font-bold text-default-500">Authentication</span>
<Select
name="credentials_type"
name={ProviderCredentialFields.CREDENTIALS_TYPE}
label="Authentication Method"
placeholder="Select credentials type"
defaultSelectedKeys={["aws-sdk-default"]}
@@ -42,7 +43,7 @@ export const AWSRoleCredentialsForm = ({
variant="bordered"
onSelectionChange={(keys) =>
setValue(
"credentials_type",
ProviderCredentialFields.CREDENTIALS_TYPE,
Array.from(keys)[0] as "aws-sdk-default" | "access-secret-key",
)
}
@@ -55,36 +56,48 @@ export const AWSRoleCredentialsForm = ({
<>
<CustomInput
control={control}
name="aws_access_key_id"
name={ProviderCredentialFields.AWS_ACCESS_KEY_ID}
type="password"
label="AWS Access Key ID"
labelPlacement="inside"
placeholder="Enter the AWS Access Key ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.aws_access_key_id}
isInvalid={
!!control._formState.errors[
ProviderCredentialFields.AWS_ACCESS_KEY_ID
]
}
/>
<CustomInput
control={control}
name="aws_secret_access_key"
name={ProviderCredentialFields.AWS_SECRET_ACCESS_KEY}
type="password"
label="AWS Secret Access Key"
labelPlacement="inside"
placeholder="Enter the AWS Secret Access Key"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.aws_secret_access_key}
isInvalid={
!!control._formState.errors[
ProviderCredentialFields.AWS_SECRET_ACCESS_KEY
]
}
/>
<CustomInput
control={control}
name="aws_session_token"
name={ProviderCredentialFields.AWS_SESSION_TOKEN}
type="password"
label="AWS Session Token (optional)"
labelPlacement="inside"
placeholder="Enter the AWS Session Token"
variant="bordered"
isRequired={false}
isInvalid={!!control._formState.errors.aws_session_token}
isInvalid={
!!control._formState.errors[
ProviderCredentialFields.AWS_SESSION_TOKEN
]
}
/>
</>
)}
@@ -96,18 +109,20 @@ export const AWSRoleCredentialsForm = ({
<CustomInput
control={control}
name="role_arn"
name={ProviderCredentialFields.ROLE_ARN}
type="text"
label="Role ARN"
labelPlacement="inside"
placeholder="Enter the Role ARN"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.role_arn}
isInvalid={
!!control._formState.errors[ProviderCredentialFields.ROLE_ARN]
}
/>
<CustomInput
control={control}
name="external_id"
name={ProviderCredentialFields.EXTERNAL_ID}
type="text"
label="External ID"
labelPlacement="inside"
@@ -116,32 +131,42 @@ export const AWSRoleCredentialsForm = ({
defaultValue={externalId}
isDisabled
isRequired
isInvalid={!!control._formState.errors.external_id}
isInvalid={
!!control._formState.errors[ProviderCredentialFields.EXTERNAL_ID]
}
/>
<span className="text-xs text-default-500">Optional fields</span>
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<CustomInput
control={control}
name="role_session_name"
name={ProviderCredentialFields.ROLE_SESSION_NAME}
type="text"
label="Role Session Name"
labelPlacement="inside"
placeholder="Enter the Role Session Name"
variant="bordered"
isRequired={false}
isInvalid={!!control._formState.errors.role_session_name}
isInvalid={
!!control._formState.errors[
ProviderCredentialFields.ROLE_SESSION_NAME
]
}
/>
<CustomInput
control={control}
name="session_duration"
name={ProviderCredentialFields.SESSION_DURATION}
type="number"
label="Session Duration (seconds)"
labelPlacement="inside"
placeholder="Enter the session duration (default: 3600)"
variant="bordered"
isRequired={false}
isInvalid={!!control._formState.errors.session_duration}
isInvalid={
!!control._formState.errors[
ProviderCredentialFields.SESSION_DURATION
]
}
/>
</div>
</>
@@ -1,6 +1,7 @@
import { Control } from "react-hook-form";
import { CustomInput } from "@/components/ui/custom";
import { ProviderCredentialFields } from "@/lib/provider-credentials/provider-credential-fields";
import { AWSCredentials } from "@/types";
export const AWSStaticCredentialsForm = ({
@@ -20,36 +21,48 @@ export const AWSStaticCredentialsForm = ({
</div>
<CustomInput
control={control}
name="aws_access_key_id"
name={ProviderCredentialFields.AWS_ACCESS_KEY_ID}
type="password"
label="AWS Access Key ID"
labelPlacement="inside"
placeholder="Enter the AWS Access Key ID"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.aws_access_key_id}
isInvalid={
!!control._formState.errors[
ProviderCredentialFields.AWS_ACCESS_KEY_ID
]
}
/>
<CustomInput
control={control}
name="aws_secret_access_key"
name={ProviderCredentialFields.AWS_SECRET_ACCESS_KEY}
type="password"
label="AWS Secret Access Key"
labelPlacement="inside"
placeholder="Enter the AWS Secret Access Key"
variant="bordered"
isRequired
isInvalid={!!control._formState.errors.aws_secret_access_key}
isInvalid={
!!control._formState.errors[
ProviderCredentialFields.AWS_SECRET_ACCESS_KEY
]
}
/>
<CustomInput
control={control}
name="aws_session_token"
name={ProviderCredentialFields.AWS_SESSION_TOKEN}
type="password"
label="AWS Session Token"
labelPlacement="inside"
placeholder="Enter the AWS Session Token"
variant="bordered"
isRequired={false}
isInvalid={!!control._formState.errors.aws_session_token}
isInvalid={
!!control._formState.errors[
ProviderCredentialFields.AWS_SESSION_TOKEN
]
}
/>
</>
);
@@ -0,0 +1,31 @@
"use client";
import { addCredentialsProvider } from "@/actions/providers/providers";
import { ProviderType } from "@/types";
import { BaseCredentialsForm } from "../../base-credentials-form";
export const AddViaServiceAccountForm = ({
searchParams,
}: {
searchParams: { type: ProviderType; id: string };
}) => {
const providerType = searchParams.type;
const providerId = searchParams.id;
const handleAddCredentials = async (formData: FormData) => {
return await addCredentialsProvider(formData);
};
const successNavigationUrl = `/providers/test-connection?type=${providerType}&id=${providerId}`;
return (
<BaseCredentialsForm
providerType={providerType}
providerId={providerId}
onSubmit={handleAddCredentials}
successNavigationUrl={successNavigationUrl}
submitButtonText="Next"
/>
);
};
@@ -1,3 +1,3 @@
export * from "./add-via-service-account-form";
export * from "./radio-group-gcp-via-credentials-type-form";
export * from "./select-via-gcp";
export * from "./via-service-account-form";
@@ -1,160 +0,0 @@
"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 { addCredentialsProvider } 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 { GCPServiceAccountKeyForm } from "./credentials-type/gcp-service-account-key-form";
export const ViaServiceAccountForm = ({
searchParams,
}: {
searchParams: { type: ProviderType; 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;
const providerId = searchParams.id;
const formSchema = addCredentialsServiceAccountFormSchema(providerType);
type FormSchemaType = z.infer<typeof formSchema>;
const form = useForm<FormSchemaType>({
resolver: zodResolver(formSchema),
defaultValues: {
providerId,
providerType,
...(providerType === "gcp"
? {
service_account_key: "",
secretName: "",
}
: {}),
},
});
const isLoading = form.formState.isSubmitting;
const onSubmitClient = async (values: FormSchemaType) => {
const formData = new FormData();
Object.entries(values).forEach(([key, value]) => {
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/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}`,
);
}
} 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 (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitClient)}
className="flex flex-col space-y-4"
>
<input type="hidden" name="providerId" value={providerId} />
<input type="hidden" name="providerType" value={providerType} />
<ProviderTitleDocs providerType={providerType} />
<Divider />
{providerType === "gcp" && (
<GCPServiceAccountKeyForm
control={form.control as unknown as Control<GCPServiceAccountKey>}
/>
)}
<div className="flex w-full justify-end sm:space-x-6">
{searchParamsObj.get("via") === "service-account" && (
<CustomButton
type="button"
ariaLabel="Back"
className="w-1/2 bg-transparent"
variant="faded"
size="lg"
radius="lg"
onPress={handleBackStep}
startContent={!isLoading && <ChevronLeftIcon size={24} />}
isDisabled={isLoading}
>
<span>Back</span>
</CustomButton>
)}
<CustomButton
type="submit"
ariaLabel={"Save"}
className="w-1/2"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
endContent={!isLoading && <ChevronRightIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Next</span>}
</CustomButton>
</div>
</form>
</Form>
);
};
@@ -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<typeof addCredentialsFormSchema>
>;
// 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<FormType>({
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 (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitClient)}
className="flex flex-col space-y-4"
>
<input type="hidden" name="providerId" value={providerId} />
<input type="hidden" name="providerType" value={providerType} />
<ProviderTitleDocs providerType={providerType} />
<Divider />
{providerType === "aws" && (
<AWSStaticCredentialsForm
control={form.control as unknown as Control<AWSCredentials>}
/>
)}
{providerType === "azure" && (
<AzureCredentialsForm
control={form.control as unknown as Control<AzureCredentials>}
/>
)}
{providerType === "m365" && (
<M365CredentialsForm
control={form.control as unknown as Control<M365Credentials>}
/>
)}
{providerType === "gcp" && (
<GCPDefaultCredentialsForm
control={form.control as unknown as Control<GCPDefaultCredentials>}
/>
)}
{providerType === "kubernetes" && (
<KubernetesCredentialsForm
control={form.control as unknown as Control<KubernetesCredentials>}
/>
)}
<div className="flex w-full justify-end sm:space-x-6">
{searchParamsObj.get("via") === "credentials" && (
<CustomButton
type="button"
ariaLabel="Back"
className="w-1/2 bg-transparent"
variant="faded"
size="lg"
radius="lg"
onPress={handleBackStep}
startContent={!isLoading && <ChevronLeftIcon size={24} />}
isDisabled={isLoading}
>
<span>Back</span>
</CustomButton>
)}
<CustomButton
type="submit"
ariaLabel={"Save"}
className="w-1/2"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
endContent={!isLoading && <ChevronRightIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Next</span>}
</CustomButton>
</div>
</form>
</Form>
<BaseCredentialsForm
providerType={providerType}
providerId={providerId}
onSubmit={handleUpdateCredentials}
successNavigationUrl={successNavigationUrl}
submitButtonText="Next"
/>
);
};
@@ -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<typeof formSchema> & {
credentials_type: "aws-sdk-default" | "access-secret-key";
const handleUpdateCredentials = async (formData: FormData) => {
return await updateCredentialsProvider(providerSecretId, formData);
};
const form = useForm<FormSchemaType>({
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 (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitClient)}
className="flex flex-col space-y-4"
>
<input type="hidden" name="providerId" value={providerId} />
<input type="hidden" name="providerType" value={providerType} />
{/* Conditional AWS Form */}
{providerType === "aws" && (
<AWSRoleCredentialsForm
control={form.control as unknown as Control<AWSCredentialsRole>}
setValue={
form.setValue as unknown as UseFormSetValue<AWSCredentialsRole>
}
externalId={externalId || ""}
/>
)}
{/* Action Buttons */}
<div className="flex w-full justify-end sm:space-x-6">
{searchParamsObj.get("via") === "role" && (
<CustomButton
type="button"
ariaLabel="Back"
className="w-1/2 bg-transparent"
variant="faded"
size="lg"
radius="lg"
onPress={handleBackStep}
startContent={!isLoading && <ChevronLeftIcon size={24} />}
isDisabled={isLoading}
>
<span>Back</span>
</CustomButton>
)}
<CustomButton
type="submit"
ariaLabel="Save"
className="w-1/2"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
endContent={!isLoading && <ChevronRightIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Next</span>}
</CustomButton>
</div>
</form>
</Form>
<BaseCredentialsForm
providerType={providerType}
providerId={providerId}
onSubmit={handleUpdateCredentials}
successNavigationUrl={successNavigationUrl}
submitButtonText="Next"
/>
);
};
@@ -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<typeof formSchema>;
const form = useForm<FormSchemaType>({
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 (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitClient)}
className="flex flex-col space-y-4"
>
<input type="hidden" name="providerId" value={providerId} />
<input type="hidden" name="providerType" value={providerType} />
<ProviderTitleDocs providerType={providerType} />
<Divider />
{providerType === "gcp" && (
<GCPServiceAccountKeyForm
control={form.control as unknown as Control<GCPServiceAccountKey>}
/>
)}
<div className="flex w-full justify-end sm:space-x-6">
{searchParamsObj.get("via") === "service-account" && (
<CustomButton
type="button"
ariaLabel="Back"
className="w-1/2 bg-transparent"
variant="faded"
size="lg"
radius="lg"
onPress={handleBackStep}
startContent={!isLoading && <ChevronLeftIcon size={24} />}
isDisabled={isLoading}
>
<span>Back</span>
</CustomButton>
)}
<CustomButton
type="submit"
ariaLabel={"Save"}
className="w-1/2"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
endContent={!isLoading && <ChevronRightIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Next</span>}
</CustomButton>
</div>
</form>
</Form>
<BaseCredentialsForm
providerType={providerType}
providerId={providerId}
onSubmit={handleUpdateCredentials}
successNavigationUrl={successNavigationUrl}
submitButtonText="Next"
/>
);
};
@@ -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<typeof addCredentialsFormSchema>
>;
// 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<FormType>({
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 (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitClient)}
className="flex flex-col space-y-4"
>
<input type="hidden" name="providerId" value={providerId} />
<input type="hidden" name="providerType" value={providerType} />
<ProviderTitleDocs providerType={providerType} />
<Divider />
{providerType === "aws" && (
<AWSStaticCredentialsForm
control={form.control as unknown as Control<AWSCredentials>}
/>
)}
{providerType === "azure" && (
<AzureCredentialsForm
control={form.control as unknown as Control<AzureCredentials>}
/>
)}
{providerType === "m365" && (
<M365CredentialsForm
control={form.control as unknown as Control<M365Credentials>}
/>
)}
{providerType === "gcp" && (
<GCPDefaultCredentialsForm
control={form.control as unknown as Control<GCPDefaultCredentials>}
/>
)}
{providerType === "kubernetes" && (
<KubernetesCredentialsForm
control={form.control as unknown as Control<KubernetesCredentials>}
/>
)}
<div className="flex w-full justify-end sm:space-x-6">
{searchParamsObj.get("via") === "credentials" && (
<CustomButton
type="button"
ariaLabel="Back"
className="w-1/2 bg-transparent"
variant="faded"
size="lg"
radius="lg"
onPress={handleBackStep}
startContent={!isLoading && <ChevronLeftIcon size={24} />}
isDisabled={isLoading}
>
<span>Back</span>
</CustomButton>
)}
<CustomButton
type="submit"
ariaLabel={"Save"}
className="w-1/2"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
endContent={!isLoading && <ChevronRightIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Next</span>}
</CustomButton>
</div>
</form>
</Form>
);
};
@@ -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<typeof formSchema> & {
credentials_type: "aws-sdk-default" | "access-secret-key";
};
const form = useForm<FormSchemaType>({
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 (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmitClient)}
className="flex flex-col space-y-4"
>
<input type="hidden" name="providerId" value={providerId} />
<input type="hidden" name="providerType" value={providerType} />
{providerType === "aws" && (
<AWSRoleCredentialsForm
control={form.control as unknown as Control<AWSCredentialsRole>}
setValue={
form.setValue as unknown as UseFormSetValue<AWSCredentialsRole>
}
externalId={externalId || ""}
/>
)}
<div className="flex w-full justify-end sm:space-x-6">
{searchParamsObj.get("via") === "role" && (
<CustomButton
type="button"
ariaLabel="Back"
className="w-1/2 bg-transparent"
variant="faded"
size="lg"
radius="lg"
onPress={handleBackStep}
startContent={!isLoading && <ChevronLeftIcon size={24} />}
isDisabled={isLoading}
>
<span>Back</span>
</CustomButton>
)}
<CustomButton
type="submit"
ariaLabel={"Save"}
className="w-1/2"
variant="solid"
color="action"
size="lg"
isLoading={isLoading}
endContent={!isLoading && <ChevronRightIcon size={24} />}
>
{isLoading ? <>Loading</> : <span>Next</span>}
</CustomButton>
</div>
</form>
</Form>
);
};
+3 -1
View File
@@ -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";
+168
View File
@@ -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<any>;
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<CredentialsFormData>({
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 || "",
};
};
+61
View File
@@ -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 = <T extends Record<string, any>>(
form: UseFormReturn<T>,
customErrorMapping?: Record<string, string>,
) => {
const { toast } = useToast();
const handleServerErrors = (
errors: ApiError[],
errorMapping?: Record<string, string>,
) => {
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<string, string>,
) => {
// 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 };
};
+31
View File
@@ -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<string, string> = {
[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,
};
+38
View File
@@ -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<string, any>,
): Record<string, any> {
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();
+1
View File
@@ -1,3 +1,4 @@
export * from "./error-mappings";
export * from "./external-urls";
export * from "./helper";
export * from "./helper-filters";
@@ -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),
};
};
@@ -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];
+29 -31
View File
@@ -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<SVGSVGElement> & {
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 =
+74 -48
View File
@@ -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({