mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 03:45:06 +03:00
* Scaffold advisor rules * Wrap up implementation * Add feature flag * Fix type issues * Fix types * fix * Scaffold * Revise UI for advisor rules * Hide edit button * Refactor AdvisorRulesLayout * Some fixes from PR feedback
471 lines
19 KiB
TypeScript
471 lines
19 KiB
TypeScript
import { yupResolver } from '@hookform/resolvers/yup'
|
|
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { useEffect, useState } from 'react'
|
|
import { useForm } from 'react-hook-form'
|
|
import { toast } from 'sonner'
|
|
import * as yup from 'yup'
|
|
|
|
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
|
|
import NoPermission from 'components/ui/NoPermission'
|
|
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
|
|
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
|
|
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
|
|
import { AlertTriangle, Eye, EyeOff } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import {
|
|
AlertDescription_Shadcn_,
|
|
AlertTitle_Shadcn_,
|
|
Alert_Shadcn_,
|
|
Button,
|
|
Card,
|
|
CardContent,
|
|
CardFooter,
|
|
FormControl_Shadcn_,
|
|
FormField_Shadcn_,
|
|
Form_Shadcn_,
|
|
Input_Shadcn_,
|
|
PrePostTab,
|
|
Switch,
|
|
WarningIcon,
|
|
} from 'ui'
|
|
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
|
|
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
|
|
}
|
|
|
|
const SmtpForm = () => {
|
|
const { ref: projectRef } = useParams()
|
|
const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef })
|
|
const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation()
|
|
|
|
const [enableSmtp, setEnableSmtp] = useState(false)
|
|
const [hidden, setHidden] = useState(true)
|
|
|
|
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
|
|
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
|
|
|
|
const smtpSchema = yup.object({
|
|
SMTP_ADMIN_EMAIL: yup.string().when('ENABLE_SMTP', {
|
|
is: true,
|
|
then: (schema) => schema.email('Must be a valid email').required('Sender email is required'),
|
|
otherwise: (schema) => schema,
|
|
}),
|
|
SMTP_SENDER_NAME: yup.string().when('ENABLE_SMTP', {
|
|
is: true,
|
|
then: (schema) => schema.required('Sender name is required'),
|
|
otherwise: (schema) => schema,
|
|
}),
|
|
SMTP_HOST: yup.string().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().when('ENABLE_SMTP', {
|
|
is: true,
|
|
then: (schema) => schema.required('SMTP Username is required'),
|
|
otherwise: (schema) => schema,
|
|
}),
|
|
SMTP_PASS: yup.string(),
|
|
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 payload = ENABLE_SMTP ? rest : defaultDisabledSmtpFormValues
|
|
|
|
// 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 (
|
|
<Alert_Shadcn_ variant="destructive">
|
|
<WarningIcon />
|
|
<AlertTitle_Shadcn_>Failed to retrieve auth configuration</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>{authConfigError.message}</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
)
|
|
}
|
|
|
|
if (!canReadConfig) {
|
|
return <NoPermission resourceText="view SMTP settings" />
|
|
}
|
|
|
|
return (
|
|
<ScaffoldContainer>
|
|
<ScaffoldSection isFullWidth>
|
|
<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{' '}
|
|
<Link
|
|
className="underline"
|
|
href={`/project/${projectRef}/auth/rate-limits`}
|
|
>
|
|
here
|
|
</Link>
|
|
.
|
|
</p>
|
|
}
|
|
>
|
|
<FormControl_Shadcn_>
|
|
<Switch
|
|
checked={field.value}
|
|
onCheckedChange={field.onChange}
|
|
disabled={!canUpdateConfig}
|
|
/>
|
|
</FormControl_Shadcn_>
|
|
</FormItemLayout>
|
|
)}
|
|
/>
|
|
|
|
{enableSmtp && !isSmtpEnabled(form.getValues() as any) && (
|
|
<div className="mt-4">
|
|
<Alert_Shadcn_ variant="warning">
|
|
<AlertTriangle strokeWidth={2} />
|
|
<AlertTitle_Shadcn_>All fields below must be filled</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
The following fields must be filled before custom SMTP can be properly
|
|
enabled
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
</div>
|
|
)}
|
|
</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">
|
|
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"
|
|
description="This is 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="The name shown on the email"
|
|
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">
|
|
Your SMTP Credentials will always be encrypted in our database.
|
|
</p>
|
|
</div>
|
|
<div className="col-span-8 space-y-4">
|
|
{form.watch('SMTP_HOST')?.endsWith('.gmail.com') && (
|
|
<Alert_Shadcn_ variant="warning" className="mb-4">
|
|
<AlertTriangle strokeWidth={2} />
|
|
<AlertTitle_Shadcn_>Check your SMTP provider</AlertTitle_Shadcn_>
|
|
<AlertDescription_Shadcn_>
|
|
Not all SMTP providers are designed for the email sending required by
|
|
Supabase Auth. It looks like the SMTP provider you entered is designed
|
|
for sending personal email messages and not for sending transactional
|
|
messages. Although you can ignore this warning, email deliverability
|
|
may be impacted.
|
|
</AlertDescription_Shadcn_>
|
|
</Alert_Shadcn_>
|
|
)}
|
|
|
|
<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>
|
|
)}
|
|
/>
|
|
|
|
<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 25, 465, and
|
|
587.{' '}
|
|
</span>
|
|
<span className="mt-2 block">
|
|
Avoid using port 25 as modern SMTP email clients shouldn't use
|
|
this port, it is traditionally blocked by residential ISPs and
|
|
Cloud Hosting Providers, to curb the amount of 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 between emails being sent"
|
|
description="How long between each email can a new email be sent via your SMTP server."
|
|
>
|
|
<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="For security reasons, the password is write-only. Once saved, it cannot be retrieved or displayed."
|
|
>
|
|
<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="justify-end space-x-2">
|
|
{form.formState.isDirty && (
|
|
<Button type="default" onClick={() => form.reset()}>
|
|
Cancel
|
|
</Button>
|
|
)}
|
|
<Button
|
|
type="primary"
|
|
htmlType="submit"
|
|
loading={isUpdatingConfig}
|
|
disabled={!canUpdateConfig || !form.formState.isDirty}
|
|
>
|
|
Save changes
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
</form>
|
|
</Form_Shadcn_>
|
|
</ScaffoldSection>
|
|
</ScaffoldContainer>
|
|
)
|
|
}
|
|
|
|
export default SmtpForm
|