Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions apps/docs/content/guides/auth/auth-email-passwordless.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -145,10 +145,10 @@ That's it for the implicit flow.
If you're using PKCE flow, edit the Magic Link [email template](/docs/guides/auth/auth-email-templates) to send a token hash:

```html
<h2>Magic Link</h2>
<h2>Sign in to your account</h2>

<p>Follow this link to login:</p>
<p><a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email">Log In</a></p>
<p>Use this link to sign in to your account:</p>
<p><a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email">Sign in</a></p>
```

At the `/auth/confirm` endpoint, exchange the hash for the session:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ npm run dev

And then open the browser to [localhost:3000/login](http://localhost:3000/login) and you should see the completed app.

When you enter your email and password, you will receive an email with the title **Confirm Your Signup**. Congrats 🎉!!!
When you enter your email and password, you will receive an email with the title **Confirm your email**. Congrats 🎉!!!

At this stage you have a fully functional application!

Expand Down
2 changes: 1 addition & 1 deletion apps/docs/content/guides/platform/backups.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: 'Database Backups'
description: 'Learn about backups for your Supabase project.'
---

We automatically back up all Free, Pro, Team, and Enterprise Plan projects on a daily basis. You can find backups in the [**Database** > **Backups**](/dashboard/project/_/database/backups/scheduled) section of the Dashboard.
We automatically back up all Pro, Team, and Enterprise Plan projects on a daily basis. You can find backups in the [**Database** > **Backups**](/dashboard/project/_/database/backups/scheduled) section of the Dashboard.

Pro Plan projects can access the last 7 days of daily backups. Team Plan projects can access the last 14 days of daily backups, while Enterprise Plan projects can access up to 30 days of daily backups. If you need more frequent backups, consider enabling [Point-in-Time Recovery](#point-in-time-recovery). We recommend that free tier plan projects regularly export their data using the [Supabase CLI `db dump` command](/docs/reference/cli/supabase-db-dump) and maintain off-site backups.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ services:
environment:
GOTRUE_MAILER_NOTIFICATIONS_PASSWORD_CHANGED_ENABLED: 'true' # 👈 enabling the notification is required
GOTRUE_MAILER_TEMPLATES_PASSWORD_CHANGED_NOTIFICATION: 'http://templates-server/password_changed_notification.html'
GOTRUE_MAILER_SUBJECTS_PASSWORD_CHANGED_NOTIFICATION: 'Your password has been changed'
GOTRUE_MAILER_SUBJECTS_PASSWORD_CHANGED_NOTIFICATION: 'Your password was changed'

templates-server:
image: caddy:2-alpine
Expand Down
1 change: 1 addition & 0 deletions apps/studio/components/grid/components/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ const RowHeader = ({ tableQueriesEnabled = true }: RowHeaderProps) => {
table: snap.table,
projectRef: project.ref,
connectionString: project.connectionString ?? null,
roleImpersonationState: roleImpersonationState as RoleImpersonationState,
})
if (hydrated.status !== 'ok') {
throw new Error('Failed to fetch full values for truncated cells')
Expand Down
4 changes: 4 additions & 0 deletions apps/studio/components/grid/components/header/Header.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Papa from 'papaparse'
import type { SupaTable } from '@/components/grid/types'
import { isValueTruncated } from '@/components/interfaces/TableGridEditor/SidePanelEditor/RowEditor/RowEditor.utils'
import { getCellValue } from '@/data/table-rows/get-cell-value-mutation'
import type { RoleImpersonationState } from '@/lib/role-impersonation'

export const formatRowsForCSV = ({ rows, columns }: { rows: any[]; columns: string[] }) => {
const formattedRows = rows.map((row) => {
Expand Down Expand Up @@ -34,11 +35,13 @@ export const hydrateTruncatedRows = async ({
table,
projectRef,
connectionString,
roleImpersonationState,
}: {
rows: Record<string, unknown>[]
table: SupaTable
projectRef: string
connectionString: string | null
roleImpersonationState?: RoleImpersonationState
}): Promise<HydrateTruncatedRowsResult> => {
const jobs: { rowIdx: number; column: string }[] = []
rows.forEach((row, rowIdx) => {
Expand Down Expand Up @@ -73,6 +76,7 @@ export const hydrateTruncatedRows = async ({
table: { schema: table.schema ?? 'public', name: table.name },
column,
pkMatch,
roleImpersonationState,
})
})
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { useParams } from 'common'
import { ChevronDown } from 'lucide-react'
import Link from 'next/link'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from 'ui'
import { Admonition } from 'ui-patterns/admonition'

import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'

export const CustomEmailTemplateRestrictionAdmonition = () => {
const { ref: projectRef } = useParams()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const organizationSlug = selectedOrganization?.slug ?? '_'

return (
<Admonition
type="default"
layout="responsive"
title="Set up custom SMTP to edit templates"
description="Emails will be sent using the default templates. Set up custom SMTP to edit their subject and body."
actions={
<div className="flex w-full @lg:w-auto">
<Button
asChild
type="default"
className="flex-1 rounded-r-none px-3 @lg:flex-none hover:z-10"
>
<Link href={`/project/${projectRef}/auth/smtp`}>Set up SMTP</Link>
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="default"
aria-label="More email template editing options"
className="shrink-0 rounded-l-none px-[4px] py-[5px] -ml-px"
icon={<ChevronDown />}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuItem asChild>
<Link
href={`/org/${organizationSlug}/billing?panel=subscriptionPlan&source=authEmailTemplates`}
>
<div className="flex flex-col gap-y-0.5">
<p className="block text-foreground">Upgrade to Pro</p>
<p className="block text-foreground-lighter text-balance">
Customize templates while using Supabase’s email service
</p>
</div>
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href={`/project/${projectRef}/auth/hooks?hook=send-email`}>
<div className="flex flex-col gap-y-0.5">
<p className="block text-foreground">Configure Send Email hook</p>
<p className="block text-foreground-lighter text-balance">
Send auth emails through your own workflow
</p>
</div>
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
}
/>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { AUTH_TEMPLATE_TYPES, type AuthTemplateResetType } from './EmailTemplates.types'
import { getAuthTemplateType } from './EmailTemplates.utils'

export const AUTH_TEMPLATE_RESET_TYPES: AuthTemplateResetType[] =
AUTH_TEMPLATE_TYPES.map(getAuthTemplateType)
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,20 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import * as z from 'zod'

import { TEMPLATES_SCHEMAS } from './AuthTemplatesValidation'
import { slugifyTitle } from './EmailTemplates.utils'
import { CustomEmailTemplateRestrictionAdmonition } from './CustomEmailTemplateRestrictionAdmonition'
import {
hasCustomEmailSender,
isCustomEmailTemplateEditingRestricted,
isCustomEmailTemplateRestrictionStatusKnown,
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 { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DOCS_URL } from '@/lib/constants'

const notificationEnabledKeys = TEMPLATES_SCHEMAS.filter(
Expand All @@ -45,6 +53,9 @@ const NotificationsFormSchema = z.object({

export const EmailTemplates = () => {
const { ref: projectRef } = useParams()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const { data: selectedProject } = useSelectedProjectQuery()

const { can: canUpdateConfig } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
Expand All @@ -67,10 +78,19 @@ export const EmailTemplates = () => {
},
})

const builtInSMTP =
isSuccess &&
authConfig &&
(!authConfig.SMTP_HOST || !authConfig.SMTP_USER || !authConfig.SMTP_PASS)
const usingBuiltInEmailSender = !hasCustomEmailSender(authConfig)
const isTemplateRestrictionStatusKnown = isCustomEmailTemplateRestrictionStatusKnown({
authConfig,
organization: selectedOrganization,
projectInsertedAt: selectedProject?.inserted_at,
})
const isTemplateEditBlocked =
isTemplateRestrictionStatusKnown &&
isCustomEmailTemplateEditingRestricted({
authConfig,
organization: selectedOrganization,
projectInsertedAt: selectedProject?.inserted_at,
})

const defaultValues = notificationEnabledKeys.reduce(
(acc, key) => {
Expand All @@ -85,7 +105,7 @@ export const EmailTemplates = () => {
defaultValues,
})

const onSubmit = (values: any) => {
const onSubmit = (values: z.infer<typeof NotificationsFormSchema>) => {
if (!projectRef) return console.error('Project ref is required')
updateAuthConfig({ projectRef: projectRef, config: { ...values } })
}
Expand Down Expand Up @@ -116,7 +136,9 @@ export const EmailTemplates = () => {
{isSuccess && (
<>
<PageSection>
{builtInSMTP && (
{isTemplateEditBlocked ? (
<CustomEmailTemplateRestrictionAdmonition />
) : usingBuiltInEmailSender ? (
<Admonition
type="warning"
title="Set up custom SMTP"
Expand All @@ -132,14 +154,13 @@ export const EmailTemplates = () => {
</p>
}
layout="horizontal"
className="mb-4"
actions={
<Button asChild type="default">
<Link href={`/project/${projectRef}/auth/smtp`}>Set up SMTP</Link>
</Button>
}
/>
)}
) : null}
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Authentication</PageSectionTitle>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type KebabCase<S extends string> = S extends `${infer A}_${infer B}`
? `${Lowercase<A>}-${KebabCase<B>}`
: Lowercase<S>

const AUTH_TEMPLATE_TYPES = [
export const AUTH_TEMPLATE_TYPES = [
'CONFIRMATION',
'EMAIL_CHANGE',
'INVITE',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { describe, expect, it } from 'vitest'

import {
FREE_TIER_TEMPLATE_BLOCK_CUTOFF_DATE,
hasCustomEmailSender,
isCustomEmailTemplateEditingRestricted,
isCustomEmailTemplateRestrictionStatusKnown,
} from './EmailTemplates.utils'
import type { Organization } from '@/types'

const freeOrganization = { plan: { id: 'free', name: 'Free' } } as unknown as Organization
const proOrganization = { plan: { id: 'pro', name: 'Pro' } } as unknown as Organization

// Dates relative to the cutoff
const PRE_CUTOFF = '2025-01-01T00:00:00Z'
const POST_CUTOFF = '2026-12-01T00:00:00Z'

describe('EmailTemplates.utils', () => {
it('waits for auth config, organization, and project before resolving restriction status', () => {
expect(
isCustomEmailTemplateRestrictionStatusKnown({
authConfig: {},
organization: freeOrganization,
projectInsertedAt: POST_CUTOFF,
})
).toBe(true)

expect(
isCustomEmailTemplateRestrictionStatusKnown({
authConfig: undefined,
organization: freeOrganization,
projectInsertedAt: POST_CUTOFF,
})
).toBe(false)

expect(
isCustomEmailTemplateRestrictionStatusKnown({
authConfig: {},
organization: undefined,
projectInsertedAt: POST_CUTOFF,
})
).toBe(false)

expect(
isCustomEmailTemplateRestrictionStatusKnown({
authConfig: {},
organization: freeOrganization,
projectInsertedAt: undefined,
})
).toBe(false)
})

it('restricts post-cutoff free projects that use the built-in email sender', () => {
expect(
isCustomEmailTemplateEditingRestricted({
authConfig: {},
organization: freeOrganization,
projectInsertedAt: POST_CUTOFF,
})
).toBe(true)
})

it('does not restrict pre-cutoff free projects (grandfathered)', () => {
expect(
isCustomEmailTemplateEditingRestricted({
authConfig: {},
organization: freeOrganization,
projectInsertedAt: PRE_CUTOFF,
})
).toBe(false)
})

it('uses the correct cutoff date', () => {
expect(FREE_TIER_TEMPLATE_BLOCK_CUTOFF_DATE).toBe('2026-06-03T00:00:00Z')
})

it('allows paid projects that use the built-in email sender', () => {
expect(
isCustomEmailTemplateEditingRestricted({
authConfig: {},
organization: proOrganization,
projectInsertedAt: POST_CUTOFF,
})
).toBe(false)
})

it('allows projects with custom SMTP configured', () => {
const authConfig = {
SMTP_ADMIN_EMAIL: 'support@example.com',
SMTP_SENDER_NAME: 'Example',
SMTP_USER: 'smtp-user',
SMTP_HOST: 'smtp.example.com',
SMTP_PASS: '******',
SMTP_PORT: '587',
SMTP_MAX_FREQUENCY: 60,
}

expect(hasCustomEmailSender(authConfig)).toBe(true)
expect(
isCustomEmailTemplateEditingRestricted({
authConfig,
organization: freeOrganization,
projectInsertedAt: POST_CUTOFF,
})
).toBe(false)
})

it('restricts projects when custom SMTP is incomplete', () => {
expect(
isCustomEmailTemplateEditingRestricted({
authConfig: {
SMTP_ADMIN_EMAIL: 'support@example.com',
SMTP_SENDER_NAME: 'Example',
SMTP_USER: 'smtp-user',
SMTP_HOST: 'smtp.example.com',
SMTP_PORT: '587',
SMTP_MAX_FREQUENCY: 60,
},
organization: freeOrganization,
projectInsertedAt: POST_CUTOFF,
})
).toBe(true)
})

it('allows projects with a configured send-email hook', () => {
expect(
isCustomEmailTemplateEditingRestricted({
authConfig: {
HOOK_SEND_EMAIL_ENABLED: true,
HOOK_SEND_EMAIL_URI: 'https://example.com/auth/send-email',
},
organization: freeOrganization,
projectInsertedAt: POST_CUTOFF,
})
).toBe(false)
})
})
Loading
Loading