Files
supabase/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx
T
fe929aa416 fix: SMTP form shows incorrect rate limit message when already enabled (#43914)
## Description

Fixes #43800

When editing SMTP settings while custom SMTP is already enabled, the
form footer was incorrectly displaying "Rate limit for sending emails
will be increased to 30". The rate limit is only set during the initial
enable transition, so the message and submit behavior were out of sync.

## Changes

This fix aligns the footer message with the submit logic (Option A from
the issue):

- **Enabling SMTP**: Shows rate limit increase message
- **Already enabled, editing settings**: Shows "Custom SMTP settings
will be updated"
- **Disabling SMTP**: Shows rate limit reduction message

## Testing

1. Go to Authentication → SMTP Settings
2. Enable custom SMTP and save (rate limit is set to 30)
3. Edit any SMTP field (e.g. change host or port)
4. Footer now correctly shows "Custom SMTP settings will be updated"
instead of the rate limit message

Made with [Cursor](https://cursor.com)

---------

Co-authored-by: vj2303 <vishnu.jangid@ax-ia.ai>
Co-authored-by: Danny White <3104761+dnywh@users.noreply.github.com>
2026-03-24 19:11:13 +11:00

513 lines
19 KiB
TypeScript

import { yupResolver } from '@hookform/resolvers/yup'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import AlertError from 'components/ui/AlertError'
import { InlineLink } from 'components/ui/InlineLink'
import NoPermission from 'components/ui/NoPermission'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Eye, EyeOff } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import {
Button,
Card,
CardContent,
CardFooter,
cn,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Input_Shadcn_,
PrePostTab,
Switch,
} from 'ui'
import { Admonition, PageSection, PageSectionContent } from 'ui-patterns'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as yup from 'yup'
import { urlRegex } from '../Auth.constants'
import { defaultDisabledSmtpFormValues } from './SmtpForm.constants'
import { generateFormValues, isSmtpEnabled } from './SmtpForm.utils'
interface SmtpFormValues {
SMTP_ADMIN_EMAIL?: string
SMTP_SENDER_NAME?: string
SMTP_HOST?: string
SMTP_PORT?: number
SMTP_MAX_FREQUENCY?: number
SMTP_USER?: string
SMTP_PASS?: string
ENABLE_SMTP: boolean
}
export const SmtpForm = () => {
const { ref: projectRef } = useParams()
const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation()
const [enableSmtp, setEnableSmtp] = useState(false)
const [hidden, setHidden] = useState(true)
const { can: canReadConfig } = useAsyncCheckPermissions(
PermissionAction.READ,
'custom_config_gotrue'
)
const { can: canUpdateConfig } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const smtpSchema = yup.object({
SMTP_ADMIN_EMAIL: yup
.string()
.trim()
.when('ENABLE_SMTP', {
is: true,
then: (schema) =>
schema.email('Must be a valid email').required('Sender email address is required'),
otherwise: (schema) => schema,
}),
SMTP_SENDER_NAME: yup
.string()
.trim()
.when('ENABLE_SMTP', {
is: true,
then: (schema) => schema.required('Sender name is required'),
otherwise: (schema) => schema,
}),
SMTP_HOST: yup
.string()
.trim()
.when('ENABLE_SMTP', {
is: true,
then: (schema) =>
schema
.matches(urlRegex({ excludeSimpleDomains: false }), 'Must be a valid URL or IP address')
.required('Host URL is required.'),
otherwise: (schema) => schema,
}),
SMTP_PORT: yup.number().when('ENABLE_SMTP', {
is: true,
then: (schema) =>
schema
.required('Port number is required.')
.min(1, 'Must be a valid port number more than 0')
.max(65535, 'Must be a valid port number no more than 65535'),
otherwise: (schema) => schema,
}),
SMTP_MAX_FREQUENCY: yup.number().when('ENABLE_SMTP', {
is: true,
then: (schema) =>
schema
.required('Rate limit is required.')
.min(1, 'Must be more than 0')
.max(32767, 'Must not be more than 32,767 an hour'),
otherwise: (schema) => schema,
}),
SMTP_USER: yup
.string()
.trim()
.when('ENABLE_SMTP', {
is: true,
then: (schema) => schema.required('SMTP Username is required'),
otherwise: (schema) => schema,
}),
SMTP_PASS: yup.string().trim(),
ENABLE_SMTP: yup.boolean().required(),
})
const form = useForm<SmtpFormValues>({
resolver: yupResolver(smtpSchema),
defaultValues: {
SMTP_ADMIN_EMAIL: '',
SMTP_SENDER_NAME: '',
SMTP_HOST: '',
SMTP_PORT: undefined,
SMTP_MAX_FREQUENCY: undefined,
SMTP_USER: '',
SMTP_PASS: '',
ENABLE_SMTP: false,
},
})
// Update form values when auth config is loaded
useEffect(() => {
if (authConfig) {
const formValues = generateFormValues(authConfig)
// Convert SMTP_PORT from string to number if it exists
if (formValues.SMTP_PORT) {
formValues.SMTP_PORT = Number(formValues.SMTP_PORT) as any
}
form.reset({
...formValues,
ENABLE_SMTP: isSmtpEnabled(authConfig),
} as SmtpFormValues)
setEnableSmtp(isSmtpEnabled(authConfig))
}
}, [authConfig, form])
// Update enableSmtp state when the form field changes
useEffect(() => {
const subscription = form.watch((value, { name }) => {
if (name === 'ENABLE_SMTP') {
setEnableSmtp(value.ENABLE_SMTP as boolean)
}
})
return () => subscription.unsubscribe()
}, [form])
const onSubmit = (values: SmtpFormValues) => {
const { ENABLE_SMTP, ...rest } = values
const basePayload = ENABLE_SMTP ? rest : defaultDisabledSmtpFormValues
// When enabling SMTP, set RATE_LIMIT_EMAIL_SENT to 30
// When disabling, backend will handle resetting to default
const isEnablingSmtp = ENABLE_SMTP && !isSmtpEnabled(authConfig)
const payload = {
...basePayload,
...(isEnablingSmtp && { RATE_LIMIT_EMAIL_SENT: 30 }),
}
// Format payload: Convert port to string
if (payload.SMTP_PORT) {
payload.SMTP_PORT = payload.SMTP_PORT.toString() as any
}
// the SMTP_PASS is write-only, it's never shown. If we don't delete it from the payload, it will replace the
// previously saved value with an empty one
if (payload.SMTP_PASS === '') {
delete payload.SMTP_PASS
}
updateAuthConfig(
{ projectRef: projectRef!, config: payload as any },
{
onError: (error) => {
toast.error(`Failed to update settings: ${error.message}`)
},
onSuccess: () => {
setHidden(true)
toast.success('Successfully updated settings')
},
}
)
}
if (isError) {
return (
<PageSection>
<PageSectionContent>
<AlertError error={authConfigError} subject="Failed to retrieve auth configuration" />
</PageSectionContent>
</PageSection>
)
}
if (!canReadConfig) {
return (
<PageSection>
<PageSectionContent>
<NoPermission resourceText="view SMTP settings" />
</PageSectionContent>
</PageSection>
)
}
const showFooterMessage =
form.formState.isDirty && ((enableSmtp && !isSmtpEnabled(authConfig)) || !enableSmtp)
return (
<PageSection>
<PageSectionContent>
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="ENABLE_SMTP"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Enable custom SMTP"
description={
<p className="max-w-full prose text-sm text-foreground-lighter">
Emails will be sent using your custom SMTP provider. Email rate limits can
be adjusted{' '}
<InlineLink href={`/project/${projectRef}/auth/rate-limits`}>
here
</InlineLink>
.
</p>
}
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
{enableSmtp && !isSmtpEnabled(form.getValues() as any) && (
<Admonition
type="warning"
title="All fields must be filled"
description="Each of the fields below must be filled before custom SMTP can be enabled."
className="bg-warning-200 border-warning-400 mt-4"
/>
)}
</CardContent>
{enableSmtp && (
<>
<CardContent className="py-6">
<div className="grid grid-cols-12 gap-6">
<div className="col-span-4">
<h3 className="text-sm mb-1">Sender details</h3>
<p className="text-sm text-foreground-lighter text-balance">
Configure the sender information for your emails.
</p>
</div>
<div className="col-span-8 space-y-4">
<FormField_Shadcn_
control={form.control}
name="SMTP_ADMIN_EMAIL"
render={({ field }) => (
<FormItemLayout
label="Sender email address"
description="The email address the emails are sent from."
>
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
placeholder="noreply@yourdomain.com"
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
control={form.control}
name="SMTP_SENDER_NAME"
render={({ field }) => (
<FormItemLayout
label="Sender name"
description="Name displayed in the recipient's inbox."
>
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
placeholder="Your Name"
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</div>
</div>
</CardContent>
<CardContent className="py-6">
<div className="grid grid-cols-12 gap-6">
<div className="col-span-4">
<h3 className="text-sm mb-1">SMTP provider settings</h3>
<p className="text-sm text-foreground-lighter text-balance">
Your SMTP credentials will always be encrypted in our database.
</p>
</div>
<div className="col-span-8 space-y-4">
<FormField_Shadcn_
control={form.control}
name="SMTP_HOST"
render={({ field }) => (
<FormItemLayout
label="Host"
description="Hostname or IP address of your SMTP server."
>
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
placeholder="your.smtp.host.com"
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
{form.watch('SMTP_HOST')?.endsWith('.gmail.com') && (
<Admonition
type="warning"
title="Check your SMTP provider"
description="It looks like the SMTP provider you entered is designed
for sending personal rather than transactional email messages. Email deliverability may
be impacted."
className="mb-4 bg-warning-200 border-warning-400"
/>
)}
<FormField_Shadcn_
control={form.control}
name="SMTP_PORT"
render={({ field }) => (
<FormItemLayout
label="Port number"
description={
<>
<span className="block">
Port used by your SMTP server. Common ports include 465 and 587.
Avoid using port 25 as it is often blocked by providers to curb
spam.
</span>
</>
}
>
<FormControl_Shadcn_>
<Input_Shadcn_
type="number"
value={field.value}
onChange={(e) => field.onChange(Number(e.target.value))}
placeholder="587"
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
control={form.control}
name="SMTP_MAX_FREQUENCY"
render={({ field }) => (
<FormItemLayout
label="Minimum interval per user"
description="The minimum time in seconds between emails before another email can be sent to the same user."
>
<FormControl_Shadcn_>
<PrePostTab postTab="seconds">
<Input_Shadcn_
type="number"
value={field.value}
onChange={(e) => field.onChange(Number(e.target.value))}
disabled={!canUpdateConfig}
/>
</PrePostTab>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
control={form.control}
name="SMTP_USER"
render={({ field }) => (
<FormItemLayout
label="Username"
description="Username for your SMTP server."
>
<FormControl_Shadcn_>
<Input_Shadcn_
{...field}
placeholder="SMTP Username"
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
control={form.control}
name="SMTP_PASS"
render={({ field }) => (
<FormItemLayout
label="Password"
description="Password for your SMTP server. For security reasons, this password cannot be viewed once saved."
>
<FormControl_Shadcn_>
<PrePostTab
postTab={
<Button
type="text"
className="p-0"
onClick={() => setHidden(!hidden)}
icon={hidden ? <Eye /> : <EyeOff />}
/>
}
>
<Input_Shadcn_
{...field}
type={hidden ? 'password' : 'text'}
placeholder={
authConfig?.SMTP_PASS === null ? 'SMTP Password' : '••••••••'
}
disabled={!canUpdateConfig}
/>
</PrePostTab>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</div>
</div>
</CardContent>
</>
)}
<CardFooter
className={cn(showFooterMessage ? 'justify-between' : 'justify-end', 'gap-x-2')}
>
{showFooterMessage &&
(enableSmtp ? (
<p className="text-sm text-foreground-light">
Rate limit for sending emails will be increased to 30 and{' '}
<InlineLink href={`/project/${projectRef}/auth/rate-limits`}>
can be adjusted
</InlineLink>{' '}
after enabling custom SMTP
</p>
) : (
<p className="text-sm text-foreground-light">
Rate limit for sending emails will be reduced to 2 after disabling custom SMTP
</p>
))}
<div className="flex items-center gap-x-2">
{form.formState.isDirty && (
<Button
type="default"
onClick={() => {
form.reset()
setEnableSmtp(isSmtpEnabled(authConfig))
}}
>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
loading={isUpdatingConfig}
disabled={!canUpdateConfig || !form.formState.isDirty}
>
Save changes
</Button>
</div>
</CardFooter>
</Card>
</form>
</Form_Shadcn_>
</PageSectionContent>
</PageSection>
)
}