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
36 changes: 25 additions & 11 deletions apps/docs/content/guides/auth/auth-hooks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -313,23 +313,37 @@ Hooks return status codes based on the nature of the response. These status code
| HTTP Status Code | Description | Example Usage |
| ---------------- | ------------------------------------------------------------- | ---------------------------------------------- |
| 200, 202, 204 | Valid response, proceed | Successful processing of the request |
| 429, 503 | Retry-able errors with Retry-after header supported | Temporary server overload or maintenance |
| 403, 400 | Treated as Internal Server Errors and return a 500 Error Code | Malformed requests or insufficient permissions |
| 429, 503 | Retry-able errors | Temporary server overload or maintenance |

Errors are responses which contain status codes 400 and above. On a retry-able error, such as an error with a `429` or `503` status code, HTTP Hooks will attempt up to three retries with a back-off of two seconds.
<Admonition type="note">

`204` Status is not supported by the following hooks which require a response body:

- [Custom Access Token](/docs/guides/auth/auth-hooks/custom-access-token-hook)
- [MFA Verification Attempt](/docs/guides/auth/auth-hooks/mfa-verification-hook)
- [Password Verification Attempt](/docs/guides/auth/auth-hooks/password-verification-hook)

</Admonition>

Errors are responses which contain status codes 400 and above. On a retry-able error, such as an error with a `429` or `503` status code, HTTP Hooks will attempt up to three retries with a back-off of two seconds. We have a time budget of 5s for the entire webhook invocation, including retry requests.

Here's a sample HTTP retry schedule:

| Time Since Start (HH:MM:SS) | Event | Notes |
| --------------------------- | ----------------------- | ------------------------------------------------- |
| 00:00:00 | Initial Attempt | Initial invocation begins. |
| 00:00:05 | Initial Attempt Timeout | Initial invocation must complete. |
| 00:00:07 | Retry Start #1 | After 2 sec delay, first retry begins. |
| 00:00:12 | Retry Timeout #1 | First retry timeout. |
| 00:00:14 | Retry Start #2 | After 2 sec delay, second retry begins. |
| 00:00:19 | Retry Timeout #2 | Second retry timeout. Returns an error on failure |
| Time Since Start (HH:MM:SS) | Event | Notes |
| --------------------------- | --------------------- | -------------------------------------------------------------------------------- |
| 00:00:00 | Initial Attempt | Initial invocation begins. |
| 00:00:02 | Initial Attempt Fails | Initial invocation returns `429` or `503` with non-empty `retry-after` header. |
| 00:00:04 | Retry Start #1 | After 2 sec delay, first retry begins. |
| 00:00:05 | Retry Timeout #1 | First retry times out, exceeded 5 second budget and invocation returns an error. |

Return a retry-able error by attaching a appropriate status code (`429`, `503` ) and a non-empty `retry-after` header
Return a retry-able error by attaching a appropriate status code (`429`, `503`) and a non-empty `retry-after` header

<Admonition type="note">

`Retry-After` Supabase Auth does not fully support the `Retry-After` header as described in RFC7231, we only check if it is a non-empty value such as `true` or `10`. Setting this to your preferred value is fine as a future update may address this.

</Admonition>

```jsx
return new Response(
Expand Down
48 changes: 45 additions & 3 deletions apps/docs/content/guides/auth/auth-hooks/send-email-hook.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,33 @@ Email sending depends on two settings: Email Provider and Auth Hook status.
| Disabled | Enabled | Email Signups Disabled |
| Disabled | Disabled | Email Signups Disabled |

## Email change behavior and token hash mapping

When `email_action_type` is `email_change`, the hook payload can include one or two OTPs and their hashes. This depends on your [Secure Email Change](/dashboard/project/_/auth/providers?provider=Email) setting.

- Secure Email Change enabled: two OTPs are generated, one for the current email (`user.email`) and one for the new email (`user.email_new`). You must send two emails.
- Secure Email Change disabled: only one OTP is generated for the new email. You send a single email.

<Admonition type="note">

Important quirk (backward compatibility):

- `email_data.token_hash_new` = Hash(`user.email`, `email_data.token`)
- `email_data.token_hash` = Hash(`user.email_new`, `email_data.token_new`)

This naming is historical and kept for backward compatibility. Do not assume that the `_new` suffix refers to the new email.

</Admonition>

### What to send

If both `token_hash` and `token_hash_new` are present, send two messages:

- To the current email (`user.email`): use `token` with `token_hash_new`.
- To the new email (`user.email_new`): use `token_new` with `token_hash`.

If only one token/hash pair is present, send a single email. In non-secure mode, this is typically the new email OTP. Use `token` with `token_hash` or `token_new` with `token_hash`, depending on which fields are present in the payload.

**Inputs**

| Field | Type | Description |
Expand Down Expand Up @@ -282,7 +309,15 @@ Email sending depends on two settings: Email Provider and Auth Hook status.
},
"email_action_type": {
"type": "string",
"enum": ["signup", "invite", "magiclink", "recovery", "email_change", "email"]
"enum": [
"signup",
"invite",
"magiclink",
"recovery",
"email_change",
"email",
"reauthentication"
]
},
"site_url": {
"type": "string",
Expand Down Expand Up @@ -578,6 +613,7 @@ import { readAll } from 'https://deno.land/std/io/read_all.ts'
const postmarkEndpoint = 'https://api.postmarkapp.com/email'
// Replace this with your email
const FROM_EMAIL = '[email protected]'
const PROJECT_REF = '<your-project-ref>'

// Email Subjects
const subjects = {
Expand Down Expand Up @@ -642,8 +678,14 @@ const templates = {
}

function generateConfirmationURL(email_data) {
// TODO: replace the ref with your project ref
return `https://<ref>.supabase.co/auth/v1/verify?token=${email_data.token_hash}&type=${email_data.email_action_type}&redirect_to=${email_data.redirect_to}`
const baseUrl = `https://${PROJECT_REF}.supabase.co/auth/v1/verify`
const params = new URLSearchParams({
token: email_data.token_hash,
type: email_data.email_action_type,
redirect_to: email_data.redirect_to,
})

