Files
supabase/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx
Danny WhiteandJoshen Lim f45a6085f4 feat(studio): add auth email template reset flow (#45572)
## What kind of change does this PR introduce?

Feature. Resolves AUTH-879.

## What is the current behavior?

Studio does not provide a way to reset an Auth email template back to
the default subject and body once it has been customised.

## What is the new behavior?

Studio shows a `Reset template` action when Platform reports that the
selected Auth email template subject or body has been customised. The
action opens a confirmation dialog, calls the dedicated Platform reset
endpoint, and refreshes the editor with the default subject and body
returned by the API.

The Auth config save/reset mutations now run their user-facing success
handling before refreshing Auth lint data, so the success toast and
local editor cleanup are not delayed by lint refetches.

## Additional context

Depends on supabase/platform#32417.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Reset email templates to defaults via a confirmation dialog; button
appears when custom content is detected and respects update permissions.
* Typed email-template definitions and improved template variable
display and descriptions.

* **Tests**
* Added tests covering template reset visibility, confirmation flow,
state updates, permission handling, and toast notifications.

* **Documentation**
* Example email template placeholders updated for internationalization
and provider snippets.

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/supabase/supabase/pull/45572)

<!-- review_stack_entry_end -->
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-05-18 14:33:26 +10:00

267 lines
9.9 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { ChevronRight } from 'lucide-react'
import Link from 'next/link'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { Button, Card, CardContent, CardFooter, Form, FormControl, FormField, Switch } from 'ui'
import { Admonition } from 'ui-patterns/admonition'
import {
PageSection,
PageSectionContent,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import * as z from 'zod'
import { TEMPLATES_SCHEMAS } from './AuthTemplatesValidation'
import { slugifyTitle } from './EmailTemplates.utils'
import AlertError from '@/components/ui/AlertError'
import { InlineLink } from '@/components/ui/InlineLink'
import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { DOCS_URL } from '@/lib/constants'
const notificationEnabledKeys = TEMPLATES_SCHEMAS.filter(
(t) => t.misc?.emailTemplateType === 'security'
).map((template) => {
return `MAILER_NOTIFICATIONS_${template.id?.replace('_NOTIFICATION', '')}_ENABLED`
})
const NotificationsFormSchema = z.object({
...notificationEnabledKeys.reduce(
(acc, key) => {
acc[key] = z.boolean()
return acc
},
{} as Record<string, z.ZodBoolean>
),
})
export const EmailTemplates = () => {
const { ref: projectRef } = useParams()
const { can: canUpdateConfig } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const {
data: authConfig,
error: authConfigError,
isPending: isLoading,
isError,
isSuccess,
} = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation({
onError: (error) => {
toast.error(`Failed to update settings: ${error?.message}`)
},
onSuccess: () => {
toast.success('Successfully updated settings')
},
})
const builtInSMTP =
isSuccess &&
authConfig &&
(!authConfig.SMTP_HOST || !authConfig.SMTP_USER || !authConfig.SMTP_PASS)
const defaultValues = notificationEnabledKeys.reduce(
(acc, key) => {
acc[key] = authConfig ? Boolean(authConfig[key as keyof typeof authConfig]) : false
return acc
},
{} as Record<string, boolean>
)
const notificationsForm = useForm<z.infer<typeof NotificationsFormSchema>>({
resolver: zodResolver(NotificationsFormSchema),
defaultValues,
})
const onSubmit = (values: any) => {
if (!projectRef) return console.error('Project ref is required')
updateAuthConfig({ projectRef: projectRef, config: { ...values } })
}
useEffect(() => {
if (authConfig) {
notificationsForm.reset(defaultValues)
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [authConfig])
return (
<>
{isError && (
<PageSection>
<PageSectionContent>
<AlertError error={authConfigError} subject="Failed to retrieve auth configuration" />
</PageSectionContent>
</PageSection>
)}
{isLoading && (
<PageSection>
<PageSectionContent>
<GenericSkeletonLoader />
</PageSectionContent>
</PageSection>
)}
{isSuccess && (
<>
<PageSection>
{builtInSMTP && (
<Admonition
type="warning"
title="Set up custom SMTP"
description={
<p>
You’re using the built-in email service. This service has rate limits and is not
meant to be used for production apps.{' '}
<InlineLink
href={`${DOCS_URL}/guides/platform/going-into-prod#auth-rate-limits`}
>
Learn more
</InlineLink>{' '}
</p>
}
layout="horizontal"
className="mb-4"
actions={
<Button asChild type="default">
<Link href={`/project/${projectRef}/auth/smtp`}>Set up SMTP</Link>
</Button>
}
/>
)}
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Authentication</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
{TEMPLATES_SCHEMAS.filter(
(t) => t.misc?.emailTemplateType === 'authentication'
).map((template) => {
const templateSlug = slugifyTitle(template.title)
return (
<CardContent key={`${template.id}`} className="p-0">
<Link
href={`/project/${projectRef}/auth/templates/${templateSlug}`}
className="flex items-center justify-between hover:bg-surface-200 transition-colors py-4 px-6 w-full h-full"
>
<div className="flex flex-col">
<h3 className="text-sm text-foreground">{template.title}</h3>
{template.purpose && (
<p className="text-sm text-foreground-lighter">{template.purpose}</p>
)}
</div>
<div className="flex items-center gap-4">
<ChevronRight size={16} className="text-foreground-muted" />
</div>
</Link>
</CardContent>
)
})}
</Card>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Security</PageSectionTitle>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form {...notificationsForm}>
<form onSubmit={notificationsForm.handleSubmit(onSubmit)} className="space-y-4">
<Card>
{TEMPLATES_SCHEMAS.filter((t) => t.misc?.emailTemplateType === 'security').map(
(template) => {
const templateSlug = slugifyTitle(template.title)
const templateEnabledKey =
`MAILER_NOTIFICATIONS_${template.id?.replace('_NOTIFICATION', '')}_ENABLED` as keyof typeof authConfig
return (
<CardContent
key={`${template.id}`}
className="p-0 flex items-center justify-between hover:bg-surface-200 transition-colors w-full h-full"
>
<Link
href={`/project/${projectRef}/auth/templates/${templateSlug}`}
className="flex flex-col flex-1 py-4 px-6"
>
<h3 className="text-sm text-foreground">{template.title}</h3>
{template.purpose && (
<p className="text-sm text-foreground-lighter">
{template.purpose}
</p>
)}
</Link>
<div className="flex items-center gap-4 h-full pl-2 relative">
<FormField
control={notificationsForm.control}
name={templateEnabledKey}
render={({ field }) => (
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl>
)}
/>
<Link
href={`/project/${projectRef}/auth/templates/${templateSlug}`}
className="py-6 pr-6"
>
<ChevronRight size={16} className="text-foreground-muted" />
</Link>
</div>
</CardContent>
)
}
)}
<CardFooter className="justify-end space-x-2">
{notificationsForm.formState.isDirty && (
<Button type="default" onClick={() => notificationsForm.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={
!canUpdateConfig ||
isUpdatingConfig ||
!notificationsForm.formState.isDirty
}
loading={isUpdatingConfig}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form>
</PageSectionContent>
</PageSection>
</>
)}
</>
)
}