EmailTemplates.tsx266 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import { useParams } from 'common'
4import { ChevronRight } from 'lucide-react'
5import Link from 'next/link'
6import { useEffect } from 'react'
7import { useForm } from 'react-hook-form'
8import { toast } from 'sonner'
9import { Button, Card, CardContent, CardFooter, Form, FormControl, FormField, Switch } from 'ui'
10import { Admonition } from 'ui-patterns/admonition'
11import {
12 PageSection,
13 PageSectionContent,
14 PageSectionMeta,
15 PageSectionSummary,
16 PageSectionTitle,
17} from 'ui-patterns/PageSection'
18import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
19import * as z from 'zod'
20
21import { TEMPLATES_SCHEMAS } from './AuthTemplatesValidation'
22import { slugifyTitle } from './EmailTemplates.utils'
23import AlertError from '@/components/ui/AlertError'
24import { InlineLink } from '@/components/ui/InlineLink'
25import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
26import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
27import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
28import { DOCS_URL } from '@/lib/constants'
29
30const notificationEnabledKeys = TEMPLATES_SCHEMAS.filter(
31 (t) => t.misc?.emailTemplateType === 'security'
32).map((template) => {
33 return `MAILER_NOTIFICATIONS_${template.id?.replace('_NOTIFICATION', '')}_ENABLED`
34})
35
36const NotificationsFormSchema = z.object({
37 ...notificationEnabledKeys.reduce(
38 (acc, key) => {
39 acc[key] = z.boolean()
40 return acc
41 },
42 {} as Record<string, z.ZodBoolean>
43 ),
44})
45
46export const EmailTemplates = () => {
47 const { ref: projectRef } = useParams()
48 const { can: canUpdateConfig } = useAsyncCheckPermissions(
49 PermissionAction.UPDATE,
50 'custom_config_gotrue'
51 )
52
53 const {
54 data: authConfig,
55 error: authConfigError,
56 isPending: isLoading,
57 isError,
58 isSuccess,
59 } = useAuthConfigQuery({ projectRef })
60
61 const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation({
62 onError: (error) => {
63 toast.error(`Failed to update settings: ${error?.message}`)
64 },
65 onSuccess: () => {
66 toast.success('Successfully updated settings')
67 },
68 })
69
70 const builtInSMTP =
71 isSuccess &&
72 authConfig &&
73 (!authConfig.SMTP_HOST || !authConfig.SMTP_USER || !authConfig.SMTP_PASS)
74
75 const defaultValues = notificationEnabledKeys.reduce(
76 (acc, key) => {
77 acc[key] = authConfig ? Boolean(authConfig[key as keyof typeof authConfig]) : false
78 return acc
79 },
80 {} as Record<string, boolean>
81 )
82
83 const notificationsForm = useForm<z.infer<typeof NotificationsFormSchema>>({
84 resolver: zodResolver(NotificationsFormSchema as any),
85 defaultValues,
86 })
87
88 const onSubmit = (values: any) => {
89 if (!projectRef) return console.error('Project ref is required')
90 updateAuthConfig({ projectRef: projectRef, config: { ...values } })
91 }
92
93 useEffect(() => {
94 if (authConfig) {
95 notificationsForm.reset(defaultValues)
96 }
97 // eslint-disable-next-line react-hooks/exhaustive-deps
98 }, [authConfig])
99
100 return (
101 <>
102 {isError && (
103 <PageSection>
104 <PageSectionContent>
105 <AlertError error={authConfigError} subject="Failed to retrieve auth configuration" />
106 </PageSectionContent>
107 </PageSection>
108 )}
109 {isLoading && (
110 <PageSection>
111 <PageSectionContent>
112 <GenericSkeletonLoader />
113 </PageSectionContent>
114 </PageSection>
115 )}
116 {isSuccess && (
117 <>
118 <PageSection>
119 {builtInSMTP && (
120 <Admonition
121 type="warning"
122 title="Set up custom SMTP"
123 description={
124 <p>
125 You’re using the built-in email service. This service has rate limits and is not
126 meant to be used for production apps.{' '}
127 <InlineLink
128 href={`${DOCS_URL}/guides/platform/going-into-prod#auth-rate-limits`}
129 >
130 Learn more
131 </InlineLink>{' '}
132 </p>
133 }
134 layout="horizontal"
135 className="mb-4"
136 actions={
137 <Button asChild type="default">
138 <Link href={`/project/${projectRef}/auth/smtp`}>Set up SMTP</Link>
139 </Button>
140 }
141 />
142 )}
143 <PageSectionMeta>
144 <PageSectionSummary>
145 <PageSectionTitle>Authentication</PageSectionTitle>
146 </PageSectionSummary>
147 </PageSectionMeta>
148 <PageSectionContent>
149 <Card>
150 {TEMPLATES_SCHEMAS.filter(
151 (t) => t.misc?.emailTemplateType === 'authentication'
152 ).map((template) => {
153 const templateSlug = slugifyTitle(template.title)
154
155 return (
156 <CardContent key={`${template.id}`} className="p-0">
157 <Link
158 href={`/project/${projectRef}/auth/templates/${templateSlug}`}
159 className="flex items-center justify-between hover:bg-surface-200 transition-colors py-4 px-6 w-full h-full"
160 >
161 <div className="flex flex-col">
162 <h3 className="text-sm text-foreground">{template.title}</h3>
163 {template.purpose && (
164 <p className="text-sm text-foreground-lighter">{template.purpose}</p>
165 )}
166 </div>
167
168 <div className="flex items-center gap-4">
169 <ChevronRight size={16} className="text-foreground-muted" />
170 </div>
171 </Link>
172 </CardContent>
173 )
174 })}
175 </Card>
176 </PageSectionContent>
177 </PageSection>
178
179 <PageSection>
180 <PageSectionMeta>
181 <PageSectionSummary>
182 <PageSectionTitle>Security</PageSectionTitle>
183 </PageSectionSummary>
184 </PageSectionMeta>
185 <PageSectionContent>
186 <Form {...notificationsForm}>
187 <form onSubmit={notificationsForm.handleSubmit(onSubmit)} className="space-y-4">
188 <Card>
189 {TEMPLATES_SCHEMAS.filter((t) => t.misc?.emailTemplateType === 'security').map(
190 (template) => {
191 const templateSlug = slugifyTitle(template.title)
192 const templateEnabledKey =
193 `MAILER_NOTIFICATIONS_${template.id?.replace('_NOTIFICATION', '')}_ENABLED` as keyof typeof authConfig
194
195 return (
196 <CardContent
197 key={`${template.id}`}
198 className="p-0 flex items-center justify-between hover:bg-surface-200 transition-colors w-full h-full"
199 >
200 <Link
201 href={`/project/${projectRef}/auth/templates/${templateSlug}`}
202 className="flex flex-col flex-1 py-4 px-6"
203 >
204 <h3 className="text-sm text-foreground">{template.title}</h3>
205 {template.purpose && (
206 <p className="text-sm text-foreground-lighter">
207 {template.purpose}
208 </p>
209 )}
210 </Link>
211
212 <div className="flex items-center gap-4 h-full pl-2 relative">
213 <FormField
214 control={notificationsForm.control}
215 name={templateEnabledKey}
216 render={({ field }) => (
217 <FormControl>
218 <Switch
219 checked={field.value}
220 onCheckedChange={field.onChange}
221 disabled={!canUpdateConfig}
222 />
223 </FormControl>
224 )}
225 />
226
227 <Link
228 href={`/project/${projectRef}/auth/templates/${templateSlug}`}
229 className="py-6 pr-6"
230 >
231 <ChevronRight size={16} className="text-foreground-muted" />
232 </Link>
233 </div>
234 </CardContent>
235 )
236 }
237 )}
238 <CardFooter className="justify-end space-x-2">
239 {notificationsForm.formState.isDirty && (
240 <Button type="default" onClick={() => notificationsForm.reset()}>
241 Cancel
242 </Button>
243 )}
244 <Button
245 type="primary"
246 htmlType="submit"
247 disabled={
248 !canUpdateConfig ||
249 isUpdatingConfig ||
250 !notificationsForm.formState.isDirty
251 }
252 loading={isUpdatingConfig}
253 >
254 Save changes
255 </Button>
256 </CardFooter>
257 </Card>
258 </form>
259 </Form>
260 </PageSectionContent>
261 </PageSection>
262 </>
263 )}
264 </>
265 )
266}