mirror of
https://github.com/supabase/supabase.git
synced 2026-10-11 04:15:04 +03:00
## Problem We used to have a `_Shadcn_` suffix for all the shadcn form components because we also had `formik` form components. This is not needed anymore. ## Solution - Remove the suffix - Update all usages
497 lines
19 KiB
TypeScript
497 lines
19 KiB
TypeScript
import { zodResolver } from '@hookform/resolvers/zod'
|
|
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { useEffect, useState } from 'react'
|
|
import { SubmitHandler, useForm } from 'react-hook-form'
|
|
import { toast } from 'sonner'
|
|
import {
|
|
Button,
|
|
Card,
|
|
CardContent,
|
|
CardFooter,
|
|
cn,
|
|
Form,
|
|
FormControl,
|
|
FormField,
|
|
FormInputGroupInput,
|
|
Input_Shadcn_,
|
|
InputGroup,
|
|
InputGroupAddon,
|
|
InputGroupText,
|
|
Switch,
|
|
} from 'ui'
|
|
import { Admonition, PageSection, PageSectionContent } from 'ui-patterns'
|
|
import { Input } from 'ui-patterns/DataInputs/Input'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
import * as z from 'zod'
|
|
|
|
import { urlRegex } from '../Auth.constants'
|
|
import { defaultDisabledSmtpFormValues } from './SmtpForm.constants'
|
|
import { generateFormValues, isSmtpEnabled } from './SmtpForm.utils'
|
|
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'
|
|
|
|
const smtpEnabledSchema = z.object({
|
|
ENABLE_SMTP: z.literal(true),
|
|
SMTP_ADMIN_EMAIL: z
|
|
.string()
|
|
.trim()
|
|
.min(1, 'Sender email address is required')
|
|
.email('Must be a valid email'),
|
|
SMTP_SENDER_NAME: z.string().trim().min(1, 'Sender name is required'),
|
|
SMTP_HOST: z
|
|
.string()
|
|
.trim()
|
|
.min(1, 'Host URL is required')
|
|
.regex(urlRegex({ excludeSimpleDomains: false }), 'Must be a valid URL or IP address'),
|
|
SMTP_PORT: z.preprocess(
|
|
(val) => (val === '' || val == null ? undefined : val),
|
|
z.coerce
|
|
.number({
|
|
required_error: 'Port number is required',
|
|
invalid_type_error: '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')
|
|
),
|
|
SMTP_MAX_FREQUENCY: z.preprocess(
|
|
(val) => (val === '' || val == null ? undefined : val),
|
|
z.coerce
|
|
.number({
|
|
required_error: 'Rate limit is required',
|
|
invalid_type_error: 'Rate limit is required',
|
|
})
|
|
.min(1, 'Must be more than 0')
|
|
.max(32767, 'Must not be more than 32,767 an hour')
|
|
),
|
|
SMTP_USER: z.string().trim().min(1, 'SMTP Username is required'),
|
|
SMTP_PASS: z.string().trim().optional(),
|
|
})
|
|
|
|
const smtpDisabledSchema = z.object({
|
|
ENABLE_SMTP: z.literal(false),
|
|
SMTP_ADMIN_EMAIL: z.string().optional(),
|
|
SMTP_SENDER_NAME: z.string().optional(),
|
|
SMTP_HOST: z.string().optional(),
|
|
SMTP_PORT: z.preprocess(
|
|
(val) => (val === '' || val == null ? undefined : val),
|
|
z.coerce.number().optional()
|
|
),
|
|
SMTP_MAX_FREQUENCY: z.preprocess(
|
|
(val) => (val === '' || val == null ? undefined : val),
|
|
z.coerce.number().optional()
|
|
),
|
|
SMTP_USER: z.string().optional(),
|
|
SMTP_PASS: z.string().optional(),
|
|
})
|
|
|
|
const smtpSchema = z.discriminatedUnion('ENABLE_SMTP', [smtpEnabledSchema, smtpDisabledSchema])
|
|
|
|
type SmtpFormValues = z.infer<typeof smtpSchema>
|
|
|
|
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 { can: canReadConfig } = useAsyncCheckPermissions(
|
|
PermissionAction.READ,
|
|
'custom_config_gotrue'
|
|
)
|
|
const { can: canUpdateConfig } = useAsyncCheckPermissions(
|
|
PermissionAction.UPDATE,
|
|
'custom_config_gotrue'
|
|
)
|
|
|
|
const form = useForm<SmtpFormValues>({
|
|
resolver: zodResolver(
|
|
smtpSchema.superRefine((data, ctx) => {
|
|
const isEnablingSmtp = data.ENABLE_SMTP && !isSmtpEnabled(authConfig)
|
|
|
|
if (isEnablingSmtp && !data.SMTP_PASS) {
|
|
ctx.addIssue({
|
|
code: 'custom',
|
|
message: 'SMTP Password is required',
|
|
path: ['SMTP_PASS'],
|
|
})
|
|
}
|
|
})
|
|
),
|
|
defaultValues: {
|
|
SMTP_ADMIN_EMAIL: '',
|
|
SMTP_SENDER_NAME: '',
|
|
SMTP_HOST: '',
|
|
SMTP_PORT: undefined,
|
|
SMTP_MAX_FREQUENCY: undefined,
|
|
SMTP_USER: '',
|
|
SMTP_PASS: '',
|
|
ENABLE_SMTP: false,
|
|
},
|
|
})
|
|
|
|
const { isDirty } = form.formState
|
|
|
|
// Update form values when auth config is loaded
|
|
useEffect(() => {
|
|
if (authConfig) {
|
|
const formValues = generateFormValues(authConfig)
|
|
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: SubmitHandler<SmtpFormValues> = (values) => {
|
|
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: () => {
|
|
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 {...form}>
|
|
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
<Card>
|
|
<CardContent>
|
|
<FormField
|
|
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>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
disabled={!canUpdateConfig}
|
|
/>
|
|
</FormControl>
|
|
</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
|
|
control={form.control}
|
|
name="SMTP_ADMIN_EMAIL"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
label="Sender email address"
|
|
description="The email address the emails are sent from."
|
|
>
|
|
<FormControl>
|
|
<Input_Shadcn_
|
|
{...field}
|
|
placeholder="noreply@yourdomain.com"
|
|
disabled={!canUpdateConfig}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="SMTP_SENDER_NAME"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
label="Sender name"
|
|
description="Name displayed in the recipient's inbox."
|
|
>
|
|
<FormControl>
|
|
<Input_Shadcn_
|
|
{...field}
|
|
placeholder="Your Name"
|
|
disabled={!canUpdateConfig}
|
|
/>
|
|
</FormControl>
|
|
</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
|
|
control={form.control}
|
|
name="SMTP_HOST"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
label="Host"
|
|
description="Hostname or IP address of your SMTP server."
|
|
>
|
|
<FormControl>
|
|
<Input_Shadcn_
|
|
{...field}
|
|
placeholder="your.smtp.host.com"
|
|
disabled={!canUpdateConfig}
|
|
/>
|
|
</FormControl>
|
|
</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
|
|
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>
|
|
<Input_Shadcn_
|
|
type="number"
|
|
value={field.value}
|
|
onChange={(e) => field.onChange(e.target.value)}
|
|
placeholder="587"
|
|
disabled={!canUpdateConfig}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
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>
|
|
<InputGroup>
|
|
<FormInputGroupInput
|
|
type="number"
|
|
value={field.value}
|
|
onChange={(e) => field.onChange(e.target.value)}
|
|
disabled={!canUpdateConfig}
|
|
/>
|
|
<InputGroupAddon align="inline-end">
|
|
<InputGroupText>seconds</InputGroupText>
|
|
</InputGroupAddon>
|
|
</InputGroup>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="SMTP_USER"
|
|
render={({ field }) => (
|
|
<FormItemLayout
|
|
label="Username"
|
|
description="Username for your SMTP server."
|
|
>
|
|
<FormControl>
|
|
<Input_Shadcn_
|
|
{...field}
|
|
placeholder="SMTP Username"
|
|
disabled={!canUpdateConfig}
|
|
/>
|
|
</FormControl>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
|
|
<FormField
|
|
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>
|
|
<Input {...field} reveal copy disabled={!canUpdateConfig} />
|
|
</FormControl>
|
|
</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">
|
|
{isDirty && (
|
|
<Button
|
|
type="default"
|
|
onClick={() => {
|
|
form.reset()
|
|
setEnableSmtp(isSmtpEnabled(authConfig))
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
)}
|
|
<Button
|
|
type="primary"
|
|
htmlType="submit"
|
|
loading={isUpdatingConfig}
|
|
disabled={!canUpdateConfig || !isDirty}
|
|
>
|
|
Save changes
|
|
</Button>
|
|
</div>
|
|
</CardFooter>
|
|
</Card>
|
|
</form>
|
|
</Form>
|
|
</PageSectionContent>
|
|
</PageSection>
|
|
)
|
|
}
|