RateLimits.tsx674 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import { useParams } from 'common'
4import Link from 'next/link'
5import { useEffect } from 'react'
6import { useForm } from 'react-hook-form'
7import { toast } from 'sonner'
8import {
9 Button,
10 Card,
11 CardContent,
12 CardFooter,
13 Form,
14 FormControl,
15 FormField,
16 FormInputGroupInput,
17 InputGroup,
18 InputGroupAddon,
19 InputGroupText,
20 Switch,
21 Tooltip,
22 TooltipContent,
23 TooltipTrigger,
24} from 'ui'
25import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
26import {
27 PageSection,
28 PageSectionContent,
29 PageSectionDescription,
30 PageSectionMeta,
31 PageSectionSummary,
32 PageSectionTitle,
33} from 'ui-patterns/PageSection'
34import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
35import * as z from 'zod'
36
37import { isSmtpEnabled } from '../SmtpForm/SmtpForm.utils'
38import AlertError from '@/components/ui/AlertError'
39import { InlineLink } from '@/components/ui/InlineLink'
40import NoPermission from '@/components/ui/NoPermission'
41import { useAuthConfigQuery } from '@/data/auth/auth-config-query'
42import { useAuthConfigUpdateMutation } from '@/data/auth/auth-config-update-mutation'
43import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
44import { DOCS_URL } from '@/lib/constants'
45
46export const RateLimits = () => {
47 const { ref: projectRef } = useParams()
48 const { can: canUpdateConfig } = useAsyncCheckPermissions(
49 PermissionAction.UPDATE,
50 'custom_config_gotrue'
51 )
52 const { can: canReadConfig } = useAsyncCheckPermissions(
53 PermissionAction.READ,
54 'custom_config_gotrue'
55 )
56
57 const {
58 data: authConfig,
59 error,
60 isPending: isLoading,
61 isError,
62 isSuccess,
63 } = useAuthConfigQuery({ projectRef })
64 const { mutate: updateAuthConfig, isPending: isUpdatingConfig } = useAuthConfigUpdateMutation({
65 onSuccess: () => {
66 toast.success('Rate limits successfully updated')
67 },
68 onError: (error) => {
69 toast.error(`Failed to update rate limits: ${error.message}`)
70 },
71 })
72
73 const canUpdateEmailLimit = authConfig?.EXTERNAL_EMAIL_ENABLED && isSmtpEnabled(authConfig)
74 const canUpdateSMSRateLimit = authConfig?.EXTERNAL_PHONE_ENABLED
75 const canUpdateAnonymousUsersRateLimit = authConfig?.EXTERNAL_ANONYMOUS_USERS_ENABLED
76 const canUpdateWeb3RateLimit = authConfig?.EXTERNAL_WEB3_SOLANA_ENABLED
77
78 const IPForwardingFormSchema = z.object({
79 SECURITY_SB_FORWARDED_FOR_ENABLED: z.coerce.boolean(),
80 })
81
82 const ipForwardingForm = useForm<z.infer<typeof IPForwardingFormSchema>>({
83 resolver: zodResolver(IPForwardingFormSchema as any),
84 defaultValues: {
85 SECURITY_SB_FORWARDED_FOR_ENABLED: false,
86 },
87 })
88
89 const onSubmitIPForwardingForm = (data: z.infer<typeof IPForwardingFormSchema>) => {
90 if (!projectRef) return console.error('Project ref is required')
91
92 const payload: Partial<z.infer<typeof IPForwardingFormSchema>> = {}
93
94 if (data.SECURITY_SB_FORWARDED_FOR_ENABLED !== authConfig?.SECURITY_SB_FORWARDED_FOR_ENABLED) {
95 payload.SECURITY_SB_FORWARDED_FOR_ENABLED = data.SECURITY_SB_FORWARDED_FOR_ENABLED
96 }
97
98 updateAuthConfig(
99 { projectRef, config: payload },
100 { onSuccess: () => ipForwardingForm.reset(data) }
101 )
102 }
103
104 const RateLimitFormSchema = z.object({
105 RATE_LIMIT_TOKEN_REFRESH: z.coerce
106 .number()
107 .min(0, 'Must be not be lower than 0')
108 .max(32767, 'Must not be more than 32,767 an 5 minutes'),
109 RATE_LIMIT_VERIFY: z.coerce
110 .number()
111 .min(0, 'Must be not be lower than 0')
112 .max(32767, 'Must not be more than 32,767 an 5 minutes'),
113 RATE_LIMIT_EMAIL_SENT: z.coerce
114 .number()
115 .min(0, 'Must be not be lower than 0')
116 .max(32767, 'Must not be more than 32,767 an hour'),
117 RATE_LIMIT_SMS_SENT: z.coerce
118 .number()
119 .min(0, 'Must be not be lower than 0')
120 .max(32767, 'Must not be more than 32,767 an hour'),
121 RATE_LIMIT_ANONYMOUS_USERS: z.coerce
122 .number()
123 .min(0, 'Must be not be lower than 0')
124 .max(32767, 'Must not be more than 32,767 an hour'),
125 RATE_LIMIT_OTP: z.coerce
126 .number()
127 .min(0, 'Must be not be lower than 0')
128 .max(32767, 'Must not be more than 32,767 an hour'),
129 RATE_LIMIT_WEB3: z.coerce
130 .number()
131 .min(0, 'Must be not be lower than 0')
132 .max(32767, 'Must not be more than 32,767 an hour'),
133 })
134
135 const rateLimitForm = useForm<z.infer<typeof RateLimitFormSchema>>({
136 resolver: zodResolver(RateLimitFormSchema as any),
137 defaultValues: {
138 RATE_LIMIT_TOKEN_REFRESH: 0,
139 RATE_LIMIT_VERIFY: 0,
140 RATE_LIMIT_EMAIL_SENT: 0,
141 RATE_LIMIT_SMS_SENT: 0,
142 RATE_LIMIT_ANONYMOUS_USERS: 0,
143 RATE_LIMIT_OTP: 0,
144 RATE_LIMIT_WEB3: 0,
145 },
146 })
147
148 const onSubmitRateLimitForm = (data: z.infer<typeof RateLimitFormSchema>) => {
149 if (!projectRef) return console.error('Project ref is required')
150
151 const payload: Partial<z.infer<typeof RateLimitFormSchema>> = {}
152 const params = [
153 'RATE_LIMIT_TOKEN_REFRESH',
154 'RATE_LIMIT_VERIFY',
155 'RATE_LIMIT_EMAIL_SENT',
156 'RATE_LIMIT_SMS_SENT',
157 'RATE_LIMIT_ANONYMOUS_USERS',
158 'RATE_LIMIT_OTP',
159 'RATE_LIMIT_WEB3',
160 ] as (keyof typeof payload)[]
161 params.forEach((param) => {
162 if (data[param] !== authConfig?.[param]) payload[param] = data[param]
163 })
164
165 updateAuthConfig(
166 { projectRef, config: payload },
167 { onSuccess: () => rateLimitForm.reset(data) }
168 )
169 }
170
171 useEffect(() => {
172 if (isSuccess) {
173 rateLimitForm.reset({
174 RATE_LIMIT_TOKEN_REFRESH: authConfig.RATE_LIMIT_TOKEN_REFRESH,
175 RATE_LIMIT_VERIFY: authConfig.RATE_LIMIT_VERIFY,
176 RATE_LIMIT_EMAIL_SENT: authConfig.RATE_LIMIT_EMAIL_SENT,
177 RATE_LIMIT_SMS_SENT: authConfig.RATE_LIMIT_SMS_SENT,
178 RATE_LIMIT_ANONYMOUS_USERS: authConfig.RATE_LIMIT_ANONYMOUS_USERS,
179 RATE_LIMIT_OTP: authConfig.RATE_LIMIT_OTP,
180 RATE_LIMIT_WEB3: authConfig.RATE_LIMIT_WEB3 ?? 0,
181 })
182
183 ipForwardingForm.reset({
184 SECURITY_SB_FORWARDED_FOR_ENABLED: authConfig.SECURITY_SB_FORWARDED_FOR_ENABLED,
185 })
186 }
187 // eslint-disable-next-line react-hooks/exhaustive-deps
188 }, [isSuccess])
189
190 if (isError) {
191 return (
192 <PageSection>
193 <PageSectionContent>
194 <AlertError error={error} subject="Failed to retrieve auth configuration" />
195 </PageSectionContent>
196 </PageSection>
197 )
198 }
199
200 if (!canReadConfig) {
201 return (
202 <PageSection>
203 <PageSectionContent>
204 <NoPermission resourceText="view auth configuration settings" />
205 </PageSectionContent>
206 </PageSection>
207 )
208 }
209
210 if (isLoading) {
211 return (
212 <PageSection>
213 <PageSectionContent>
214 <GenericSkeletonLoader />
215 </PageSectionContent>
216 </PageSection>
217 )
218 }
219
220 return (
221 <>
222 <PageSection>
223 <PageSectionContent>
224 <Form {...rateLimitForm}>
225 <form onSubmit={rateLimitForm.handleSubmit(onSubmitRateLimitForm)}>
226 <Card>
227 <CardContent>
228 <FormField
229 control={rateLimitForm.control}
230 name="RATE_LIMIT_EMAIL_SENT"
231 render={({ field }) => (
232 <FormItemLayout
233 layout="flex-row-reverse"
234 label="Rate limit for sending emails"
235 description="Number of emails that can be sent per hour from your project"
236 >
237 <Tooltip>
238 <TooltipTrigger asChild>
239 <FormControl>
240 <InputGroup>
241 <FormInputGroupInput
242 type="number"
243 min={0}
244 {...field}
245 disabled={!canUpdateConfig || !canUpdateEmailLimit}
246 />
247 <InputGroupAddon align="inline-end">
248 <InputGroupText>emails/h</InputGroupText>
249 </InputGroupAddon>
250 </InputGroup>
251 </FormControl>
252 </TooltipTrigger>
253 {!canUpdateConfig || !canUpdateEmailLimit ? (
254 <TooltipContent side="left" className="w-80 p-4">
255 {!authConfig.EXTERNAL_EMAIL_ENABLED ? (
256 <>
257 <p className="font-medium">
258 Email-based logins are not enabled for your project
259 </p>
260 <p className="mt-1">
261 Enable email-based logins to update this rate limit
262 </p>
263 <div className="mt-3">
264 <Button asChild type="default" size="tiny">
265 <Link href={`/project/${projectRef}/auth/providers`}>
266 View auth providers
267 </Link>
268 </Button>
269 </div>
270 </>
271 ) : (
272 <>
273 <p className="font-medium">
274 Custom SMTP provider is required to update this configuration
275 </p>
276 <p className="mt-1">
277 The built-in email service has a fixed rate limit. You will need
278 to set up your own custom SMTP provider to update your email
279 rate limit
280 </p>
281 <div className="mt-3">
282 <Button asChild type="default" size="tiny">
283 <Link href={`/project/${projectRef}/auth/smtp`}>
284 View SMTP settings
285 </Link>
286 </Button>
287 </div>
288 </>
289 )}
290 </TooltipContent>
291 ) : null}
292 </Tooltip>
293 </FormItemLayout>
294 )}
295 />
296 </CardContent>
297
298 <CardContent>
299 <FormField
300 control={rateLimitForm.control}
301 name="RATE_LIMIT_SMS_SENT"
302 render={({ field }) => (
303 <FormItemLayout
304 layout="flex-row-reverse"
305 label="Rate limit for sending SMS messages"
306 description="Number of SMS messages that can be sent per hour from your project"
307 >
308 <Tooltip>
309 <TooltipTrigger asChild>
310 <FormControl>
311 <InputGroup>
312 <FormInputGroupInput
313 type="number"
314 min={0}
315 {...field}
316 disabled={!canUpdateConfig || !canUpdateSMSRateLimit}
317 />
318 <InputGroupAddon align="inline-end">
319 <InputGroupText>sms/h</InputGroupText>
320 </InputGroupAddon>
321 </InputGroup>
322 </FormControl>
323 </TooltipTrigger>
324 {!canUpdateConfig || !canUpdateSMSRateLimit ? (
325 <TooltipContent side="left" className="w-80 p-4">
326 <p className="font-medium">
327 Phone-based logins are not enabled for your project
328 </p>
329 <p className="mt-1">
330 Enable phone-based logins to update this rate limit
331 </p>
332 <div className="mt-3">
333 <Button asChild type="default" size="tiny">
334 <Link href={`/project/${projectRef}/auth/providers`}>
335 View auth providers
336 </Link>
337 </Button>
338 </div>
339 </TooltipContent>
340 ) : null}
341 </Tooltip>
342 </FormItemLayout>
343 )}
344 />
345 </CardContent>
346
347 <CardContent>
348 <FormField
349 control={rateLimitForm.control}
350 name="RATE_LIMIT_TOKEN_REFRESH"
351 render={({ field }) => (
352 <FormItemLayout
353 layout="flex-row-reverse"
354 label="Rate limit for token refreshes"
355 description="Number of sessions that can be refreshed in a 5 minute interval per IP address"
356 >
357 <Tooltip>
358 <TooltipTrigger asChild>
359 <FormControl>
360 <InputGroup>
361 <FormInputGroupInput
362 type="number"
363 min={0}
364 {...field}
365 disabled={!canUpdateConfig}
366 />
367 <InputGroupAddon align="inline-end">
368 <InputGroupText>requests/5 min</InputGroupText>
369 </InputGroupAddon>
370 </InputGroup>
371 </FormControl>
372 </TooltipTrigger>
373 {!canUpdateConfig && (
374 <TooltipContent side="left" className="w-80 p-4">
375 <p className="font-medium">
376 You don't have permission to update this setting
377 </p>
378 <p className="mt-1">
379 You need additional permissions to update auth configuration
380 settings
381 </p>
382 </TooltipContent>
383 )}
384 </Tooltip>
385 {rateLimitForm.watch('RATE_LIMIT_TOKEN_REFRESH') > 0 && (
386 <p className="text-foreground-lighter text-sm mt-2">
387 {rateLimitForm.watch('RATE_LIMIT_TOKEN_REFRESH') * 12} requests per hour
388 </p>
389 )}
390 </FormItemLayout>
391 )}
392 />
393 </CardContent>
394
395 <CardContent>
396 <FormField
397 control={rateLimitForm.control}
398 name="RATE_LIMIT_VERIFY"
399 render={({ field }) => (
400 <FormItemLayout
401 layout="flex-row-reverse"
402 label="Rate limit for token verifications"
403 description="Number of OTP/Magic link verifications that can be made in a 5 minute interval per IP address"
404 >
405 <Tooltip>
406 <TooltipTrigger asChild>
407 <FormControl>
408 <InputGroup>
409 <FormInputGroupInput
410 type="number"
411 min={0}
412 {...field}
413 disabled={!canUpdateConfig}
414 />
415 <InputGroupAddon align="inline-end">
416 <InputGroupText>requests/5 min</InputGroupText>
417 </InputGroupAddon>
418 </InputGroup>
419 </FormControl>
420 </TooltipTrigger>
421 {!canUpdateConfig && (
422 <TooltipContent side="left" className="w-80 p-4">
423 <p className="font-medium">
424 You don't have permission to update this setting
425 </p>
426 <p className="mt-1">
427 You need additional permissions to update auth configuration
428 settings
429 </p>
430 </TooltipContent>
431 )}
432 </Tooltip>
433 {rateLimitForm.watch('RATE_LIMIT_VERIFY') > 0 && (
434 <p className="text-foreground-lighter text-sm mt-2">
435 {rateLimitForm.watch('RATE_LIMIT_VERIFY') * 12} requests per hour
436 </p>
437 )}
438 </FormItemLayout>
439 )}
440 />
441 </CardContent>
442
443 <CardContent>
444 <FormField
445 control={rateLimitForm.control}
446 name="RATE_LIMIT_ANONYMOUS_USERS"
447 render={({ field }) => (
448 <FormItemLayout
449 layout="flex-row-reverse"
450 label="Rate limit for anonymous users"
451 description="Number of anonymous sign-ins that can be made per hour per IP address"
452 >
453 <Tooltip>
454 <TooltipTrigger asChild>
455 <FormControl>
456 <InputGroup>
457 <FormInputGroupInput
458 type="number"
459 min={0}
460 {...field}
461 disabled={!canUpdateConfig || !canUpdateAnonymousUsersRateLimit}
462 />
463 <InputGroupAddon align="inline-end">
464 <InputGroupText>requests/h</InputGroupText>
465 </InputGroupAddon>
466 </InputGroup>
467 </FormControl>
468 </TooltipTrigger>
469 {!canUpdateConfig || !canUpdateAnonymousUsersRateLimit ? (
470 <TooltipContent side="left" className="w-80 p-4">
471 <p className="font-medium">
472 Anonymous sign-ins are not enabled for your project. Enable them to
473 control this rate limit.
474 </p>
475 <div className="mt-3">
476 <Button asChild type="default" size="tiny">
477 <Link href={`/project/${projectRef}/auth/providers`}>
478 View auth settings
479 </Link>
480 </Button>
481 </div>
482 </TooltipContent>
483 ) : null}
484 </Tooltip>
485 </FormItemLayout>
486 )}
487 />
488 </CardContent>
489
490 <CardContent>
491 <FormField
492 control={rateLimitForm.control}
493 name="RATE_LIMIT_OTP"
494 render={({ field }) => (
495 <FormItemLayout
496 layout="flex-row-reverse"
497 label="Rate limit for sign-ups and sign-ins"
498 description="Number of sign-up and sign-in requests that can be made in a 5 minute interval per IP address (excludes anonymous users)"
499 >
500 <Tooltip>
501 <TooltipTrigger asChild>
502 <FormControl>
503 <InputGroup>
504 <FormInputGroupInput
505 type="number"
506 min={0}
507 {...field}
508 disabled={!canUpdateConfig}
509 />
510 <InputGroupAddon align="inline-end">
511 <InputGroupText>requests/5 min</InputGroupText>
512 </InputGroupAddon>
513 </InputGroup>
514 </FormControl>
515 </TooltipTrigger>
516 {!canUpdateConfig && (
517 <TooltipContent side="left" className="w-80 p-4">
518 <p className="font-medium">
519 You don't have permission to update this setting
520 </p>
521 <p className="mt-1">
522 You need additional permissions to update auth configuration
523 settings
524 </p>
525 </TooltipContent>
526 )}
527 </Tooltip>
528 {rateLimitForm.watch('RATE_LIMIT_OTP') > 0 && (
529 <p className="text-foreground-lighter text-sm mt-2">
530 {rateLimitForm.watch('RATE_LIMIT_OTP') * 12} requests per hour
531 </p>
532 )}
533 </FormItemLayout>
534 )}
535 />
536 </CardContent>
537
538 <CardContent>
539 <FormField
540 control={rateLimitForm.control}
541 name="RATE_LIMIT_WEB3"
542 render={({ field }) => (
543 <FormItemLayout
544 layout="flex-row-reverse"
545 label="Rate limit for Web3 sign-ups and sign-ins"
546 description="Number of Web3 sign-up or sign-in requests that can be made per IP address in 5 minutes"
547 >
548 <Tooltip>
549 <TooltipTrigger asChild>
550 <FormControl>
551 <InputGroup>
552 <FormInputGroupInput
553 type="number"
554 min={0}
555 {...field}
556 disabled={!canUpdateConfig || !canUpdateWeb3RateLimit}
557 />
558 <InputGroupAddon align="inline-end">
559 <InputGroupText>requests/5 min</InputGroupText>
560 </InputGroupAddon>
561 </InputGroup>
562 </FormControl>
563 </TooltipTrigger>
564 {!canUpdateConfig || !canUpdateWeb3RateLimit ? (
565 <TooltipContent side="left" className="w-80 p-4">
566 <p className="font-medium">
567 Web3 auth provider is not enabled for this project. Enable it to
568 control this rate limit.
569 </p>
570 <div className="mt-3">
571 <Button asChild type="default" size="tiny">
572 <Link href={`/project/${projectRef}/auth/providers`}>
573 View Auth provider settings
574 </Link>
575 </Button>
576 </div>
577 </TooltipContent>
578 ) : null}
579 </Tooltip>
580 </FormItemLayout>
581 )}
582 />
583 </CardContent>
584
585 <CardFooter className="justify-end space-x-2">
586 {rateLimitForm.formState.isDirty && (
587 <Button type="default" onClick={() => rateLimitForm.reset()}>
588 Cancel
589 </Button>
590 )}
591 <Button
592 type="primary"
593 htmlType="submit"
594 disabled={
595 !canUpdateConfig || isUpdatingConfig || !rateLimitForm.formState.isDirty
596 }
597 loading={isUpdatingConfig}
598 >
599 Save changes
600 </Button>
601 </CardFooter>
602 </Card>
603 </form>
604 </Form>
605 </PageSectionContent>
606 </PageSection>
607
608 <PageSection>
609 <PageSectionMeta>
610 <PageSectionSummary>
611 <PageSectionTitle>IP Address Forwarding</PageSectionTitle>
612 <PageSectionDescription>
613 Control how Auth determines source IP address for rate limiting.
614 </PageSectionDescription>
615 </PageSectionSummary>
616 </PageSectionMeta>
617 <PageSectionContent>
618 <Form {...ipForwardingForm}>
619 <form onSubmit={ipForwardingForm.handleSubmit(onSubmitIPForwardingForm)}>
620 <Card>
621 <CardContent>
622 <FormField
623 control={ipForwardingForm.control}
624 name="SECURITY_SB_FORWARDED_FOR_ENABLED"
625 render={({ field }) => (
626 <FormItemLayout
627 layout="flex-row-reverse"
628 label="Enable IP address forwarding"
629 description=<>
630 Clients can forward end-user IP addresses to Auth for rate limiting when
631 using secret API keys.{' '}
632 <InlineLink
633 href={`${DOCS_URL}/guides/auth/rate-limits#ip-address-forwarding`}
634 >
635 Learn more
636 </InlineLink>
637 </>
638 >
639 <FormControl>
640 <Switch
641 checked={field.value}
642 onCheckedChange={(value) => field.onChange(value)}
643 disabled={!canUpdateConfig}
644 />
645 </FormControl>
646 </FormItemLayout>
647 )}
648 />
649 </CardContent>
650 <CardFooter className="justify-end space-x-2">
651 {ipForwardingForm.formState.isDirty && (
652 <Button type="default" onClick={() => ipForwardingForm.reset()}>
653 Cancel
654 </Button>
655 )}
656 <Button
657 type="primary"
658 htmlType="submit"
659 disabled={
660 !canUpdateConfig || isUpdatingConfig || !ipForwardingForm.formState.isDirty
661 }
662 loading={isUpdatingConfig}
663 >
664 Save changes
665 </Button>
666 </CardFooter>
667 </Card>
668 </form>
669 </Form>
670 </PageSectionContent>
671 </PageSection>
672 </>
673 )
674}