)
diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx
index 648a3f075fe..3cd49de2d3c 100644
--- a/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx
+++ b/apps/studio/components/interfaces/Auth/EmailTemplates/SpamValidation.tsx
@@ -1,17 +1,7 @@
import { Markdown } from 'components/interfaces/Markdown'
-import InformationBox from 'components/ui/InformationBox'
import { ValidateSpamResponse } from 'data/auth/validate-spam-mutation'
-import {
- cn,
- Separator,
- Table,
- TableBody,
- TableCell,
- TableHead,
- TableHeader,
- TableRow,
- WarningIcon,
-} from 'ui'
+import { Check, MailWarning } from 'lucide-react'
+import { Separator, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from 'ui'
interface SpamValidationProps {
validationResult?: ValidateSpamResponse
@@ -26,23 +16,23 @@ export const SpamValidation = ({ validationResult }: SpamValidationProps) => {
const hasSpamWarning = spamRules.length > 0
return (
- }
- title={
- hasSpamWarning
- ? 'Email has a high probability of being marked as spam and deliverability may be affected'
- : 'Email content is unlikely to be marked as spam'
- }
- description={
- hasSpamWarning ? (
+
+
+ {hasSpamWarning ? (
+
+ ) : (
+
+ )}
+
+
+
+ {hasSpamWarning
+ ? 'Email has a high probability of being marked as spam - review issues below to improve deliverability.'
+ : 'Email content is unlikely to be marked as spam'}
+
+ {hasSpamWarning && (
<>
-
- {hasSpamWarning
- ? ` Rectify the following issues to improve your email's deliverability in order of priority:`
- : ` Address the following issues to improve your email's deliverability:`}
-
@@ -68,8 +58,8 @@ export const SpamValidation = ({ validationResult }: SpamValidationProps) => {
content="Spam validation is powered by [SpamAssassin](https://spamassassin.apache.org/doc.html). Full list of all available warnings can be found [here](https://gist.github.com/ychaouche/a2faff159c2a1fea16019156972c7f8b)."
/>
>
- ) : null
- }
- />
+ )}
+
+
)
}
diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx
index 5e1d5738d1a..34d72fd0e02 100644
--- a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx
+++ b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.tsx
@@ -1,28 +1,39 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Code, Monitor } from 'lucide-react'
-import { useEffect, useMemo, useState } from 'react'
+import { editor } from 'monaco-editor'
+import { useEffect, useMemo, useRef, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import { toast } from 'sonner'
+import { useForm } from 'react-hook-form'
import { useParams } from 'common'
import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
-import { FormActions } from 'components/ui/Forms/FormActions'
-import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
-import InformationBox from 'components/ui/InformationBox'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useValidateSpamMutation, ValidateSpamResponse } from 'data/auth/validate-spam-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import type { FormSchema } from 'types'
import {
- Form,
- Input,
+ Badge,
+ Button,
+ Card,
+ CardContent,
+ CardFooter,
+ Form_Shadcn_,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ Input_Shadcn_,
+ Label_Shadcn_,
Tabs_Shadcn_,
TabsContent_Shadcn_,
TabsList_Shadcn_,
TabsTrigger_Shadcn_,
+ Tooltip,
+ TooltipContent,
+ TooltipTrigger,
} from 'ui'
import { Admonition } from 'ui-patterns'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { SpamValidation } from './SpamValidation'
interface TemplateEditorProps {
@@ -33,6 +44,9 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => {
const { ref: projectRef } = useParams()
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
+ // Add a ref to the code editor
+ const editorRef = useRef()
+
// [Joshen] Error state is handled in the parent
const { data: authConfig, isSuccess } = useAuthConfigQuery({ projectRef })
@@ -49,15 +63,6 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => {
const { id, properties } = template
- const formId = `auth-config-email-templates-${id}`
- const INITIAL_VALUES = useMemo(() => {
- const result: { [x: string]: string } = {}
- Object.keys(properties).forEach((key) => {
- result[key] = ((authConfig && authConfig[key as keyof typeof authConfig]) ?? '') as string
- })
- return result
- }, [authConfig, properties])
-
const messageSlug = `MAILER_TEMPLATES_${id}_CONTENT` as keyof typeof authConfig
const messageProperty = properties[messageSlug]
const builtInSMTP =
@@ -74,7 +79,33 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => {
const spamRules = (validationResult?.rules ?? []).filter((rule) => rule.score > 0)
const preventSaveFromSpamCheck = builtInSMTP && spamRules.length > 0
- const onSubmit = (values: any, { resetForm }: any) => {
+ // Create form values
+ const INITIAL_VALUES = useMemo(() => {
+ const result: { [x: string]: string } = {}
+ Object.keys(properties).forEach((key) => {
+ result[key] = ((authConfig && authConfig[key as keyof typeof authConfig]) ?? '') as string
+ })
+ return result
+ }, [authConfig, properties])
+
+ // Setup React Hook Form
+ const form = useForm({
+ defaultValues: INITIAL_VALUES,
+ })
+
+ // Update form values when authConfig changes
+ useEffect(() => {
+ if (authConfig) {
+ const values: { [key: string]: string } = {}
+ Object.keys(properties).forEach((key) => {
+ values[key] = ((authConfig && authConfig[key as keyof typeof authConfig]) ?? '') as string
+ })
+ form.reset(values)
+ setBodyValue((authConfig && authConfig[messageSlug]) ?? '')
+ }
+ }, [authConfig, properties, messageSlug, form])
+
+ const onSubmit = (values: any) => {
if (!projectRef) return console.error('Project ref is required')
setIsSavingTemplate(true)
@@ -112,10 +143,6 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => {
onSuccess: () => {
setIsSavingTemplate(false)
toast.success('Successfully updated settings')
- resetForm({
- values: values,
- initialValues: values,
- })
setHasUnsavedChanges(false) // Reset the unsaved changes state
},
}
@@ -157,140 +184,202 @@ const TemplateEditor = ({ template }: TemplateEditorProps) => {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [id])
- return (
-
+
)
}
diff --git a/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx b/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx
index f099a80fa74..99fc0c68105 100644
--- a/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx
+++ b/apps/studio/components/interfaces/Auth/SmtpForm/SmtpForm.tsx
@@ -1,14 +1,16 @@
+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 { number, object, string } from 'yup'
+import * as yup from 'yup'
-import { Markdown } from 'components/interfaces/Markdown'
-import { FormActions } from 'components/ui/Forms/FormActions'
-import { FormHeader } from 'components/ui/Forms/FormHeader'
-import { FormPanel } from 'components/ui/Forms/FormPanel'
-import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
+import {
+ ScaffoldContainer,
+ ScaffoldSection,
+ ScaffoldSectionTitle,
+} 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'
@@ -18,19 +20,35 @@ import {
AlertTitle_Shadcn_,
Alert_Shadcn_,
Button,
- Form,
- Input,
- InputNumber,
- Toggle,
+ Card,
+ CardContent,
+ CardFooter,
+ FormControl_Shadcn_,
+ FormField_Shadcn_,
+ Form_Shadcn_,
+ Input_Shadcn_,
+ PrePostTab,
+ Switch,
WarningIcon,
} from 'ui'
-import EmailRateLimitsAlert from '../EmailRateLimitsAlert'
+import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { urlRegex } from './../Auth.constants'
import { defaultDisabledSmtpFormValues } from './SmtpForm.constants'
import { generateFormValues, isSmtpEnabled } from './SmtpForm.utils'
import { AlertTriangle, Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
+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 {
@@ -45,46 +63,30 @@ const SmtpForm = () => {
const [enableSmtp, setEnableSmtp] = useState(false)
const [hidden, setHidden] = useState(true)
- const formId = 'auth-config-smtp-form'
- const initialValues = generateFormValues(authConfig)
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
- useEffect(() => {
- if (isSuccess && isSmtpEnabled(authConfig)) {
- setEnableSmtp(true)
- }
- }, [isSuccess, authConfig])
-
- const schema = object({
- SMTP_ADMIN_EMAIL: string().when([], {
- is: () => {
- return enableSmtp
- },
+ 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: string().when([], {
- is: () => {
- return enableSmtp
- },
+ SMTP_SENDER_NAME: yup.string().when('ENABLE_SMTP', {
+ is: true,
then: (schema) => schema.required('Sender name is required'),
otherwise: (schema) => schema,
}),
- SMTP_HOST: string().when([], {
- is: () => {
- return enableSmtp
- },
+ 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: number().when([], {
- is: () => {
- return enableSmtp
- },
+ SMTP_PORT: yup.number().when('ENABLE_SMTP', {
+ is: true,
then: (schema) =>
schema
.required('Port number is required.')
@@ -92,10 +94,8 @@ const SmtpForm = () => {
.max(65535, 'Must be a valid port number no more than 65535'),
otherwise: (schema) => schema,
}),
- SMTP_MAX_FREQUENCY: number().when([], {
- is: () => {
- return enableSmtp
- },
+ SMTP_MAX_FREQUENCY: yup.number().when('ENABLE_SMTP', {
+ is: true,
then: (schema) =>
schema
.required('Rate limit is required.')
@@ -103,28 +103,63 @@ const SmtpForm = () => {
.max(32767, 'Must not be more than 32,767 an hour'),
otherwise: (schema) => schema,
}),
- SMTP_USER: string().when([], {
- is: () => {
- return enableSmtp
- },
+ SMTP_USER: yup.string().when('ENABLE_SMTP', {
+ is: true,
then: (schema) => schema.required('SMTP Username is required'),
otherwise: (schema) => schema,
}),
- SMTP_PASS: string().when([], {
- is: () => {
- return enableSmtp && authConfig?.SMTP_PASS === null
- },
- then: (schema) => schema.required('SMTP password is required'),
- otherwise: (schema) => schema,
- }),
+ SMTP_PASS: yup.string(),
+ ENABLE_SMTP: yup.boolean().required(),
})
- const onSubmit = (values: any, { resetForm }: any) => {
- const payload = enableSmtp ? values : defaultDisabledSmtpFormValues
+ const form = useForm({
+ 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,
+ },
+ })
- // Format payload: Remove redundant value + convert port to string
- delete payload.ENABLE_SMTP
- payload.SMTP_PORT = payload.SMTP_PORT ? payload.SMTP_PORT.toString() : payload.SMTP_PORT
+ // 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
@@ -133,7 +168,7 @@ const SmtpForm = () => {
}
updateAuthConfig(
- { projectRef: projectRef!, config: payload },
+ { projectRef: projectRef!, config: payload as any },
{
onError: (error) => {
toast.error(`Failed to update settings: ${error.message}`)
@@ -141,8 +176,6 @@ const SmtpForm = () => {
onSuccess: () => {
setHidden(true)
toast.success('Successfully updated settings')
- const updatedFormValues = generateFormValues(payload)
- resetForm({ values: updatedFormValues, initialValues: updatedFormValues })
},
}
)
@@ -163,206 +196,284 @@ const SmtpForm = () => {
}
return (
-
- {({ resetForm, values }: any) => {
- const isValidSmtpConfig = isSmtpEnabled(values)
- const hasChanges = JSON.stringify(values) !== JSON.stringify(initialValues)
-
- // [Alaister] although this "technically" is breaking the rules of React hooks
- // it won't error because the hooks are always rendered in the same order
- // eslint-disable-next-line react-hooks/rules-of-hooks
- useEffect(() => {
- if (isSuccess) {
- const formValues = generateFormValues(authConfig)
- resetForm({ values: formValues, initialValues: formValues })
- }
- }, [isSuccess, authConfig])
-
- const onResetForm = () => {
- setEnableSmtp(isSmtpEnabled(initialValues))
- resetForm({ values: initialValues })
- }
-
- return (
- <>
-
-
-
- }
- >
-
-
- setEnableSmtp(value)}
- descriptionText={
-
- Emails will be sent using your custom SMTP provider. Email rate limits can
- be adjusted{' '}
-
- here
-
- .
-
- Check your SMTP provider
+ All fields below must be filled
- 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.
+ The following fields must be filled before custom SMTP can be properly
+ enabled
- )}
-
-
-
- Port used by your SMTP server. Common ports include 25, 465, and 587.{' '}
-
-
- 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.
-
- >
- }
- disabled={!canUpdateConfig}
- />
- seconds}
- disabled={!canUpdateConfig}
- />
-
- : }
- type="default"
- onClick={() => setHidden(!hidden)}
- />
- }
- disabled={!canUpdateConfig}
- descriptionText={
-
- For security reasons, the password is write-only. Once saved, it cannot be
- retrieved or displayed.
-
- }
- />
-
-
-
- >
- )
- }}
-
+
+ )}
+
+
+ {enableSmtp && (
+ <>
+
+
+
+
Sender details
+
+ Configure the sender information for your emails.
+
+ Your SMTP Credentials will always be encrypted in our database.
+
+
+
+ {form.watch('SMTP_HOST')?.endsWith('.gmail.com') && (
+
+
+ Check your SMTP provider
+
+ 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.
+
+
+ )}
+
+ (
+
+
+
+
+
+ )}
+ />
+
+ (
+
+
+ Port used by your SMTP server. Common ports include 25, 465, and
+ 587.{' '}
+
+
+ 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.
+
+ >
+ }
+ >
+
+ field.onChange(Number(e.target.value))}
+ placeholder="587"
+ disabled={!canUpdateConfig}
+ />
+
+
+ )}
+ />
+
+ (
+
+
+
+ field.onChange(Number(e.target.value))}
+ disabled={!canUpdateConfig}
+ />
+
+
+
+ )}
+ />
+
+ (
+
+
+
+
+
+ )}
+ />
+
+ (
+
+
+ setHidden(!hidden)}
+ icon={hidden ? : }
+ />
+ }
+ >
+
+
+
+
+ )}
+ />
+