import { PermissionAction } from '@supabase/shared-types/out/constants' import { Check } from 'lucide-react' import { useTheme } from 'next-themes' import { useQueryState } from 'nuqs' import { useEffect, useState } from 'react' import ReactMarkdown from 'react-markdown' import { toast } from 'sonner' import { useParams } from 'common' 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 { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query' import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization' import { BASE_PATH } from 'lib/constants' import { Button, Form, Input, Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from 'ui' import { Admonition } from 'ui-patterns' import { NO_REQUIRED_CHARACTERS } from '../Auth.constants' import { AuthAlert } from './AuthAlert' import type { Provider } from './AuthProvidersForm.types' import FormField from './FormField' 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 { 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 isFreePlan = organization?.plan.id === 'free' const { data: settings } = useProjectSettingsV2Query({ projectRef }) const protocol = settings?.app_config?.protocol ?? 'https' const endpoint = settings?.app_config?.endpoint const apiUrl = `${protocol}://${endpoint}` const { data: customDomainData } = useCustomDomainsQuery({ projectRef }) const INITIAL_VALUES = (() => { const initialValues: { [x: string]: string | boolean } = {} Object.keys(provider.properties).forEach((key) => { const isDoubleNegative = doubleNegativeKeys.includes(key) if (provider.title === 'SAML 2.0') { const configValue = (config as any)[key] initialValues[key] = configValue || (provider.properties[key].type === 'boolean' ? false : '') } else { if (isDoubleNegative) { initialValues[key] = !(config as any)[key] } else { const configValue = (config as any)[key] initialValues[key] = configValue ? configValue : provider.properties[key].type === 'boolean' ? false : '' } } }) return initialValues })() const onSubmit = (values: any, { resetForm }: 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: () => { resetForm({ values: { ...values }, initialValues: { ...values } }) setOpen(false) setUrlProvider(null) 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]) return ( <> } meta={ isActive ? (
Enabled
) : (
Disabled
) } > {provider.title}
{`${provider.title} {provider.title}
{({ handleReset, initialValues, values, setFieldValue }: any) => { const noChanges = JSON.stringify(initialValues) === JSON.stringify(values) return ( <>
{Object.keys(provider.properties).map((x: string) => { let description = provider.properties[x].description if (description && projectRef) { description = description.replace( /\(\.\.\/auth\/(.*?)\)/g, `(/project/${projectRef}/auth/$1)` ) } const properties = { ...provider.properties[x], description: provider.properties[x].isPaid && isFreePlan ? `${description} Only available on [Pro plan](/org/${organization.slug}/billing?panel=subscriptionPlan) and above.` : description, } const isDisabledDueToPlan = properties.isPaid && isFreePlan const shouldDisable = properties.type === 'boolean' ? isDisabledDueToPlan && !values[x] : isDisabledDueToPlan return ( ) })} {provider?.misc?.alert && ( {provider.misc.alert.description} } /> )} {provider.misc.requiresRedirect && ( } /> )}
Save
) }}
) }