| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- import { PermissionAction } from '@supabase/shared-types/out/constants'
- import { useParams } from 'common'
- import { AlertCircle, ChevronDown, Globe, Lock } from 'lucide-react'
- import { useState } from 'react'
- import {
- Badge,
- Button,
- Card,
- CardContent,
- CardDescription,
- CardHeader,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
- Tooltip,
- TooltipContent,
- TooltipTrigger,
- } from 'ui'
- import {
- PageSection,
- PageSectionAside,
- PageSectionContent,
- PageSectionMeta,
- PageSectionSummary,
- PageSectionTitle,
- } from 'ui-patterns'
- import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
- import AddRestrictionModal from './AddRestrictionModal'
- import AllowAllModal from './AllowAllModal'
- import DisallowAllModal from './DisallowAllModal'
- import RemoveRestrictionModal from './RemoveRestrictionModal'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { DocsButton } from '@/components/ui/DocsButton'
- import { useNetworkRestrictionsQuery } from '@/data/network-restrictions/network-restrictions-query'
- import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { DOCS_URL } from '@/lib/constants'
- interface AccessButtonProps {
- disabled: boolean
- onClick: (value: boolean) => void
- }
- const AllowAllAccessButton = ({ disabled, onClick }: AccessButtonProps) => (
- <Tooltip>
- <TooltipTrigger asChild>
- <Button type="default" disabled={disabled} onClick={() => onClick(true)}>
- Allow all access
- </Button>
- </TooltipTrigger>
- {disabled && (
- <TooltipContent side="bottom">
- You need additional permissions to update network restrictions
- </TooltipContent>
- )}
- </Tooltip>
- )
- const DisallowAllAccessButton = ({ disabled, onClick }: AccessButtonProps) => (
- <ButtonTooltip
- disabled={disabled}
- type="default"
- onClick={() => onClick(true)}
- tooltip={{
- content: {
- side: 'bottom',
- text: disabled
- ? 'You need additional permissions to update network restrictions'
- : undefined,
- },
- }}
- >
- Restrict all access
- </ButtonTooltip>
- )
- export const NetworkRestrictions = () => {
- const { ref } = useParams()
- const { data: project } = useSelectedProjectQuery()
- const [isAddingAddress, setIsAddingAddress] = useState<undefined | 'IPv4' | 'IPv6'>()
- const [isAllowingAll, setIsAllowingAll] = useState(false)
- const [isDisallowingAll, setIsDisallowingAll] = useState(false)
- const [selectedRestrictionToRemove, setSelectedRestrictionToRemove] = useState<string>()
- const { data, isPending: isLoading } = useNetworkRestrictionsQuery({ projectRef: ref })
- const { can: canUpdateNetworkRestrictions } = useAsyncCheckPermissions(
- PermissionAction.UPDATE,
- 'projects',
- {
- resource: {
- project_id: project?.id,
- },
- }
- )
- const hasAccessToRestrictions = data?.entitlement === 'allowed'
- const ipv4Restrictions = data?.config?.dbAllowedCidrs ?? []
- // @ts-ignore [Joshen] API typing issue
- const ipv6Restrictions = data?.config?.dbAllowedCidrsV6 ?? []
- const restrictedIps = ipv4Restrictions.concat(ipv6Restrictions)
- const restrictionStatus = data?.status ?? ''
- const hasApplyError = restrictionStatus === 'stored'
- const isUninitialized = restrictedIps.length === 0 && restrictionStatus.length === 0
- const isAllowedAll = restrictedIps.includes('0.0.0.0/0') && restrictedIps.includes('::/0')
- const isDisallowedAll = restrictedIps.length === 0
- if (!hasAccessToRestrictions) return null
- return (
- <>
- <PageSection id="network-restrictions">
- <PageSectionMeta>
- <PageSectionSummary>
- <PageSectionTitle>Network restrictions</PageSectionTitle>
- </PageSectionSummary>
- <PageSectionAside className="flex items-center gap-x-2">
- <DocsButton href={`${DOCS_URL}/guides/platform/network-restrictions`} />
- {!canUpdateNetworkRestrictions ? (
- <ButtonTooltip
- disabled
- type="primary"
- tooltip={{
- content: {
- side: 'bottom',
- text: 'You need additional permissions to update network restrictions',
- },
- }}
- >
- Add restriction
- </ButtonTooltip>
- ) : (
- <DropdownMenu>
- <DropdownMenuTrigger asChild>
- <Button
- type="primary"
- disabled={!canUpdateNetworkRestrictions}
- iconRight={<ChevronDown size={14} />}
- >
- Add restriction
- </Button>
- </DropdownMenuTrigger>
- <DropdownMenuContent align="end" side="bottom" className="w-48">
- <DropdownMenuItem
- key="IPv4"
- disabled={isLoading}
- onClick={() => setIsAddingAddress('IPv4')}
- >
- <p className="block text-foreground">Add IPv4 restriction</p>
- </DropdownMenuItem>
- <DropdownMenuItem
- key="IPv6"
- disabled={isLoading}
- onClick={() => setIsAddingAddress('IPv6')}
- >
- <p className="block text-foreground">Add IPv6 restriction</p>
- </DropdownMenuItem>
- </DropdownMenuContent>
- </DropdownMenu>
- )}
- </PageSectionAside>
- </PageSectionMeta>
- <PageSectionContent>
- {isLoading ? (
- <Card>
- <CardContent>
- <div className="space-y-2">
- <ShimmeringLoader />
- <ShimmeringLoader className="w-[70%]" />
- <ShimmeringLoader className="w-[50%]" />
- </div>
- </CardContent>
- </Card>
- ) : hasApplyError ? (
- <Card>
- <CardContent>
- <div className="flex items-center justify-between">
- <div className="space-y-2">
- <div className="flex items-center space-x-2">
- <AlertCircle size={20} strokeWidth={1.5} className="text-foreground-light" />
- <p className="text-sm">
- Your network restrictions were not applied correctly
- </p>
- </div>
- <p className="text-sm text-foreground-light">
- Please try to add your network restrictions again
- </p>
- </div>
- <div className="flex items-center space-x-2">
- <AllowAllAccessButton
- disabled={!canUpdateNetworkRestrictions}
- onClick={setIsAllowingAll}
- />
- <DisallowAllAccessButton
- disabled={!canUpdateNetworkRestrictions}
- onClick={setIsDisallowingAll}
- />
- </div>
- </div>
- </CardContent>
- </Card>
- ) : (
- <Card>
- {isUninitialized || isAllowedAll ? (
- <CardContent className="flex items-center justify-between">
- <div className="flex items-start space-x-4">
- <div className="space-y-0.5">
- <p className="text-foreground text-sm">
- Your database can be accessed by all IP addresses
- </p>
- <p className="text-foreground-light text-sm">
- You may start limiting access to your database by adding a network
- restriction.
- </p>
- </div>
- </div>
- <div>
- <DisallowAllAccessButton
- disabled={!canUpdateNetworkRestrictions}
- onClick={setIsDisallowingAll}
- />
- </div>
- </CardContent>
- ) : isDisallowedAll ? (
- <CardContent className="flex items-center justify-between">
- <div className="flex items-start space-x-4">
- <Lock size={20} className="text-foreground-light" strokeWidth={1.5} />
- <div className="space-y-1">
- <p className="text-foreground-light text-sm">
- Your database <span className="text-amber-900 opacity-80">cannot</span> be
- accessed externally
- </p>
- <p className="text-foreground-light text-sm">
- All external IP addresses have been disallowed from accessing your project's
- database.
- </p>
- <p className="text-foreground-light text-sm">
- Note: Restrictions only apply to your database, and not to Briven services
- </p>
- </div>
- </div>
- <div>
- <AllowAllAccessButton
- disabled={!canUpdateNetworkRestrictions}
- onClick={setIsAllowingAll}
- />
- </div>
- </CardContent>
- ) : (
- <>
- <CardHeader className="md:flex-row md:items-center justify-between">
- <CardDescription className="text-foreground-light">
- <p>Only the following IP addresses have access to your database.</p>
- <p>
- You may remove all of them to allow all IP addresses to have access to your
- database.
- </p>
- <p>
- Note: Restrictions only apply to your database, and not to Briven services
- </p>
- </CardDescription>
- <div className="flex items-center space-x-2">
- <AllowAllAccessButton
- disabled={!canUpdateNetworkRestrictions}
- onClick={setIsAllowingAll}
- />
- <DisallowAllAccessButton
- disabled={!canUpdateNetworkRestrictions}
- onClick={setIsDisallowingAll}
- />
- </div>
- </CardHeader>
- <CardContent className="py-0">
- <div className="divide-y">
- {restrictedIps.map((ip) => {
- return (
- <div key={ip} className="py-4 flex items-center justify-between">
- <div className="flex items-center space-x-5">
- <Globe size={16} className="text-foreground-lighter" />
- <Badge>{ipv4Restrictions.includes(ip) ? 'IPv4' : 'IPv6'}</Badge>
- <p className="text-sm font-mono">{ip}</p>
- </div>
- <Button
- type="default"
- onClick={() => setSelectedRestrictionToRemove(ip)}
- >
- Remove
- </Button>
- </div>
- )
- })}
- </div>
- </CardContent>
- </>
- )}
- </Card>
- )}
- </PageSectionContent>
- </PageSection>
- <AllowAllModal visible={isAllowingAll} onClose={() => setIsAllowingAll(false)} />
- <DisallowAllModal visible={isDisallowingAll} onClose={() => setIsDisallowingAll(false)} />
- <AddRestrictionModal
- type={isAddingAddress}
- hasOverachingRestriction={isAllowedAll || isDisallowedAll}
- onClose={() => setIsAddingAddress(undefined)}
- />
- <RemoveRestrictionModal
- visible={selectedRestrictionToRemove !== undefined}
- selectedRestriction={selectedRestrictionToRemove}
- onClose={() => setSelectedRestrictionToRemove(undefined)}
- />
- </>
- )
- }
|