| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266 |
- 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 as any),
- 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>
- </>
- )}
- </>
- )
- }
|