From 461be92fb087fe3b4c50a4552d99effe68464a61 Mon Sep 17 00:00:00 2001 From: alejandrobailo Date: Thu, 16 Oct 2025 17:25:54 +0200 Subject: [PATCH] feat(m365): enhance M365 credentials handling with new forms and selection options --- .../providers/credentials-update-info.tsx | 4 ++ .../workflow/forms/base-credentials-form.tsx | 38 ++++++++-- .../m365/credentials-type/index.ts | 2 + .../m365-certificate-credentials-form.tsx | 72 +++++++++++++++++++ .../m365-client-secret-credentials-form.tsx | 58 +++++++++++++++ .../select-credentials-type/m365/index.ts | 5 ++ ...o-group-m365-via-credentials-type-form.tsx | 72 +++++++++++++++++++ .../m365/select-via-m365.tsx | 38 ++++++++++ .../workflow/forms/via-credentials/index.ts | 1 - .../build-crendentials.ts | 17 +++-- .../provider-credential-fields.ts | 2 + ui/lib/provider-helpers.ts | 12 +++- 12 files changed, 307 insertions(+), 14 deletions(-) create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/index.ts create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-certificate-credentials-form.tsx create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-client-secret-credentials-form.tsx create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/m365/index.ts create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx create mode 100644 ui/components/providers/workflow/forms/select-credentials-type/m365/select-via-m365.tsx diff --git a/ui/components/providers/credentials-update-info.tsx b/ui/components/providers/credentials-update-info.tsx index 0fe75f0cb2..3fb13f97a7 100644 --- a/ui/components/providers/credentials-update-info.tsx +++ b/ui/components/providers/credentials-update-info.tsx @@ -3,6 +3,7 @@ import { SelectViaAWS } from "@/components/providers/workflow/forms/select-credentials-type/aws"; import { SelectViaGCP } from "@/components/providers/workflow/forms/select-credentials-type/gcp"; import { SelectViaGitHub } from "@/components/providers/workflow/forms/select-credentials-type/github"; +import { SelectViaM365 } from "@/components/providers/workflow/forms/select-credentials-type/m365"; import { ProviderType } from "@/types/providers"; interface UpdateCredentialsInfoProps { @@ -24,6 +25,9 @@ export const CredentialsUpdateInfo = ({ if (providerType === "github") { return ; } + if (providerType === "m365") { + return ; + } return null; }; diff --git a/ui/components/providers/workflow/forms/base-credentials-form.tsx b/ui/components/providers/workflow/forms/base-credentials-form.tsx index 9c66103e1d..a50cfdeb74 100644 --- a/ui/components/providers/workflow/forms/base-credentials-form.tsx +++ b/ui/components/providers/workflow/forms/base-credentials-form.tsx @@ -17,7 +17,8 @@ import { GCPDefaultCredentials, GCPServiceAccountKey, KubernetesCredentials, - M365Credentials, + M365CertificateCredentials, + M365ClientSecretCredentials, ProviderType, } from "@/types"; @@ -26,10 +27,13 @@ import { AWSStaticCredentialsForm } from "./select-credentials-type/aws/credenti 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 { + M365CertificateCredentialsForm, + M365ClientSecretCredentialsForm, +} from "./select-credentials-type/m365"; import { AzureCredentialsForm } from "./via-credentials/azure-credentials-form"; import { GitHubCredentialsForm } from "./via-credentials/github-credentials-form"; import { KubernetesCredentialsForm } from "./via-credentials/k8s-credentials-form"; -import { M365CredentialsForm } from "./via-credentials/m365-credentials-form"; type BaseCredentialsFormProps = { providerType: ProviderType; @@ -103,11 +107,22 @@ export const BaseCredentialsForm = ({ control={form.control as unknown as Control} /> )} - {providerType === "m365" && ( - } - /> - )} + {providerType === "m365" && + searchParamsObj.get("via") === "app_client_secret" && ( + + } + /> + )} + {providerType === "m365" && + searchParamsObj.get("via") === "app_certificate" && ( + + } + /> + )} {providerType === "gcp" && searchParamsObj.get("via") === "service-account" && ( } + onPress={(e) => { + const formElement = e.target as HTMLElement; + const form = formElement.closest("form"); + if (form) { + form.dispatchEvent( + new Event("submit", { bubbles: true, cancelable: true }), + ); + } + }} > {isLoading ? <>Loading : {submitButtonText}} diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/index.ts b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/index.ts new file mode 100644 index 0000000000..91d6766bac --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/index.ts @@ -0,0 +1,2 @@ +export { M365CertificateCredentialsForm } from "./m365-certificate-credentials-form"; +export { M365ClientSecretCredentialsForm } from "./m365-client-secret-credentials-form"; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-certificate-credentials-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-certificate-credentials-form.tsx new file mode 100644 index 0000000000..1cc846fc4f --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-certificate-credentials-form.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { Control } from "react-hook-form"; + +import { CustomInput, CustomTextarea } from "@/components/ui/custom"; +import { CustomLink } from "@/components/ui/custom/custom-link"; +import { M365CertificateCredentials } from "@/types"; + +export const M365CertificateCredentialsForm = ({ + control, +}: { + control: Control; +}) => { + return ( + <> +
+
+ App Certificate Credentials +
+
+ Please provide your Microsoft 365 application credentials with + certificate authentication. +
+
+ + + +

+ The certificate content must be base64 encoded from an unsigned + certificate. For detailed instructions on how to generate and encode + your certificate, please refer to the{" "} + + Microsoft 365 getting started guide + + . +

+ + ); +}; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-client-secret-credentials-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-client-secret-credentials-form.tsx new file mode 100644 index 0000000000..3312fb536a --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/credentials-type/m365-client-secret-credentials-form.tsx @@ -0,0 +1,58 @@ +"use client"; + +import { Control } from "react-hook-form"; + +import { CustomInput } from "@/components/ui/custom"; +import { M365ClientSecretCredentials } from "@/types"; + +export const M365ClientSecretCredentialsForm = ({ + control, +}: { + control: Control; +}) => { + return ( + <> +
+
+ App Client Secret Credentials +
+
+ Please provide your Microsoft 365 application credentials. +
+
+ + + + + ); +}; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/index.ts b/ui/components/providers/workflow/forms/select-credentials-type/m365/index.ts new file mode 100644 index 0000000000..84610f510a --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/index.ts @@ -0,0 +1,5 @@ +export { + M365CertificateCredentialsForm, + M365ClientSecretCredentialsForm, +} from "./credentials-type"; +export { SelectViaM365 } from "./select-via-m365"; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx new file mode 100644 index 0000000000..2f5599363b --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/radio-group-m365-via-credentials-type-form.tsx @@ -0,0 +1,72 @@ +"use client"; + +import { RadioGroup } from "@heroui/radio"; +import React from "react"; +import { Control, Controller } from "react-hook-form"; + +import { CustomRadio } from "@/components/ui/custom"; +import { FormMessage } from "@/components/ui/form"; + +type RadioGroupM365ViaCredentialsFormProps = { + control: Control; + isInvalid: boolean; + errorMessage?: string; + onChange?: (value: string) => void; +}; + +export const RadioGroupM365ViaCredentialsTypeForm = ({ + control, + isInvalid, + errorMessage, + onChange, +}: RadioGroupM365ViaCredentialsFormProps) => { + return ( + ( + <> + { + field.onChange(value); + if (onChange) { + onChange(value); + } + }} + > +
+ + Select Authentication Method + + +
+ App Client Secret Credentials +
+
+ +
+ App Certificate Credentials +
+
+
+
+ {errorMessage && ( + + {errorMessage} + + )} + + )} + /> + ); +}; diff --git a/ui/components/providers/workflow/forms/select-credentials-type/m365/select-via-m365.tsx b/ui/components/providers/workflow/forms/select-credentials-type/m365/select-via-m365.tsx new file mode 100644 index 0000000000..020e4ed0e1 --- /dev/null +++ b/ui/components/providers/workflow/forms/select-credentials-type/m365/select-via-m365.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; + +import { Form } from "@/components/ui/form"; + +import { RadioGroupM365ViaCredentialsTypeForm } from "./radio-group-m365-via-credentials-type-form"; + +interface SelectViaM365Props { + initialVia?: string; +} + +export const SelectViaM365 = ({ initialVia }: SelectViaM365Props) => { + const router = useRouter(); + const form = useForm({ + defaultValues: { + m365CredentialsType: initialVia || "", + }, + }); + + const handleSelectionChange = (value: string) => { + const url = new URL(window.location.href); + url.searchParams.set("via", value); + router.push(url.toString()); + }; + + return ( +
+ + + ); +}; diff --git a/ui/components/providers/workflow/forms/via-credentials/index.ts b/ui/components/providers/workflow/forms/via-credentials/index.ts index d020b9715f..982e5ca701 100644 --- a/ui/components/providers/workflow/forms/via-credentials/index.ts +++ b/ui/components/providers/workflow/forms/via-credentials/index.ts @@ -1,4 +1,3 @@ export * from "./azure-credentials-form"; export * from "./github-credentials-form"; export * from "./k8s-credentials-form"; -export * from "./m365-credentials-form"; diff --git a/ui/lib/provider-credentials/build-crendentials.ts b/ui/lib/provider-credentials/build-crendentials.ts index 475702db0c..2a0abd31c7 100644 --- a/ui/lib/provider-credentials/build-crendentials.ts +++ b/ui/lib/provider-credentials/build-crendentials.ts @@ -80,14 +80,21 @@ export const buildAzureSecret = (formData: FormData) => { export const buildM365Secret = (formData: FormData) => { const secret = { - ...buildAzureSecret(formData), - [ProviderCredentialFields.USER]: getFormValue( + [ProviderCredentialFields.CLIENT_ID]: getFormValue( formData, - ProviderCredentialFields.USER, + ProviderCredentialFields.CLIENT_ID, ), - [ProviderCredentialFields.PASSWORD]: getFormValue( + [ProviderCredentialFields.TENANT_ID]: getFormValue( formData, - ProviderCredentialFields.PASSWORD, + ProviderCredentialFields.TENANT_ID, + ), + [ProviderCredentialFields.CLIENT_SECRET]: getFormValue( + formData, + ProviderCredentialFields.CLIENT_SECRET, + ), + [ProviderCredentialFields.CERTIFICATE_CONTENT]: getFormValue( + formData, + ProviderCredentialFields.CERTIFICATE_CONTENT, ), }; return filterEmptyValues(secret); diff --git a/ui/lib/provider-credentials/provider-credential-fields.ts b/ui/lib/provider-credentials/provider-credential-fields.ts index 32dc184292..635c74bf44 100644 --- a/ui/lib/provider-credentials/provider-credential-fields.ts +++ b/ui/lib/provider-credentials/provider-credential-fields.ts @@ -29,6 +29,7 @@ export const ProviderCredentialFields = { TENANT_ID: "tenant_id", USER: "user", PASSWORD: "password", + CERTIFICATE_CONTENT: "certificate_content", // GCP fields REFRESH_TOKEN: "refresh_token", @@ -70,6 +71,7 @@ export const ErrorPointers = { OAUTH_APP_TOKEN: "/data/attributes/secret/oauth_app_token", GITHUB_APP_ID: "/data/attributes/secret/github_app_id", GITHUB_APP_KEY: "/data/attributes/secret/github_app_key_content", + CERTIFICATE_CONTENT: "/data/attributes/secret/certificate_content", } as const; export type ErrorPointer = (typeof ErrorPointers)[keyof typeof ErrorPointers]; diff --git a/ui/lib/provider-helpers.ts b/ui/lib/provider-helpers.ts index becd9bc4f2..1bcf5d80bb 100644 --- a/ui/lib/provider-helpers.ts +++ b/ui/lib/provider-helpers.ts @@ -53,7 +53,7 @@ export const getProviderFormType = ( via?: string, ): ProviderFormType => { // Providers that need credential type selection - const needsSelector = ["aws", "gcp", "github"].includes(providerType); + const needsSelector = ["aws", "gcp", "github", "m365"].includes(providerType); // Show selector if no via parameter and provider needs it if (needsSelector && !via) { @@ -80,6 +80,14 @@ export const getProviderFormType = ( return "credentials"; } + // M365 credential types + if ( + providerType === "m365" && + ["app_client_secret", "app_certificate"].includes(via || "") + ) { + return "credentials"; + } + // Other providers go directly to credentials form if (!needsSelector) { return "credentials"; @@ -99,6 +107,8 @@ export const requiresBackButton = (via?: string | null): boolean => { "personal_access_token", "oauth_app", "github_app", + "app_client_secret", + "app_certificate", ]; return validViaTypes.includes(via);