import { zodResolver } from '@hookform/resolvers/zod' import { useEffect } from 'react' import { SubmitHandler, useForm } from 'react-hook-form' import z from 'zod' import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold' import AlertError from 'components/ui/AlertError' import { InlineLink } from 'components/ui/InlineLink' import { UpgradeToPro } from 'components/ui/UpgradeToPro' import { useSSOConfigCreateMutation } from 'data/sso/sso-config-create-mutation' import { useOrgSSOConfigQuery } from 'data/sso/sso-config-query' import { useSSOConfigUpdateMutation } from 'data/sso/sso-config-update-mutation' import { useCheckEntitlements } from 'hooks/misc/useCheckEntitlements' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { DOCS_URL } from 'lib/constants' import { Button, Card, CardContent, CardFooter, FormControl_Shadcn_, FormField_Shadcn_, Form_Shadcn_, Switch, } from 'ui' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { AttributeMapping } from './AttributeMapping' import { JoinOrganizationOnSignup } from './JoinOrganizationOnSignup' import { SSODomains } from './SSODomains' import { SSOMetadata } from './SSOMetadata' const FormSchema = z .object({ enabled: z.boolean(), domains: z .array( z.object({ value: z.string().trim().min(1, 'Please provide a domain'), }) ) .min(1, 'At least one domain is required'), metadataXmlUrl: z.string().trim().optional(), metadataXmlFile: z.string().trim().optional(), emailMapping: z.array(z.object({ value: z.string().trim().min(1, 'This field is required') })), userNameMapping: z.array(z.object({ value: z.string().trim() })), firstNameMapping: z.array(z.object({ value: z.string().trim() })), lastNameMapping: z.array(z.object({ value: z.string().trim() })), joinOrgOnSignup: z.boolean(), roleOnJoin: z.string().optional(), }) // set the error on both fields .refine((data) => data.metadataXmlUrl || data.metadataXmlFile, { message: 'Please provide either a metadata XML URL or upload a metadata XML file', path: ['metadataXmlUrl'], }) .refine((data) => data.metadataXmlUrl || data.metadataXmlFile, { message: 'Please provide either a metadata XML URL or upload a metadata XML file', path: ['metadataXmlFile'], }) export type SSOConfigFormSchema = z.infer export const SSOConfig = () => { const FORM_ID = 'sso-config-form' const { data: organization } = useSelectedOrganizationQuery() const { hasAccess: hasAccessToSso, isLoading: isLoadingEntitlement } = useCheckEntitlements('auth.platform.sso') const { data: ssoConfig, isPending: isLoadingSSOConfig, isSuccess, isError, error: configError, } = useOrgSSOConfigQuery({ orgSlug: organization?.slug }, { enabled: !!organization }) const isSSOProviderNotFound = ssoConfig === null const form = useForm({ resolver: zodResolver(FormSchema), defaultValues: { enabled: false, domains: [{ value: '' }], metadataXmlUrl: '', metadataXmlFile: '', emailMapping: [{ value: '' }], userNameMapping: [{ value: '' }], firstNameMapping: [{ value: '' }], lastNameMapping: [{ value: '' }], joinOrgOnSignup: false, roleOnJoin: 'Developer', }, }) const isSSOEnabled = form.watch('enabled') const { mutate: createSSOConfig, isPending: isCreating } = useSSOConfigCreateMutation({ onSuccess: () => form.reset(), }) const { mutate: updateSSOConfig, isPending: isUpdating } = useSSOConfigUpdateMutation({ onSuccess: () => form.reset(), }) const onSubmit: SubmitHandler = (values) => { const roleOnJoin = (values.roleOnJoin || 'Developer') as | 'Administrator' | 'Developer' | 'Owner' | 'Read-only' | undefined const payload = { slug: organization!.slug, config: { enabled: values.enabled, domains: values.domains.map((d) => d.value), metadata_xml_file: values.metadataXmlFile!, metadata_xml_url: values.metadataXmlUrl!, email_mapping: values.emailMapping.map((item) => item.value).filter(Boolean), first_name_mapping: values.firstNameMapping.map((item) => item.value).filter(Boolean), last_name_mapping: values.lastNameMapping.map((item) => item.value).filter(Boolean), user_name_mapping: values.userNameMapping.map((item) => item.value).filter(Boolean), join_org_on_signup_enabled: values.joinOrgOnSignup, join_org_on_signup_role: roleOnJoin, }, } if (!!ssoConfig) { updateSSOConfig(payload) } else { createSSOConfig(payload) } } useEffect(() => { if (ssoConfig) { form.reset({ enabled: ssoConfig.enabled, domains: ssoConfig.domains.map((domain) => ({ value: domain })), metadataXmlUrl: ssoConfig.metadata_xml_url, metadataXmlFile: ssoConfig.metadata_xml_file, emailMapping: ssoConfig.email_mapping.map((email) => ({ value: email })), userNameMapping: ssoConfig.user_name_mapping?.map((userName) => ({ value: userName })), firstNameMapping: ssoConfig.first_name_mapping?.map((firstName) => ({ value: firstName })), lastNameMapping: ssoConfig.last_name_mapping?.map((lastName) => ({ value: lastName })), joinOrgOnSignup: ssoConfig.join_org_on_signup_enabled, roleOnJoin: ssoConfig.join_org_on_signup_role, }) } }, [ssoConfig, form]) return ( {isLoadingEntitlement || (hasAccessToSso && isLoadingSSOConfig) ? ( ) : isError && !isSSOProviderNotFound ? ( ) : !hasAccessToSso ? ( ) : isSuccess || isSSOProviderNotFound ? (
( Enable and configure SSO for your organization. Learn more about SSO{' '} here . } > )} /> {(isSSOEnabled || ssoConfig) && ( <> )} {form.formState.isDirty && ( )}
) : null}
) }