mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Updating authentication email pages (#33958)
* init PrePostTab component * init * support auto fixing rounded of inputs/select/textarea * remove form item layout prepost * Update FormLayout.tsx * Update FormLayout.tsx * Update FormLayout.tsx * form layout and advanced * Update advanced.tsx * protection and hooks * auth emails * preview styles * fix * parse --------- Co-authored-by: Jonathan Summers-Muir <MildTomato@users.noreply.github.com>
This commit is contained in:
1 parent
326d2cbdcb
commit
419252c102
7 files changed
+685
-562
No files matched your search
@@ -6,6 +6,8 @@ import { FormPanel } from 'components/ui/Forms/FormPanel'
|
||||
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
|
||||
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
ScrollArea,
|
||||
Tabs_Shadcn_,
|
||||
TabsContent_Shadcn_,
|
||||
@@ -37,20 +39,20 @@ const EmailTemplates = () => {
|
||||
<AlertError error={authConfigError} subject="Failed to retrieve auth configuration" />
|
||||
)}
|
||||
{isLoading && (
|
||||
<div className="w-[854px]">
|
||||
<div className="w-[854px] mt-12">
|
||||
<GenericSkeletonLoader />
|
||||
</div>
|
||||
)}
|
||||
{isSuccess && (
|
||||
<>
|
||||
<div className="my-12">
|
||||
{builtInSMTP ? (
|
||||
<div className="mb-4">
|
||||
<EmailRateLimitsAlert />
|
||||
</div>
|
||||
) : null}
|
||||
<FormPanel>
|
||||
<Card>
|
||||
<Tabs_Shadcn_ defaultValue={TEMPLATES_SCHEMAS[0].title.trim().replace(/\s+/g, '-')}>
|
||||
<TabsList_Shadcn_ className="px-4 md:px-8 pt-2 gap-5 overflow-x-scroll no-scrollbar">
|
||||
<TabsList_Shadcn_ className="pt-2 px-6 gap-5 mb-0 overflow-x-scroll no-scrollbar mb-4">
|
||||
{TEMPLATES_SCHEMAS.map((template) => {
|
||||
return (
|
||||
<TabsTrigger_Shadcn_ value={template.title.trim().replace(/\s+/g, '-')}>
|
||||
@@ -59,18 +61,17 @@ const EmailTemplates = () => {
|
||||
)
|
||||
})}
|
||||
</TabsList_Shadcn_>
|
||||
|
||||
{TEMPLATES_SCHEMAS.map((template) => {
|
||||
const panelId = template.title.trim().replace(/\s+/g, '-')
|
||||
return (
|
||||
<TabsContent_Shadcn_ value={panelId} key={panelId}>
|
||||
<TabsContent_Shadcn_ value={panelId} key={panelId} className="mt-0">
|
||||
<TemplateEditor key={template.title} template={template} />
|
||||
</TabsContent_Shadcn_>
|
||||
)
|
||||
})}
|
||||
</Tabs_Shadcn_>
|
||||
</FormPanel>
|
||||
</>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
<InformationBox
|
||||
className={cn('mb-2', hasSpamWarning && '!bg-alternative/75', '!px-0', 'rounded-t-none')}
|
||||
icon={hasSpamWarning && <WarningIcon />}
|
||||
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 ? (
|
||||
<div className="flex items-start gap-2">
|
||||
<div className="mt-0.5">
|
||||
{hasSpamWarning ? (
|
||||
<MailWarning size={16} strokeWidth={1.5} className="text-warning" />
|
||||
) : (
|
||||
<Check size={16} strokeWidth={1.5} className="text-brand" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<h5 className="text-sm">
|
||||
{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'}
|
||||
</h5>
|
||||
{hasSpamWarning && (
|
||||
<>
|
||||
<div className="flex flex-col gap-y-3">
|
||||
<p>
|
||||
{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:`}
|
||||
</p>
|
||||
<Table>
|
||||
<TableHeader className="font-mono uppercase text-xs [&_th]:h-auto [&_th]:py-2">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
@@ -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
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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<editor.IStandaloneCodeEditor>()
|
||||
|
||||
// [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 (
|
||||
<Form id={formId} className="!border-t-0" initialValues={INITIAL_VALUES} onSubmit={onSubmit}>
|
||||
{({ resetForm, values, initialValues }: any) => {
|
||||
const message = (authConfig && authConfig[messageSlug]) ?? ''
|
||||
const hasChanges =
|
||||
JSON.stringify(values) !== JSON.stringify(initialValues) || message !== bodyValue
|
||||
// Single useMemo hook to parse and prepare message variables
|
||||
const messageVariables = useMemo(() => {
|
||||
if (!messageProperty?.description) return []
|
||||
|
||||
return (
|
||||
// Parse bullet point format: - `{{ .Variable }}` : Description
|
||||
const lines = messageProperty.description.split('\n')
|
||||
const variables: { variable: string; description: string }[] = []
|
||||
|
||||
for (const line of lines) {
|
||||
// Match lines that start with a bullet point followed by a variable in the format {{ .Variable }}
|
||||
// Handle variations in formatting (with or without backticks, different spacing)
|
||||
const match = line.match(/-\s*`?({{\s*\.\w+\s*}})`?\s*(?::|-)?\s*(.+)/)
|
||||
if (match && match[1] && match[2]) {
|
||||
variables.push({
|
||||
variable: match[1].replace(/`/g, '').trim(),
|
||||
description: match[2].trim(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return variables
|
||||
}, [messageProperty?.description])
|
||||
|
||||
// Check if form values have changed
|
||||
const formValues = form.watch()
|
||||
const hasFormChanges = JSON.stringify(formValues) !== JSON.stringify(INITIAL_VALUES)
|
||||
const hasChanges = hasFormChanges || ((authConfig && authConfig[messageSlug]) ?? '') !== bodyValue
|
||||
|
||||
// Function to insert text at cursor position
|
||||
const insertTextAtCursor = (text: string) => {
|
||||
if (!editorRef.current) return
|
||||
|
||||
const editor = editorRef.current
|
||||
const selection = editor.getSelection()
|
||||
|
||||
if (selection) {
|
||||
const range = {
|
||||
startLineNumber: selection.startLineNumber,
|
||||
startColumn: selection.startColumn,
|
||||
endLineNumber: selection.endLineNumber,
|
||||
endColumn: selection.endColumn,
|
||||
}
|
||||
|
||||
editor.executeEdits('insert-variable', [
|
||||
{
|
||||
range,
|
||||
text,
|
||||
forceMoveMarkers: true,
|
||||
},
|
||||
])
|
||||
|
||||
// Focus the editor after insertion
|
||||
editor.focus()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form_Shadcn_ {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<CardContent>
|
||||
{Object.keys(properties).map((x: string) => {
|
||||
const property = properties[x]
|
||||
if (property.type === 'string' && x !== messageSlug) {
|
||||
return (
|
||||
<FormField_Shadcn_
|
||||
key={x}
|
||||
control={form.control}
|
||||
name={x}
|
||||
render={({ field }) => (
|
||||
<FormItemLayout
|
||||
label={property.title}
|
||||
description={
|
||||
property.description ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{property.description}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
}
|
||||
labelOptional={
|
||||
property.descriptionOptional ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{property.descriptionOptional}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
}
|
||||
>
|
||||
<FormControl_Shadcn_>
|
||||
<Input_Shadcn_ id={x} {...field} disabled={!canUpdateConfig} />
|
||||
</FormControl_Shadcn_>
|
||||
</FormItemLayout>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</CardContent>
|
||||
|
||||
{messageProperty && (
|
||||
<>
|
||||
<FormSection className="!border-t-0 pb-8 !pt-4">
|
||||
<FormSectionContent loading={false}>
|
||||
{Object.keys(properties).map((x: string) => {
|
||||
const property = properties[x]
|
||||
if (property.type === 'string') {
|
||||
return (
|
||||
<div key={x} className="space-y-3">
|
||||
<label className="col-span-12 text-sm text-foreground lg:col-span-5">
|
||||
{property.title}
|
||||
</label>
|
||||
<Input
|
||||
size="small"
|
||||
layout="vertical"
|
||||
id={x}
|
||||
key={x}
|
||||
name={x}
|
||||
descriptionText={
|
||||
property.description ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{property.description}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
}
|
||||
labelOptional={
|
||||
property.descriptionOptional ? (
|
||||
<ReactMarkdown unwrapDisallowed disallowedElements={['p']}>
|
||||
{property.descriptionOptional}
|
||||
</ReactMarkdown>
|
||||
) : null
|
||||
}
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})}
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
<FormSection className="!mt-0 grid-cols-12 !border-t-0 !pt-0 pb-7">
|
||||
<FormSectionContent fullWidth loading={false}>
|
||||
{messageProperty && (
|
||||
<>
|
||||
<div className="space-y-3">
|
||||
<FormSectionLabel>
|
||||
<span>{messageProperty.title}</span>
|
||||
</FormSectionLabel>
|
||||
<InformationBox
|
||||
defaultVisibility
|
||||
title="Message variables"
|
||||
hideCollapse={false}
|
||||
description={
|
||||
messageProperty.description && (
|
||||
<ReactMarkdown>{messageProperty.description}</ReactMarkdown>
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Tabs_Shadcn_ defaultValue="source">
|
||||
<TabsList_Shadcn_ className="gap-3">
|
||||
<TabsTrigger_Shadcn_ value="source" className="gap-2">
|
||||
<Code size={14} />
|
||||
Source
|
||||
</TabsTrigger_Shadcn_>
|
||||
<TabsTrigger_Shadcn_ value="preview" className="gap-2">
|
||||
<Monitor size={14} />
|
||||
Preview
|
||||
</TabsTrigger_Shadcn_>
|
||||
</TabsList_Shadcn_>
|
||||
<TabsContent_Shadcn_ value="source" className="-space-y-px">
|
||||
<CodeEditor
|
||||
id="code-id"
|
||||
language="html"
|
||||
isReadOnly={!canUpdateConfig}
|
||||
className="!mb-0 relative h-96 overflow-hidden rounded border rounded-b-none"
|
||||
onInputChange={(e: string | undefined) => {
|
||||
setBodyValue(e ?? '')
|
||||
if (bodyValue !== e) setHasUnsavedChanges(true)
|
||||
}}
|
||||
options={{ wordWrap: 'on', contextmenu: false }}
|
||||
value={bodyValue}
|
||||
/>
|
||||
<SpamValidation validationResult={validationResult} />
|
||||
</TabsContent_Shadcn_>
|
||||
<TabsContent_Shadcn_ value="preview">
|
||||
<Admonition
|
||||
type="default"
|
||||
title="The preview may differ slightly from the actual rendering in the email client"
|
||||
/>
|
||||
<iframe
|
||||
className="!mb-0 overflow-hidden h-96 w-full rounded border"
|
||||
title={id}
|
||||
srcDoc={bodyValue}
|
||||
/>
|
||||
</TabsContent_Shadcn_>
|
||||
</Tabs_Shadcn_>
|
||||
</>
|
||||
)}
|
||||
<div className="col-span-12 flex w-full">
|
||||
<FormActions
|
||||
handleReset={() => {
|
||||
resetForm({
|
||||
values: authConfig,
|
||||
initialValues: authConfig,
|
||||
})
|
||||
setBodyValue((authConfig && authConfig[messageSlug]) ?? '')
|
||||
<CardContent className="p-0">
|
||||
<Label_Shadcn_ className="p-6 pb-4 block">Message body</Label_Shadcn_>
|
||||
<Tabs_Shadcn_ defaultValue="source">
|
||||
<TabsList_Shadcn_ className="gap-3 px-6">
|
||||
<TabsTrigger_Shadcn_ value="source" className="gap-2">
|
||||
<Code size={14} />
|
||||
Source
|
||||
</TabsTrigger_Shadcn_>
|
||||
<TabsTrigger_Shadcn_ value="preview" className="gap-2">
|
||||
<Monitor size={14} />
|
||||
Preview
|
||||
</TabsTrigger_Shadcn_>
|
||||
</TabsList_Shadcn_>
|
||||
<TabsContent_Shadcn_ value="source" className="p-0 mt-0">
|
||||
<CodeEditor
|
||||
id="code-id"
|
||||
language="html"
|
||||
isReadOnly={!canUpdateConfig}
|
||||
className="!mb-0 relative h-96 overflow-hidden p-0"
|
||||
onInputChange={(e: string | undefined) => {
|
||||
setBodyValue(e ?? '')
|
||||
if (bodyValue !== e) setHasUnsavedChanges(true)
|
||||
}}
|
||||
form={formId}
|
||||
isSubmitting={isSavingTemplate}
|
||||
hasChanges={hasChanges}
|
||||
disabled={!canUpdateConfig}
|
||||
helper={
|
||||
preventSaveFromSpamCheck
|
||||
? 'Please rectify all spam warnings before saving while using the built-in email service'
|
||||
: !canUpdateConfig
|
||||
? 'You need additional permissions to update authentication settings'
|
||||
: undefined
|
||||
}
|
||||
options={{ wordWrap: 'on', contextmenu: false, padding: { top: 16 } }}
|
||||
value={bodyValue}
|
||||
editorRef={editorRef}
|
||||
/>
|
||||
</div>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
{messageVariables.length > 0 && (
|
||||
<div className="px-6 py-3 border-t bg-surface-200">
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{messageVariables.map(({ variable, description }) => (
|
||||
<Tooltip key={variable}>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="outline"
|
||||
size="tiny"
|
||||
className="rounded-full"
|
||||
onClick={() => insertTextAtCursor(variable)}
|
||||
>
|
||||
{variable}
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top">
|
||||
<p>{description || 'Variable description not available'}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</TabsContent_Shadcn_>
|
||||
<TabsContent_Shadcn_ value="preview" className="pt-0 mt-0">
|
||||
<iframe
|
||||
className="!mb-0 mt-0 overflow-hidden h-96 w-full"
|
||||
title={id}
|
||||
srcDoc={bodyValue}
|
||||
/>
|
||||
<Admonition
|
||||
type="default"
|
||||
title="The preview may differ slightly from the actual rendering in the email client"
|
||||
className="rounded-none border-0 mb-0"
|
||||
/>
|
||||
</TabsContent_Shadcn_>
|
||||
</Tabs_Shadcn_>
|
||||
</CardContent>
|
||||
<CardContent>
|
||||
<SpamValidation validationResult={validationResult} />
|
||||
</CardContent>
|
||||
<CardFooter className="justify-end space-x-2">
|
||||
{hasChanges && (
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => {
|
||||
form.reset(INITIAL_VALUES)
|
||||
setBodyValue((authConfig && authConfig[messageSlug]) ?? '')
|
||||
setHasUnsavedChanges(false)
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
disabled={
|
||||
!canUpdateConfig || isSavingTemplate || !hasChanges || preventSaveFromSpamCheck
|
||||
}
|
||||
loading={isSavingTemplate}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
)}
|
||||
</form>
|
||||
</Form_Shadcn_>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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,
|
||||
},
|
||||
})
|
||||
|
||||
// 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 (
|
||||
<Form id={formId} initialValues={initialValues} onSubmit={onSubmit} validationSchema={schema}>
|
||||
{({ 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 (
|
||||
<>
|
||||
<FormPanel
|
||||
footer={
|
||||
<div className="flex py-4 px-8">
|
||||
<FormActions
|
||||
form={formId}
|
||||
isSubmitting={isUpdatingConfig}
|
||||
hasChanges={hasChanges}
|
||||
handleReset={onResetForm}
|
||||
disabled={!canUpdateConfig}
|
||||
helper={
|
||||
!canUpdateConfig
|
||||
? 'You need additional permissions to update authentication settings'
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<FormSection>
|
||||
<FormSectionContent className="!col-span-12 !gap-y-2" loading={isLoading}>
|
||||
<Toggle
|
||||
size="small"
|
||||
layout="flex"
|
||||
name="ENABLE_SMTP"
|
||||
label="Enable Custom SMTP"
|
||||
checked={enableSmtp}
|
||||
disabled={!canUpdateConfig}
|
||||
// @ts-ignore
|
||||
onChange={(value: boolean) => setEnableSmtp(value)}
|
||||
descriptionText={
|
||||
<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>
|
||||
}
|
||||
/>
|
||||
{enableSmtp && !isValidSmtpConfig && !isValidSmtpConfig && (
|
||||
<div className="">
|
||||
<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>
|
||||
<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>
|
||||
)}
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
/>
|
||||
|
||||
<FormSection
|
||||
visible={enableSmtp}
|
||||
header={<FormSectionLabel>Sender details</FormSectionLabel>}
|
||||
disabled={!enableSmtp}
|
||||
>
|
||||
<FormSectionContent loading={isLoading}>
|
||||
<Input
|
||||
name="SMTP_ADMIN_EMAIL"
|
||||
id="SMTP_ADMIN_EMAIL"
|
||||
label="Sender email"
|
||||
descriptionText="This is the email address the emails are sent from"
|
||||
placeholder="noreply@yourdomain.com"
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<Input
|
||||
name="SMTP_SENDER_NAME"
|
||||
id="SMTP_SENDER_NAME"
|
||||
label="Sender name"
|
||||
descriptionText="Name displayed in the recipient's inbox"
|
||||
placeholder="The name shown on the email"
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
|
||||
<FormSection
|
||||
visible={enableSmtp}
|
||||
disabled={!enableSmtp}
|
||||
header={
|
||||
<FormSectionLabel>
|
||||
<span>SMTP Provider Settings</span>
|
||||
<p className="my-4 text-foreground-lighter">
|
||||
Your SMTP Credentials will always be encrypted in our database.
|
||||
</p>
|
||||
</FormSectionLabel>
|
||||
}
|
||||
>
|
||||
<FormSectionContent loading={isLoading}>
|
||||
{values['SMTP_HOST'] && values['SMTP_HOST'].endsWith('.gmail.com') && (
|
||||
{enableSmtp && !isSmtpEnabled(form.getValues() as any) && (
|
||||
<div className="mt-4">
|
||||
<Alert_Shadcn_ variant="warning">
|
||||
<AlertTriangle strokeWidth={2} />
|
||||
<AlertTitle_Shadcn_>Check your SMTP provider</AlertTitle_Shadcn_>
|
||||
<AlertTitle_Shadcn_>All fields below must be filled</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.
|
||||
The following fields must be filled before custom SMTP can be properly
|
||||
enabled
|
||||
</AlertDescription_Shadcn_>
|
||||
</Alert_Shadcn_>
|
||||
)}
|
||||
<Input
|
||||
name="SMTP_HOST"
|
||||
placeholder="your.smtp.host.com"
|
||||
id="SMTP_HOST"
|
||||
label="Host"
|
||||
descriptionText="Hostname or IP address of your SMTP server."
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<InputNumber
|
||||
name="SMTP_PORT"
|
||||
id="SMTP_PORT"
|
||||
placeholder="587"
|
||||
label="Port number"
|
||||
descriptionText={
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
}
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<InputNumber
|
||||
id="SMTP_MAX_FREQUENCY"
|
||||
name="SMTP_MAX_FREQUENCY"
|
||||
label="Minimum interval between emails being sent"
|
||||
descriptionText="How long between each email can a new email be sent via your SMTP server."
|
||||
actions={<span className="mr-3 text-foreground-lighter">seconds</span>}
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<Input
|
||||
name="SMTP_USER"
|
||||
id="SMTP_USER"
|
||||
label="Username"
|
||||
placeholder="SMTP Username"
|
||||
disabled={!canUpdateConfig}
|
||||
/>
|
||||
<Input
|
||||
name="SMTP_PASS"
|
||||
id="SMTP_PASS"
|
||||
type={hidden ? 'password' : 'text'}
|
||||
label="Password"
|
||||
placeholder={authConfig?.SMTP_PASS === null ? 'SMTP Password' : '••••••••'}
|
||||
actions={
|
||||
<Button
|
||||
icon={hidden ? <Eye /> : <EyeOff />}
|
||||
type="default"
|
||||
onClick={() => setHidden(!hidden)}
|
||||
/>
|
||||
}
|
||||
disabled={!canUpdateConfig}
|
||||
descriptionText={
|
||||
<span>
|
||||
For security reasons, the password is write-only. Once saved, it cannot be
|
||||
retrieved or displayed.
|
||||
</span>
|
||||
}
|
||||
/>
|
||||
</FormSectionContent>
|
||||
</FormSection>
|
||||
</FormPanel>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
</Form>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
import { useParams } from 'common'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
|
||||
import AuthLayout from './AuthLayout'
|
||||
|
||||
interface AuthEmailsLayoutProps {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export const AuthEmailsLayout = ({ children }: AuthEmailsLayoutProps) => {
|
||||
const { ref } = useParams()
|
||||
|
||||
const navItems = [
|
||||
{
|
||||
label: 'Templates',
|
||||
href: `/project/${ref}/auth/templates`,
|
||||
},
|
||||
{
|
||||
label: 'SMTP Settings',
|
||||
href: `/project/${ref}/auth/smtp`,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<AuthLayout>
|
||||
<PageLayout
|
||||
title="Emails"
|
||||
subtitle="Configure what emails your users receive and how they are sent"
|
||||
navigationItems={navItems}
|
||||
>
|
||||
{children}
|
||||
</PageLayout>
|
||||
</AuthLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
}
|
||||
@@ -1,79 +1,22 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
|
||||
import { SmtpForm } from 'components/interfaces/Auth'
|
||||
import AuthLayout from 'components/layouts/AuthLayout/AuthLayout'
|
||||
import { AuthEmailsLayout } from 'components/layouts/AuthLayout/AuthEmailsLayout'
|
||||
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
|
||||
import { useParams } from 'common'
|
||||
import { useCurrentPath } from 'hooks/misc/useCurrentPath'
|
||||
import Link from 'next/link'
|
||||
import { NavMenu, NavMenuItem } from 'ui'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldDescription,
|
||||
ScaffoldDivider,
|
||||
ScaffoldHeader,
|
||||
ScaffoldTitle,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
|
||||
const PageLayout: NextPageWithLayout = () => {
|
||||
const SmtpPage: NextPageWithLayout = () => {
|
||||
const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
|
||||
const isPermissionsLoaded = usePermissionsLoaded()
|
||||
const { ref } = useParams()
|
||||
const currentPath = useCurrentPath()
|
||||
|
||||
const navMenuItems = [
|
||||
{
|
||||
label: 'Templates',
|
||||
href: `/project/${ref}/auth/templates`,
|
||||
},
|
||||
{
|
||||
label: 'SMTP Settings',
|
||||
href: `/project/${ref}/auth/smtp`,
|
||||
},
|
||||
]
|
||||
|
||||
if (isPermissionsLoaded && !canReadAuthSettings) {
|
||||
return <NoPermission isFullPage resourceText="access your project's email settings" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ScaffoldHeader className="pb-0">
|
||||
<ScaffoldContainer id="auth-page-top">
|
||||
<ScaffoldTitle>Emails</ScaffoldTitle>
|
||||
<ScaffoldDescription>
|
||||
Configure what emails your users receive and how they are sent
|
||||
</ScaffoldDescription>
|
||||
<NavMenu
|
||||
className="border-none max-w-full overflow-y-hidden overflow-x-auto mt-4"
|
||||
aria-label="Auth email settings navigation"
|
||||
>
|
||||
{navMenuItems.map((item) => (
|
||||
<NavMenuItem key={item.label} active={currentPath === item.href}>
|
||||
<Link href={item.href}>{item.label}</Link>
|
||||
</NavMenuItem>
|
||||
))}
|
||||
</NavMenu>
|
||||
</ScaffoldContainer>
|
||||
</ScaffoldHeader>
|
||||
|
||||
<ScaffoldDivider />
|
||||
|
||||
<ScaffoldContainer className="my-8 space-y-8">
|
||||
<SmtpForm />
|
||||
</ScaffoldContainer>
|
||||
</div>
|
||||
)
|
||||
return <SmtpForm />
|
||||
}
|
||||
|
||||
PageLayout.getLayout = (page) => {
|
||||
return (
|
||||
<DefaultLayout>
|
||||
<AuthLayout>{page}</AuthLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
}
|
||||
export default PageLayout
|
||||
SmtpPage.getLayout = (page) => <AuthEmailsLayout>{page}</AuthEmailsLayout>
|
||||
|
||||
export default SmtpPage
|
||||
@@ -1,76 +1,26 @@
|
||||
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
||||
import { useParams } from 'common'
|
||||
import { EmailTemplates } from 'components/interfaces/Auth'
|
||||
import AuthLayout from 'components/layouts/AuthLayout/AuthLayout'
|
||||
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
|
||||
import { useCurrentPath } from 'hooks/misc/useCurrentPath'
|
||||
import Link from 'next/link'
|
||||
import { NavMenu, NavMenuItem } from 'ui'
|
||||
import {
|
||||
ScaffoldContainer,
|
||||
ScaffoldDescription,
|
||||
ScaffoldDivider,
|
||||
ScaffoldHeader,
|
||||
ScaffoldTitle,
|
||||
} from 'components/layouts/Scaffold'
|
||||
import { AuthEmailsLayout } from 'components/layouts/AuthLayout/AuthEmailsLayout'
|
||||
import { ScaffoldContainer } from 'components/layouts/Scaffold'
|
||||
import NoPermission from 'components/ui/NoPermission'
|
||||
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
|
||||
import type { NextPageWithLayout } from 'types'
|
||||
import DefaultLayout from 'components/layouts/DefaultLayout'
|
||||
const PageLayout: NextPageWithLayout = () => {
|
||||
|
||||
const TemplatesPage: NextPageWithLayout = () => {
|
||||
const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
|
||||
const isPermissionsLoaded = usePermissionsLoaded()
|
||||
const { ref } = useParams()
|
||||
const currentPath = useCurrentPath()
|
||||
|
||||
const navMenuItems = [
|
||||
{
|
||||
label: 'Templates',
|
||||
href: `/project/${ref}/auth/templates`,
|
||||
},
|
||||
{
|
||||
label: 'SMTP Settings',
|
||||
href: `/project/${ref}/auth/smtp`,
|
||||
},
|
||||
]
|
||||
|
||||
if (isPermissionsLoaded && !canReadAuthSettings) {
|
||||
return <NoPermission isFullPage resourceText="access your project's email settings" />
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ScaffoldHeader className="pb-0">
|
||||
<ScaffoldContainer id="auth-page-top">
|
||||
<ScaffoldTitle>Emails</ScaffoldTitle>
|
||||
<ScaffoldDescription>
|
||||
Configure what emails your users receive and how they are sent
|
||||
</ScaffoldDescription>
|
||||
<NavMenu
|
||||
className="border-none max-w-full overflow-y-hidden overflow-x-auto mt-4"
|
||||
aria-label="Auth email settings navigation"
|
||||
>
|
||||
{navMenuItems.map((item) => (
|
||||
<NavMenuItem key={item.label} active={currentPath === item.href}>
|
||||
<Link href={item.href}>{item.label}</Link>
|
||||
</NavMenuItem>
|
||||
))}
|
||||
</NavMenu>
|
||||
</ScaffoldContainer>
|
||||
</ScaffoldHeader>
|
||||
|
||||
<ScaffoldDivider />
|
||||
|
||||
<ScaffoldContainer className="my-8 space-y-8">
|
||||
<EmailTemplates />
|
||||
</ScaffoldContainer>
|
||||
</div>
|
||||
<ScaffoldContainer>
|
||||
<EmailTemplates />
|
||||
</ScaffoldContainer>
|
||||
)
|
||||
}
|
||||
|
||||
PageLayout.getLayout = (page) => (
|
||||
<DefaultLayout>
|
||||
<AuthLayout>{page}</AuthLayout>
|
||||
</DefaultLayout>
|
||||
)
|
||||
TemplatesPage.getLayout = (page) => <AuthEmailsLayout>{page}</AuthEmailsLayout>
|
||||
|
||||
export default PageLayout
|
||||
export default TemplatesPage
|
||||
Reference in new issue
Block a user