Files
supabase/apps/studio/components/interfaces/Auth/RateLimits/RateLimits.tsx
T
Gildas Garcia 0facd341a6 chore: remove UI form components _Shadcn_ suffix (#45212)
## Problem

We used to have a `_Shadcn_` suffix for all the shadcn form components
because we also had `formik` form components.
This is not needed anymore.

## Solution

- Remove the suffix
- Update all usages
2026-04-24 12:14:15 +02:00

675 lines
28 KiB
TypeScript

import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
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,
FormInputGroupInput,
InputGroup,
InputGroupAddon,
InputGroupText,
Switch,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import * as z from 'zod'
import { isSmtpEnabled } from '../SmtpForm/SmtpForm.utils'
import AlertError from '@/components/ui/AlertError'
import { InlineLink } from '@/components/ui/InlineLink'
import NoPermission from '@/components/ui/NoPermission'
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'
export const RateLimits = () => {
const { ref: projectRef } = useParams()
const { can: canUpdateConfig } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const { can: canReadConfig } = useAsyncCheckPermissions(
PermissionAction.READ,
'custom_config_gotrue'
)
const {
data: authConfig,
error,
isPending: isLoading,
isError,
isSuccess,
} = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation({
onSuccess: () => {
toast.success('Rate limits successfully updated')
},
onError: (error) => {
toast.error(`Failed to update rate limits: ${error.message}`)
},
})
const canUpdateEmailLimit = authConfig?.EXTERNAL_EMAIL_ENABLED && isSmtpEnabled(authConfig)
const canUpdateSMSRateLimit = authConfig?.EXTERNAL_PHONE_ENABLED
const canUpdateAnonymousUsersRateLimit = authConfig?.EXTERNAL_ANONYMOUS_USERS_ENABLED
const canUpdateWeb3RateLimit = authConfig?.EXTERNAL_WEB3_SOLANA_ENABLED
const IPForwardingFormSchema = z.object({
SECURITY_SB_FORWARDED_FOR_ENABLED: z.coerce.boolean(),
})
const ipForwardingForm = useForm<z.infer<typeof IPForwardingFormSchema>>({
resolver: zodResolver(IPForwardingFormSchema),
defaultValues: {
SECURITY_SB_FORWARDED_FOR_ENABLED: false,
},
})
const onSubmitIPForwardingForm = (data: z.infer<typeof IPForwardingFormSchema>) => {
if (!projectRef) return console.error('Project ref is required')
const payload: Partial<z.infer<typeof IPForwardingFormSchema>> = {}
if (data.SECURITY_SB_FORWARDED_FOR_ENABLED !== authConfig?.SECURITY_SB_FORWARDED_FOR_ENABLED) {
payload.SECURITY_SB_FORWARDED_FOR_ENABLED = data.SECURITY_SB_FORWARDED_FOR_ENABLED
}
updateAuthConfig(
{ projectRef, config: payload },
{ onSuccess: () => ipForwardingForm.reset(data) }
)
}
const RateLimitFormSchema = z.object({
RATE_LIMIT_TOKEN_REFRESH: z.coerce
.number()
.min(0, 'Must be not be lower than 0')
.max(32767, 'Must not be more than 32,767 an 5 minutes'),
RATE_LIMIT_VERIFY: z.coerce
.number()
.min(0, 'Must be not be lower than 0')
.max(32767, 'Must not be more than 32,767 an 5 minutes'),
RATE_LIMIT_EMAIL_SENT: z.coerce
.number()
.min(0, 'Must be not be lower than 0')
.max(32767, 'Must not be more than 32,767 an hour'),
RATE_LIMIT_SMS_SENT: z.coerce
.number()
.min(0, 'Must be not be lower than 0')
.max(32767, 'Must not be more than 32,767 an hour'),
RATE_LIMIT_ANONYMOUS_USERS: z.coerce
.number()
.min(0, 'Must be not be lower than 0')
.max(32767, 'Must not be more than 32,767 an hour'),
RATE_LIMIT_OTP: z.coerce
.number()
.min(0, 'Must be not be lower than 0')
.max(32767, 'Must not be more than 32,767 an hour'),
RATE_LIMIT_WEB3: z.coerce
.number()
.min(0, 'Must be not be lower than 0')
.max(32767, 'Must not be more than 32,767 an hour'),
})
const rateLimitForm = useForm<z.infer<typeof RateLimitFormSchema>>({
resolver: zodResolver(RateLimitFormSchema),
defaultValues: {
RATE_LIMIT_TOKEN_REFRESH: 0,
RATE_LIMIT_VERIFY: 0,
RATE_LIMIT_EMAIL_SENT: 0,
RATE_LIMIT_SMS_SENT: 0,
RATE_LIMIT_ANONYMOUS_USERS: 0,
RATE_LIMIT_OTP: 0,
RATE_LIMIT_WEB3: 0,
},
})
const onSubmitRateLimitForm = (data: z.infer<typeof RateLimitFormSchema>) => {
if (!projectRef) return console.error('Project ref is required')
const payload: Partial<z.infer<typeof RateLimitFormSchema>> = {}
const params = [
'RATE_LIMIT_TOKEN_REFRESH',
'RATE_LIMIT_VERIFY',
'RATE_LIMIT_EMAIL_SENT',
'RATE_LIMIT_SMS_SENT',
'RATE_LIMIT_ANONYMOUS_USERS',
'RATE_LIMIT_OTP',
'RATE_LIMIT_WEB3',
] as (keyof typeof payload)[]
params.forEach((param) => {
if (data[param] !== authConfig?.[param]) payload[param] = data[param]
})
updateAuthConfig(
{ projectRef, config: payload },
{ onSuccess: () => rateLimitForm.reset(data) }
)
}
useEffect(() => {
if (isSuccess) {
rateLimitForm.reset({
RATE_LIMIT_TOKEN_REFRESH: authConfig.RATE_LIMIT_TOKEN_REFRESH,
RATE_LIMIT_VERIFY: authConfig.RATE_LIMIT_VERIFY,
RATE_LIMIT_EMAIL_SENT: authConfig.RATE_LIMIT_EMAIL_SENT,
RATE_LIMIT_SMS_SENT: authConfig.RATE_LIMIT_SMS_SENT,
RATE_LIMIT_ANONYMOUS_USERS: authConfig.RATE_LIMIT_ANONYMOUS_USERS,
RATE_LIMIT_OTP: authConfig.RATE_LIMIT_OTP,
RATE_LIMIT_WEB3: authConfig.RATE_LIMIT_WEB3 ?? 0,
})
ipForwardingForm.reset({
SECURITY_SB_FORWARDED_FOR_ENABLED: authConfig.SECURITY_SB_FORWARDED_FOR_ENABLED,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isSuccess])
if (isError) {
return (
<PageSection>
<PageSectionContent>
<AlertError error={error} subject="Failed to retrieve auth configuration" />
</PageSectionContent>
</PageSection>
)
}
if (!canReadConfig) {
return (
<PageSection>
<PageSectionContent>
<NoPermission resourceText="view auth configuration settings" />
</PageSectionContent>
</PageSection>
)
}
if (isLoading) {
return (
<PageSection>
<PageSectionContent>
<GenericSkeletonLoader />
</PageSectionContent>
</PageSection>
)
}
return (
<>
<PageSection>
<PageSectionContent>
<Form {...rateLimitForm}>
<form onSubmit={rateLimitForm.handleSubmit(onSubmitRateLimitForm)}>
<Card>
<CardContent>
<FormField
control={rateLimitForm.control}
name="RATE_LIMIT_EMAIL_SENT"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Rate limit for sending emails"
description="Number of emails that can be sent per hour from your project"
>
<Tooltip>
<TooltipTrigger asChild>
<FormControl>
<InputGroup>
<FormInputGroupInput
type="number"
min={0}
{...field}
disabled={!canUpdateConfig || !canUpdateEmailLimit}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>emails/h</InputGroupText>
</InputGroupAddon>
</InputGroup>
</FormControl>
</TooltipTrigger>
{!canUpdateConfig || !canUpdateEmailLimit ? (
<TooltipContent side="left" className="w-80 p-4">
{!authConfig.EXTERNAL_EMAIL_ENABLED ? (
<>
<p className="font-medium">
Email-based logins are not enabled for your project
</p>
<p className="mt-1">
Enable email-based logins to update this rate limit
</p>
<div className="mt-3">
<Button asChild type="default" size="tiny">
<Link href={`/project/${projectRef}/auth/providers`}>
View auth providers
</Link>
</Button>
</div>
</>
) : (
<>
<p className="font-medium">
Custom SMTP provider is required to update this configuration
</p>
<p className="mt-1">
The built-in email service has a fixed rate limit. You will need
to set up your own custom SMTP provider to update your email
rate limit
</p>
<div className="mt-3">
<Button asChild type="default" size="tiny">
<Link href={`/project/${projectRef}/auth/smtp`}>
View SMTP settings
</Link>
</Button>
</div>
</>
)}
</TooltipContent>
) : null}
</Tooltip>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={rateLimitForm.control}
name="RATE_LIMIT_SMS_SENT"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Rate limit for sending SMS messages"
description="Number of SMS messages that can be sent per hour from your project"
>
<Tooltip>
<TooltipTrigger asChild>
<FormControl>
<InputGroup>
<FormInputGroupInput
type="number"
min={0}
{...field}
disabled={!canUpdateConfig || !canUpdateSMSRateLimit}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>sms/h</InputGroupText>
</InputGroupAddon>
</InputGroup>
</FormControl>
</TooltipTrigger>
{!canUpdateConfig || !canUpdateSMSRateLimit ? (
<TooltipContent side="left" className="w-80 p-4">
<p className="font-medium">
Phone-based logins are not enabled for your project
</p>
<p className="mt-1">
Enable phone-based logins to update this rate limit
</p>
<div className="mt-3">
<Button asChild type="default" size="tiny">
<Link href={`/project/${projectRef}/auth/providers`}>
View auth providers
</Link>
</Button>
</div>
</TooltipContent>
) : null}
</Tooltip>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={rateLimitForm.control}
name="RATE_LIMIT_TOKEN_REFRESH"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Rate limit for token refreshes"
description="Number of sessions that can be refreshed in a 5 minute interval per IP address"
>
<Tooltip>
<TooltipTrigger asChild>
<FormControl>
<InputGroup>
<FormInputGroupInput
type="number"
min={0}
{...field}
disabled={!canUpdateConfig}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>requests/5 min</InputGroupText>
</InputGroupAddon>
</InputGroup>
</FormControl>
</TooltipTrigger>
{!canUpdateConfig && (
<TooltipContent side="left" className="w-80 p-4">
<p className="font-medium">
You don't have permission to update this setting
</p>
<p className="mt-1">
You need additional permissions to update auth configuration
settings
</p>
</TooltipContent>
)}
</Tooltip>
{rateLimitForm.watch('RATE_LIMIT_TOKEN_REFRESH') > 0 && (
<p className="text-foreground-lighter text-sm mt-2">
{rateLimitForm.watch('RATE_LIMIT_TOKEN_REFRESH') * 12} requests per hour
</p>
)}
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={rateLimitForm.control}
name="RATE_LIMIT_VERIFY"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Rate limit for token verifications"
description="Number of OTP/Magic link verifications that can be made in a 5 minute interval per IP address"
>
<Tooltip>
<TooltipTrigger asChild>
<FormControl>
<InputGroup>
<FormInputGroupInput
type="number"
min={0}
{...field}
disabled={!canUpdateConfig}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>requests/5 min</InputGroupText>
</InputGroupAddon>
</InputGroup>
</FormControl>
</TooltipTrigger>
{!canUpdateConfig && (
<TooltipContent side="left" className="w-80 p-4">
<p className="font-medium">
You don't have permission to update this setting
</p>
<p className="mt-1">
You need additional permissions to update auth configuration
settings
</p>
</TooltipContent>
)}
</Tooltip>
{rateLimitForm.watch('RATE_LIMIT_VERIFY') > 0 && (
<p className="text-foreground-lighter text-sm mt-2">
{rateLimitForm.watch('RATE_LIMIT_VERIFY') * 12} requests per hour
</p>
)}
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={rateLimitForm.control}
name="RATE_LIMIT_ANONYMOUS_USERS"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Rate limit for anonymous users"
description="Number of anonymous sign-ins that can be made per hour per IP address"
>
<Tooltip>
<TooltipTrigger asChild>
<FormControl>
<InputGroup>
<FormInputGroupInput
type="number"
min={0}
{...field}
disabled={!canUpdateConfig || !canUpdateAnonymousUsersRateLimit}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>requests/h</InputGroupText>
</InputGroupAddon>
</InputGroup>
</FormControl>
</TooltipTrigger>
{!canUpdateConfig || !canUpdateAnonymousUsersRateLimit ? (
<TooltipContent side="left" className="w-80 p-4">
<p className="font-medium">
Anonymous sign-ins are not enabled for your project. Enable them to
control this rate limit.
</p>
<div className="mt-3">
<Button asChild type="default" size="tiny">
<Link href={`/project/${projectRef}/auth/providers`}>
View auth settings
</Link>
</Button>
</div>
</TooltipContent>
) : null}
</Tooltip>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={rateLimitForm.control}
name="RATE_LIMIT_OTP"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Rate limit for sign-ups and sign-ins"
description="Number of sign-up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users)"
>
<Tooltip>
<TooltipTrigger asChild>
<FormControl>
<InputGroup>
<FormInputGroupInput
type="number"
min={0}
{...field}
disabled={!canUpdateConfig}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>requests/5 min</InputGroupText>
</InputGroupAddon>
</InputGroup>
</FormControl>
</TooltipTrigger>
{!canUpdateConfig && (
<TooltipContent side="left" className="w-80 p-4">
<p className="font-medium">
You don't have permission to update this setting
</p>
<p className="mt-1">
You need additional permissions to update auth configuration
settings
</p>
</TooltipContent>
)}
</Tooltip>
{rateLimitForm.watch('RATE_LIMIT_OTP') > 0 && (
<p className="text-foreground-lighter text-sm mt-2">
{rateLimitForm.watch('RATE_LIMIT_OTP') * 12} requests per hour
</p>
)}
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField
control={rateLimitForm.control}
name="RATE_LIMIT_WEB3"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Rate limit for Web3 sign-ups and sign-ins"
description="Number of Web3 sign-up or sign-in requests that can be made per IP address in 5 minutes"
>
<Tooltip>
<TooltipTrigger asChild>
<FormControl>
<InputGroup>
<FormInputGroupInput
type="number"
min={0}
{...field}
disabled={!canUpdateConfig || !canUpdateWeb3RateLimit}
/>
<InputGroupAddon align="inline-end">
<InputGroupText>requests/5 min</InputGroupText>
</InputGroupAddon>
</InputGroup>
</FormControl>
</TooltipTrigger>
{!canUpdateConfig || !canUpdateWeb3RateLimit ? (
<TooltipContent side="left" className="w-80 p-4">
<p className="font-medium">
Web3 auth provider is not enabled for this project. Enable it to
control this rate limit.
</p>
<div className="mt-3">
<Button asChild type="default" size="tiny">
<Link href={`/project/${projectRef}/auth/providers`}>
View Auth provider settings
</Link>
</Button>
</div>
</TooltipContent>
) : null}
</Tooltip>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{rateLimitForm.formState.isDirty && (
<Button type="default" onClick={() => rateLimitForm.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={
!canUpdateConfig || isUpdatingConfig || !rateLimitForm.formState.isDirty
}
loading={isUpdatingConfig}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form>
</PageSectionContent>
</PageSection>
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>IP Address Forwarding</PageSectionTitle>
<PageSectionDescription>
Control how Auth determines source IP address for rate limiting.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Form {...ipForwardingForm}>
<form onSubmit={ipForwardingForm.handleSubmit(onSubmitIPForwardingForm)}>
<Card>
<CardContent>
<FormField
control={ipForwardingForm.control}
name="SECURITY_SB_FORWARDED_FOR_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Enable IP address forwarding"
description=<>
Clients can forward end-user IP addresses to Auth for rate limiting when
using secret API keys.{' '}
<InlineLink
href={`${DOCS_URL}/guides/auth/rate-limits#ip-address-forwarding`}
>
Learn more
</InlineLink>
</>
>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={(value) => field.onChange(value)}
disabled={!canUpdateConfig}
/>
</FormControl>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{ipForwardingForm.formState.isDirty && (
<Button type="default" onClick={() => ipForwardingForm.reset()}>
Cancel
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={
!canUpdateConfig || isUpdatingConfig || !ipForwardingForm.formState.isDirty
}
loading={isUpdatingConfig}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form>
</PageSectionContent>
</PageSection>
</>
)
}