diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx index e8f79f55cab..391b58d28e0 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/EmailTemplates.tsx @@ -26,6 +26,7 @@ import { isCustomEmailTemplateRestrictionStatusKnown, slugifyTitle, } from './EmailTemplates.utils' +import { SendEmailHookActiveAdmonition } from './SendEmailHookActiveAdmonition' import AlertError from '@/components/ui/AlertError' import { InlineLink } from '@/components/ui/InlineLink' import { useAuthConfigQuery } from '@/data/auth/auth-config-query' @@ -79,6 +80,9 @@ export const EmailTemplates = () => { }) const usingBuiltInEmailSender = !hasCustomEmailSender(authConfig) + const hasSendEmailHook = !!( + authConfig?.HOOK_SEND_EMAIL_ENABLED && authConfig?.HOOK_SEND_EMAIL_URI + ) const isTemplateRestrictionStatusKnown = isCustomEmailTemplateRestrictionStatusKnown({ authConfig, organization: selectedOrganization, @@ -135,10 +139,24 @@ export const EmailTemplates = () => { )} {isSuccess && ( <> + {isTemplateEditBlocked && !hasSendEmailHook && ( + + + + + + )} + + {hasSendEmailHook && ( + + + + + + )} + - {isTemplateEditBlocked ? ( - - ) : usingBuiltInEmailSender ? ( + {usingBuiltInEmailSender && !isTemplateEditBlocked && ( { } /> - ) : null} + )} Authentication diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/SendEmailHookActiveAdmonition.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/SendEmailHookActiveAdmonition.tsx new file mode 100644 index 00000000000..85aeb69b711 --- /dev/null +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/SendEmailHookActiveAdmonition.tsx @@ -0,0 +1,38 @@ +import { useParams } from 'common' +import Link from 'next/link' +import { Button } from 'ui' +import { Admonition } from 'ui-patterns/admonition' + +import { InlineLink } from '@/components/ui/InlineLink' +import { DOCS_URL } from '@/lib/constants' + +/** + * Shown on template list and editor pages when a send-email hook is active. + * The hook bypasses template rendering entirely — Auth sends event metadata to + * the hook, not rendered HTML — so templates are irrelevant while it's enabled. + */ +export const SendEmailHookActiveAdmonition = () => { + const { ref: projectRef } = useParams() + + return ( + + A Send Email hook is active. Email data is sent to your hook instead of using these + templates.{' '} + + Learn more + + + } + actions={ + + } + /> + ) +} diff --git a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.test.tsx b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.test.tsx index dac6766a961..933268de677 100644 --- a/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.test.tsx +++ b/apps/studio/components/interfaces/Auth/EmailTemplates/TemplateEditor.test.tsx @@ -124,7 +124,7 @@ const renderTemplateEditor = ({ }) useAsyncCheckPermissionsMock.mockReturnValue({ can: canUpdateConfig }) useAuthConfigUpdateMutationMock.mockReturnValue({ mutate: updateAuthConfigMock }) - useAuthTemplateResetMutationMock.mockReturnValue({ mutate: resetTemplateMock }) + useAuthTemplateResetMutationMock.mockReturnValue({ mutate: resetTemplateMock, isPending: false }) return render() } diff --git a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx index c1dbe06022e..dd552d05533 100644 --- a/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx +++ b/apps/studio/components/interfaces/Auth/Hooks/CreateHookSheet.tsx @@ -27,6 +27,7 @@ import { SheetTitle, Switch, } from 'ui' +import { Admonition } from 'ui-patterns' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { InfoTooltip } from 'ui-patterns/info-tooltip' import * as z from 'zod' @@ -38,6 +39,7 @@ import { DiscardChangesConfirmationDialog } from '@/components/ui-patterns/Dialo import { CodeEditor } from '@/components/ui/CodeEditor/CodeEditor' import { DocsButton } from '@/components/ui/DocsButton' import FunctionSelector from '@/components/ui/FunctionSelector' +import { InlineLink } from '@/components/ui/InlineLink' import { SchemaSelector } from '@/components/ui/SchemaSelector' import { AuthConfigResponse } from '@/data/auth/auth-config-query' import { useAuthHooksUpdateMutation } from '@/data/auth/auth-hooks-update-mutation' @@ -165,7 +167,7 @@ export const CreateHookSheet = ({ }) const isDirty = form.formState.isDirty - const values = form.watch() + const { postgresValues, hookType, selectedType, enabled } = form.watch() const { confirmOnClose, handleOpenChange, @@ -181,7 +183,7 @@ export const CreateHookSheet = ({ if ( hook.method.schema !== '' && hook.method.functionName !== '' && - hook.method.functionName !== values.postgresValues.functionName + hook.method.functionName !== postgresValues.functionName ) { permissionChanges = getRevokePermissionStatements( hook.method.schema, @@ -190,9 +192,9 @@ export const CreateHookSheet = ({ } } - if (values.postgresValues.functionName !== '') { - const schema = values.postgresValues.schema - const functionName = values.postgresValues.functionName + if (postgresValues.functionName !== '') { + const schema = postgresValues.schema + const functionName = postgresValues.functionName permissionChanges = [ ...permissionChanges, safeSql`-- Grant access to function to supabase_auth_admin @@ -204,11 +206,11 @@ revoke execute on function ${ident(schema)}.${ident(functionName)} from authenti ] } return permissionChanges - }, [hook, values.postgresValues.schema, values.postgresValues.functionName]) + }, [hook, postgresValues.schema, postgresValues.functionName]) const { mutate: updateAuthHooks, isPending: isUpdatingAuthHooks } = useAuthHooksUpdateMutation({ onSuccess: () => { - toast.success(`Successfully created ${values.hookType}.`) + toast.success(`Successfully ${isCreating ? 'created' : 'updated'} ${hookType}.`) if (statements.length > 0) { executeSql({ projectRef, @@ -306,7 +308,7 @@ revoke execute on function ${ident(schema)}.${ident(functionName)} from authenti - +
( )} /> + + {hook.id === 'send-email' && enabled && ( +
+ +

+ While enabled, email data is sent to this hook instead. Your configured{' '} + + email templates + {' '} + will not be used.{' '} + + Learn more + + . +

+
+
+ )} + + ( - + )} /> - {values.selectedType === 'postgres' ? ( + {selectedType === 'postgres' ? ( <> -
- ( - - - field.onChange(name)} - disabled={field.disabled} - /> - - - )} - /> - ( - - - { - if (supportedReturnTypes.includes(func.return_type)) { - const { value } = convertArgumentTypes({ - type: func.type, - value: func.argument_types, - }) - if (value.length !== 1) return false - return value[0].type === 'json' || value[0].type === 'jsonb' - } - return false - }} - noResultsLabel={ - - No function with a single JSON/B argument -
- and JSON/B - {definition.enabledKey === 'HOOK_SEND_EMAIL_ENABLED' - ? ' or void' - : ''}{' '} - return type found in this schema. -
+ ( + + + field.onChange(name)} + disabled={field.disabled} + /> + + + )} + /> + ( + + + { + if (supportedReturnTypes.includes(func.return_type)) { + const { value } = convertArgumentTypes({ + type: func.type, + value: func.argument_types, + }) + if (value.length !== 1) return false + return value[0].type === 'json' || value[0].type === 'jsonb' } - /> - - - )} - /> -
+ return false + }} + noResultsLabel={ + + No function with a single JSON/B argument +
+ and JSON/B + {definition.enabledKey === 'HOOK_SEND_EMAIL_ENABLED' + ? ' or void' + : ''}{' '} + return type found in this schema. +
+ } + /> +
+
+ )} + /> {statements.length > 0 && ( -
-

- The following statements will be executed on the selected function: -

- -
+ <> + +
+

+ The following statements will be executed on the selected function: +

+ +
+ )} ) : ( @@ -461,11 +488,12 @@ revoke execute on function ${ident(schema)}.${ident(functionName)} from authenti name="httpsValues.url" render={({ field }) => ( - + )} @@ -476,11 +504,12 @@ revoke execute on function ${ident(schema)}.${ident(functionName)} from authenti name="httpsValues.secret" render={({ field }) => (

- Should be a base64 encoded hook secret with a prefix{' '} + Should be a base64 encoded secret with a prefix{' '} v1,whsec_.

@@ -496,8 +525,7 @@ revoke execute on function ${ident(schema)}.${ident(functionName)} from authenti