import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { Check } from 'lucide-react' import { useTheme } from 'next-themes' import { useQueryState } from 'nuqs' import { useEffect, useId, useMemo, useState } from 'react' import { useForm } from 'react-hook-form' import ReactMarkdown from 'react-markdown' import { toast } from 'sonner' import { Button, Form, Sheet, SheetContent, SheetFooter, SheetHeader, SheetSection, SheetTitle, } from 'ui' import { Admonition } from 'ui-patterns' import { Input } from 'ui-patterns/DataInputs/Input' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { NO_REQUIRED_CHARACTERS } from '../Auth.constants' import { AuthAlert } from './AuthAlert' import type { Provider } from './AuthProvidersForm.types' import FormField from './FormField' import { Markdown } from '@/components/interfaces/Markdown' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { DocsButton } from '@/components/ui/DocsButton' import { ResourceItem } from '@/components/ui/Resource/ResourceItem' import type { components } from '@/data/api' import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation' import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useHasEntitlementAccess } from '@/hooks/misc/useCheckEntitlements' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent' import { BASE_PATH } from '@/lib/constants' interface ProviderFormProps { config: components['schemas']['GoTrueConfigResponse'] provider: Provider isActive: boolean } const doubleNegativeKeys = ['SMS_AUTOCONFIRM'] export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps) => { const { resolvedTheme } = useTheme() const { ref: projectRef } = useParams() const { data: organization } = useSelectedOrganizationQuery() const [urlProvider, setUrlProvider] = useQueryState('provider', { defaultValue: '' }) const [open, setOpen] = useState(false) const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation() const { data: endpoint } = useProjectApiUrl({ projectRef }) const { can: canUpdateConfig } = useAsyncCheckPermissions( PermissionAction.UPDATE, 'custom_config_gotrue' ) const shouldDisableField = (field: string): boolean => { const shouldDisableSmsFields = config.HOOK_SEND_SMS_ENABLED && field.startsWith('SMS_') && ![ 'SMS_AUTOCONFIRM', 'SMS_OTP_EXP', 'SMS_OTP_LENGTH', 'SMS_OTP_LENGTH', 'SMS_TEMPLATE', 'SMS_TEST_OTP', 'SMS_TEST_OTP_VALID_UNTIL', ].includes(field) return ( ['EXTERNAL_SLACK_CLIENT_ID', 'EXTERNAL_SLACK_SECRET'].includes(field) || shouldDisableSmsFields ) } const hasEntitlementAccess = useHasEntitlementAccess() const getValuesForProvider = useStaticEffectEvent( (config: components['schemas']['GoTrueConfigResponse']) => { const values: { [x: string]: string | boolean } = {} Object.keys(provider.properties).forEach((key) => { // This ensures the default value is visibly selected if (key === 'PASSWORD_REQUIRED_CHARACTERS' && config.PASSWORD_REQUIRED_CHARACTERS === '') { values[key] = NO_REQUIRED_CHARACTERS return } const isDoubleNegative = doubleNegativeKeys.includes(key) if (provider.title === 'SAML 2.0') { const configValue = (config as any)[key] values[key] = configValue || (provider.properties[key].type === 'boolean' ? false : '') } else { if (isDoubleNegative) { values[key] = !(config as any)[key] } else { const configValue = (config as any)[key] values[key] = configValue ? configValue : provider.properties[key].type === 'boolean' ? false : '' } } }) return values } ) const INITIAL_VALUES = useMemo(() => { // This check will always be true but let us avoid adding an eslint disable comment on unused memo dependencies // which could hide real issues in the future. // Adding the provider in the memo dependencies ensures the INITIAL_VALUES is properly applied if (!provider) return return getValuesForProvider(config) }, [config, getValuesForProvider, provider]) const onSubmit = (values: any) => { const payload = { ...values } Object.keys(values).map((x: string) => { if (doubleNegativeKeys.includes(x)) payload[x] = !values[x] if (payload[x] === '') payload[x] = null }) // The backend uses empty string to represent no required characters in the password if (payload.PASSWORD_REQUIRED_CHARACTERS === NO_REQUIRED_CHARACTERS) { payload.PASSWORD_REQUIRED_CHARACTERS = '' } updateAuthConfig( { projectRef: projectRef!, config: payload }, { onSuccess: (newValues) => { setOpen(false) setUrlProvider(null) form.reset(getValuesForProvider(newValues)) toast.success('Successfully updated settings') }, } ) } // Handle clicking on a provider in the list const handleProviderClick = () => setUrlProvider(provider.title) const handleOpenChange = (isOpen: boolean) => { // Remove provider query param from URL when closed if (!isOpen) setUrlProvider(null) } // Open or close the form based on the query parameter useEffect(() => { const isProviderInQuery = urlProvider.toLowerCase() === provider.title.toLowerCase() setOpen(isProviderInQuery) }, [urlProvider, provider.title]) const form = useForm({ defaultValues: INITIAL_VALUES, resolver: zodResolver(provider.validationSchema as any), shouldUnregister: false, }) useEffect(() => { if (open) { form.reset(INITIAL_VALUES) } }, [open, form, INITIAL_VALUES]) const formId = useId() return ( <> } meta={ isActive ? (
Enabled
) : (
Disabled
) } > {provider.title}
{`${provider.title} {provider.title}
{Object.keys(provider.properties).map((x: string) => { const { entitlementKey } = provider.properties[x] const hasAccess = entitlementKey == null || hasEntitlementAccess(entitlementKey) return ( ) })} {provider?.misc?.alert && ( {provider.misc.alert.description}} /> )} {provider.misc.requiresRedirect && ( } > )}
Save
) }