return `${baseUrl}?${params.toString()}`
}

Deno.serve(async (req) => {
Expand Down
2 changes: 2 additions & 0 deletions apps/docs/public/humans.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Danny White
Darren Cunningham
Dave Wilson
David A. Ventimiglia
David Weitzman
Deepthi Sigireddi
Deji I
Div Arora
Expand Down Expand Up @@ -68,6 +69,7 @@ Jordi Enric
José Luis Ledesma
Joshen Lim
Julien Goux
Kanishk Dudeja
Kamil Ogórek
Kang Ming Tay
Karan S
Expand Down
66 changes: 23 additions & 43 deletions apps/studio/components/interfaces/Auth/Policies/Policies.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { PostgresPolicy } from '@supabase/postgres-meta'
import { isEmpty } from 'lodash'
import { HelpCircle } from 'lucide-react'
import { useRouter } from 'next/router'
import Link from 'next/link'
import { useState } from 'react'
import { toast } from 'sonner'

Expand All @@ -11,32 +10,34 @@ import {
PolicyTableRowProps,
} from 'components/interfaces/Auth/Policies/PolicyTableRow'
import { ProtectedSchemaWarning } from 'components/interfaces/Database/ProtectedSchemaWarning'
import NoSearchResults from 'components/to-be-cleaned/NoSearchResults'
import ProductEmptyState from 'components/to-be-cleaned/ProductEmptyState'
import InformationBox from 'components/ui/InformationBox'
import { NoSearchResults } from 'components/ui/NoSearchResults'
import { useDatabasePolicyDeleteMutation } from 'data/database-policies/database-policy-delete-mutation'
import { useTableUpdateMutation } from 'data/tables/table-update-mutation'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Button, Card, CardContent } from 'ui'
import ConfirmModal from 'ui-patterns/Dialogs/ConfirmDialog'

interface PoliciesProps {
search?: string
schema: string
tables: PolicyTableRowProps['table'][]
hasTables: boolean
isLocked: boolean
onSelectCreatePolicy: (table: string) => void
onSelectEditPolicy: (policy: PostgresPolicy) => void
onResetSearch?: () => void
}

const Policies = ({
export const Policies = ({
search,
schema,
tables,
hasTables,
isLocked,
onSelectCreatePolicy,
onSelectEditPolicy: onSelectEditPolicyAI,
onResetSearch,
}: PoliciesProps) => {
const router = useRouter()
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()

Expand Down Expand Up @@ -115,40 +116,21 @@ const Policies = ({
})
}

if (tables.length === 0) {
if (!hasTables) {
return (
<div className="flex-grow flex items-center justify-center">
<ProductEmptyState
size="large"
title="Row-Level Security (RLS) Policies"
ctaButtonLabel="Create a table"
infoButtonLabel="What is RLS?"
infoButtonUrl="https://supabase.com/docs/guides/auth/row-level-security"
onClickCta={() => router.push(`/project/${ref}/editor`)}
>
<div className="space-y-4">
<InformationBox
title="What are policies?"
icon={<HelpCircle size={14} strokeWidth={2} />}
description={
<div className="space-y-2">
<p className="text-sm">
Policies restrict, on a per-user basis, which rows can be returned by normal
queries, or inserted, updated, or deleted by data modification commands.
</p>
<p className="text-sm">
This is also known as Row-Level Security (RLS). Each policy is attached to a
table, and the policy is executed each time its accessed.
</p>
</div>
}
/>
<p className="text-sm text-foreground-light">
Create a table in this schema first before creating a policy.
</p>
</div>
</ProductEmptyState>
</div>
<Card className="w-full bg-transparent">
<CardContent className="flex flex-col items-center justify-center p-8">
<h2 className="heading-default">No tables to create policies for</h2>

<p className="text-sm text-foreground-light text-center mb-4">
RLS Policies control per-user access to table rows. Create a table in this schema first
before creating a policy.
</p>
<Button asChild type="default">
<Link href={`/project/${ref}/editor`}>Create a table</Link>
</Button>
</CardContent>
</Card>
)
}

Expand All @@ -170,7 +152,7 @@ const Policies = ({
</section>
))
) : hasTables ? (
<NoSearchResults />
<NoSearchResults searchString={search ?? ''} onResetFilter={onResetSearch} />
) : null}
</div>

Expand Down Expand Up @@ -202,5 +184,3 @@ const Policies = ({
</>
)
}

export default Policies
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { noop } from 'lodash'
import { Edit, MoreVertical, Trash } from 'lucide-react'

import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
import Panel from 'components/ui/Panel'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
Expand All @@ -13,6 +12,8 @@ import {
Badge,
Button,
cn,
TableRow,
TableCell,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
Expand Down Expand Up @@ -57,58 +58,50 @@ const PolicyRow = ({
(policy.roles.includes('authenticated') || policy.roles.includes('public'))

return (
<Panel.Content
className={cn(
'flex border-overlay',
'w-full last:border-0 space-x-4 border-b py-4 lg:items-center'
)}
>
<div className="flex grow flex-col gap-y-1">
<div className="flex items-start gap-x-4">
<p className="font-mono text-xs text-foreground-light translate-y-[2px] min-w-12">
{policy.command}
</p>

<div className="flex flex-col gap-y-1">
<Button
type="text"
className="h-auto text-foreground text-sm border-none p-0 hover:bg-transparent justify-start"
onClick={() => onSelectEditPolicy(policy)}
>
{policy.name}
</Button>
<div className="flex items-center gap-x-1">
<div className="text-foreground-lighter text-sm">
Applied to:{' '}
{policy.roles.slice(0, 3).map((role, i) => (
<span key={`policy-${role}-${i}`}>
<code className="text-foreground-light text-xs">{role}</code>
{i < Math.min(policy.roles.length, 3) - 1 ? ', ' : ' '}
</span>
))}
{policy.roles.length > 1 ? 'roles' : 'role'}
</div>
{policy.roles.length > 3 && (
<Tooltip>
<TooltipTrigger asChild>
<code key="policy-etc" className="text-foreground-light text-xs">
+ {policy.roles.length - 3} more roles
</code>
</TooltipTrigger>
<TooltipContent side="bottom" align="center">
{policy.roles.slice(3).join(', ')}
</TooltipContent>
</Tooltip>
)}
</div>
</div>

<TableRow>
<TableCell className="w-[40%] truncate">
<div className="flex items-center gap-x-2 min-w-0">
<Button
type="text"
className="text-foreground text-sm p-0 hover:bg-transparent w-full truncate justify-start"
onClick={() => onSelectEditPolicy(policy)}
>
{policy.name}
</Button>
{appliesToAnonymousUsers ? (
<Badge color="yellow">Applies to anonymous users</Badge>
) : null}
</div>
</div>
<div>
</TableCell>
<TableCell className="w-[20%] truncate">
<code className="text-foreground-light text-xs">{policy.command}</code>
</TableCell>
<TableCell className="w-[30%] truncate">
<div className="flex items-center gap-x-1">
<div className="text-foreground-lighter text-sm">
{policy.roles.slice(0, 3).map((role, i) => (
<span key={`policy-${role}-${i}`}>
<code className="text-foreground-light text-xs">{role}</code>
{i < Math.min(policy.roles.length, 3) - 1 ? ', ' : ' '}
</span>
))}
{policy.roles.length > 1 ? 'roles' : 'role'}
</div>
{policy.roles.length > 3 && (
<Tooltip>
<TooltipTrigger asChild>
<code key="policy-etc" className="text-foreground-light text-xs">
+ {policy.roles.length - 3} more roles
</code>
</TooltipTrigger>
<TooltipContent side="bottom" align="center">
{policy.roles.slice(3).join(', ')}
</TooltipContent>
</Tooltip>
)}
</div>
</TableCell>
<TableCell className="w-0 text-right whitespace-nowrap">
{!isLocked && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
Expand All @@ -127,9 +120,9 @@ const PolicyRow = ({
name: `Update policy ${policy.name}`,
open: true,
sqlSnippets: [sql],
initialInput: `Update the policy with name "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table. It should...`,
initialInput: `Update the policy with name \"${policy.name}\" in the ${policy.schema} schema on the ${policy.table} table. It should...`,
suggestions: {
title: `I can help you make a change to the policy "${policy.name}" in the ${policy.schema} schema on the ${policy.table} table, here are a few example prompts to get you started:`,
title: `I can help you make a change to the policy \"${policy.name}\" in the ${policy.schema} schema on the ${policy.table} table, here are a few example prompts to get you started:`,
prompts: [
{
label: 'Improve Policy',
Expand Down Expand Up @@ -169,8 +162,8 @@ const PolicyRow = ({
</DropdownMenuContent>
</DropdownMenu>
)}
</div>
</Panel.Content>
</TableCell>
</TableRow>
)
}

Expand Down
Loading
Loading