AddRestrictionModal.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import { zodResolver } from '@hookform/resolvers/zod'
  2. import { useParams } from 'common'
  3. import { HelpCircle } from 'lucide-react'
  4. import { useEffect } from 'react'
  5. import { useForm, useWatch } from 'react-hook-form'
  6. import { toast } from 'sonner'
  7. import {
  8. Button,
  9. Form,
  10. FormControl,
  11. FormField,
  12. Input,
  13. Modal,
  14. Tooltip,
  15. TooltipContent,
  16. TooltipTrigger,
  17. } from 'ui'
  18. import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
  19. import * as z from 'zod'
  20. import { checkIfPrivate, getAddressEndRange, normalize } from './NetworkRestrictions.utils'
  21. import InformationBox from '@/components/ui/InformationBox'
  22. import { useNetworkRestrictionsQuery } from '@/data/network-restrictions/network-restrictions-query'
  23. import { useNetworkRestrictionsApplyMutation } from '@/data/network-restrictions/network-retrictions-apply-mutation'
  24. import { DOCS_URL } from '@/lib/constants'
  25. const IPV4_MAX_CIDR_BLOCK_SIZE = 32
  26. const IPV6_MAX_CIDR_BLOCK_SIZE = 128
  27. interface AddRestrictionModalProps {
  28. type?: 'IPv4' | 'IPv6'
  29. hasOverachingRestriction: boolean
  30. onClose: () => void
  31. }
  32. const AddRestrictionModal = ({
  33. type,
  34. hasOverachingRestriction,
  35. onClose,
  36. }: AddRestrictionModalProps) => {
  37. const formId = 'add-restriction-form'
  38. const { ref } = useParams()
  39. const { data } = useNetworkRestrictionsQuery({ projectRef: ref }, { enabled: type !== undefined })
  40. const ipv4Restrictions = data?.config?.dbAllowedCidrs ?? []
  41. // @ts-ignore [Joshen] API typing issue
  42. const ipv6Restrictions = data?.config?.dbAllowedCidrsV6 ?? []
  43. const restrictedIps = ipv4Restrictions.concat(ipv6Restrictions)
  44. const { mutate: applyNetworkRestrictions, isPending: isApplying } =
  45. useNetworkRestrictionsApplyMutation({
  46. onSuccess: () => {
  47. toast.success('Successfully added restriction')
  48. onClose()
  49. },
  50. })
  51. const cidrBlockSizeValidationMessage = `Size has to be between 0 to ${
  52. type === 'IPv4' ? IPV4_MAX_CIDR_BLOCK_SIZE : IPV6_MAX_CIDR_BLOCK_SIZE
  53. }`
  54. const formSchema = z.object({
  55. cidrBlockSize: z.coerce
  56. .number()
  57. .min(0, cidrBlockSizeValidationMessage)
  58. .max(
  59. type === 'IPv4' ? IPV4_MAX_CIDR_BLOCK_SIZE : IPV6_MAX_CIDR_BLOCK_SIZE,
  60. cidrBlockSizeValidationMessage
  61. ),
  62. ipAddress: z
  63. .string()
  64. .min(1, `Please enter a valid IP address`)
  65. .ip({
  66. version: type === 'IPv4' ? 'v4' : 'v6',
  67. message: `Please enter a valid ${type} address`,
  68. })
  69. .refine((val) => !checkIfPrivate(type, val), 'Private IP addresses are not supported'),
  70. })
  71. const form = useForm<z.infer<typeof formSchema>>({
  72. resolver: zodResolver(formSchema as any),
  73. defaultValues: {
  74. ipAddress: '',
  75. cidrBlockSize: type === 'IPv4' ? IPV4_MAX_CIDR_BLOCK_SIZE : IPV6_MAX_CIDR_BLOCK_SIZE,
  76. },
  77. })
  78. const { reset, formState } = form
  79. const { errors } = formState
  80. useEffect(() => {
  81. reset({
  82. ipAddress: '',
  83. cidrBlockSize: type === 'IPv4' ? IPV4_MAX_CIDR_BLOCK_SIZE : IPV6_MAX_CIDR_BLOCK_SIZE,
  84. })
  85. }, [type, reset])
  86. const onSubmit = async (values: any) => {
  87. if (!ref) return console.error('Project ref is required')
  88. const address = `${values.ipAddress}/${values.cidrBlockSize}`
  89. const normalizedAddress = normalize(address)
  90. const alreadyExists =
  91. restrictedIps.includes(address) || restrictedIps.includes(normalizedAddress)
  92. if (alreadyExists) {
  93. return toast(`The address ${address} is already restricted`)
  94. }
  95. // Need to replace over arching restriction (allow all / disallow all)
  96. if (hasOverachingRestriction) {
  97. const dbAllowedCidrs = type === 'IPv4' ? [normalizedAddress] : []
  98. const dbAllowedCidrsV6 = type === 'IPv6' ? [normalizedAddress] : []
  99. applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs, dbAllowedCidrsV6 })
  100. } else {
  101. const dbAllowedCidrs =
  102. type === 'IPv4' ? [...ipv4Restrictions, normalizedAddress] : ipv4Restrictions
  103. const dbAllowedCidrsV6 =
  104. type === 'IPv6' ? [...ipv6Restrictions, normalizedAddress] : ipv6Restrictions
  105. applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs, dbAllowedCidrsV6 })
  106. }
  107. }
  108. const [cidrBlockSize, ipAddress] = useWatch({
  109. name: ['cidrBlockSize', 'ipAddress'],
  110. control: form.control,
  111. })
  112. const availableAddresses =
  113. type === 'IPv4'
  114. ? Math.pow(2, IPV4_MAX_CIDR_BLOCK_SIZE - (cidrBlockSize ?? 0))
  115. : Math.pow(2, IPV6_MAX_CIDR_BLOCK_SIZE - (cidrBlockSize ?? 0))
  116. const addressRange =
  117. type !== undefined ? getAddressEndRange(type, `${ipAddress}/${cidrBlockSize}`) : undefined
  118. const isValidCIDR =
  119. errors.cidrBlockSize == null && errors.ipAddress == null && addressRange != null
  120. const normalizedAddress = isValidCIDR
  121. ? normalize(`${ipAddress}/${cidrBlockSize}`)
  122. : `${ipAddress}/${cidrBlockSize}`
  123. return (
  124. <Modal
  125. hideFooter
  126. size="medium"
  127. visible={type !== undefined}
  128. onCancel={onClose}
  129. header={`Add a new ${type} restriction`}
  130. >
  131. <Form {...form}>
  132. <Modal.Content className="space-y-4">
  133. <p className="text-sm text-foreground-light">
  134. This will add an IP address range to a list of allowed ranges that can access your
  135. database.
  136. </p>
  137. <InformationBox
  138. title="Note: Restrictions only apply to direct connections to your database and connection pooler"
  139. description="They do not currently apply to APIs offered over HTTPS, such as PostgREST, Storage, or Authentication."
  140. urlLabel="Learn more"
  141. url={`${DOCS_URL}/guides/platform/network-restrictions#limitations`}
  142. />
  143. <form
  144. id={formId}
  145. onSubmit={form.handleSubmit(onSubmit)}
  146. noValidate
  147. className="flex space-x-4"
  148. >
  149. <div className="w-[55%]">
  150. <FormField
  151. control={form.control}
  152. name="ipAddress"
  153. render={({ field }) => (
  154. <FormItemLayout layout="vertical" label={`${type} address`}>
  155. <FormControl>
  156. <Input {...field} placeholder={type === 'IPv4' ? '0.0.0.0' : '::0'} />
  157. </FormControl>
  158. </FormItemLayout>
  159. )}
  160. />
  161. </div>
  162. <div className="grow">
  163. <FormField
  164. control={form.control}
  165. name="cidrBlockSize"
  166. render={({ field }) => (
  167. <FormItemLayout
  168. layout="vertical"
  169. label={
  170. <div className="flex items-center space-x-2">
  171. <p>CIDR Block Size</p>
  172. <Tooltip>
  173. <TooltipTrigger>
  174. <HelpCircle size="14" strokeWidth={2} />
  175. </TooltipTrigger>
  176. <TooltipContent side="bottom" className="w-80">
  177. Classless inter-domain routing (CIDR) notation is the notation used to
  178. identify networks and hosts in the networks. The block size tells us how
  179. many bits we need to take for the network prefix, and is a value between
  180. 0 to{' '}
  181. {type === 'IPv4' ? IPV4_MAX_CIDR_BLOCK_SIZE : IPV6_MAX_CIDR_BLOCK_SIZE}.
  182. </TooltipContent>
  183. </Tooltip>
  184. </div>
  185. }
  186. >
  187. <FormControl>
  188. <Input
  189. {...field}
  190. type="number"
  191. min={0}
  192. max={type === 'IPv4' ? IPV4_MAX_CIDR_BLOCK_SIZE : IPV6_MAX_CIDR_BLOCK_SIZE}
  193. onChange={(e) => field.onChange(Number(e.target.value))}
  194. placeholder={
  195. type === 'IPv4'
  196. ? IPV4_MAX_CIDR_BLOCK_SIZE.toString()
  197. : IPV6_MAX_CIDR_BLOCK_SIZE.toString()
  198. }
  199. />
  200. </FormControl>
  201. </FormItemLayout>
  202. )}
  203. />
  204. </div>
  205. </form>
  206. </Modal.Content>
  207. <Modal.Separator />
  208. {isValidCIDR ? (
  209. <Modal.Content className="space-y-1">
  210. <p className="text-sm">
  211. The address range <code className="text-code-inline">{normalizedAddress}</code> will
  212. be restricted
  213. </p>
  214. <p className="text-sm text-foreground-light">
  215. Selected address space: <code className="text-code-inline">{addressRange.start}</code>{' '}
  216. to <code className="text-code-inline">{addressRange.end}</code>{' '}
  217. </p>
  218. <p className="text-sm text-foreground-light">
  219. Number of addresses: {availableAddresses}
  220. </p>
  221. </Modal.Content>
  222. ) : (
  223. <Modal.Content>
  224. <div className="h-[68px] flex items-center">
  225. <p className="text-sm text-foreground-light">
  226. A summary of your restriction will be shown here after entering a valid IP address
  227. and CIDR block size. IP addresses will also be normalized.
  228. </p>
  229. </div>
  230. </Modal.Content>
  231. )}
  232. <Modal.Separator />
  233. <Modal.Content className="flex items-center justify-end space-x-2">
  234. <Button type="default" disabled={isApplying} onClick={() => onClose()}>
  235. Cancel
  236. </Button>
  237. <Button form={formId} htmlType="submit" loading={isApplying} disabled={isApplying}>
  238. Save restriction
  239. </Button>
  240. </Modal.Content>
  241. </Form>
  242. </Modal>
  243. )
  244. }
  245. export default AddRestrictionModal