Merge branch 'master' into chore/swr-to-react-query-1

This commit is contained in:
Alaister Young committed 2023-02-01 22:41:56 +11:00
commit 20aca85df5
28 files changed
+930 -32

No files matched your search

@@ -3,6 +3,7 @@ import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Fragment } from 'react'
import { Badge } from '~/../../packages/ui'
const home = [
[
@@ -94,17 +95,19 @@ const home = [
href: '/reference/dart/introduction',
level: 'reference_dart',
},
{
label: 'C#',
icon: '/img/icons/menu/reference-csharp',
href: '/reference/csharp/introduction',
level: 'reference_csharp',
},
{
label: 'Python',
icon: '/img/icons/menu/reference-python',
href: '/reference/python/introduction',
level: 'reference_python',
community: true,
},
{
label: 'C#',
icon: '/img/icons/menu/reference-csharp',
href: '/reference/csharp/introduction',
level: 'reference_csharp',
community: true,
},
{
label: 'Tools',
@@ -177,6 +180,7 @@ const NavigationMenuHome = ({ active }) => {
className="w-4 h-4 group-hover:scale-110 ease-out transition-all"
/>
{link.label}
{link.community && <Badge size="small">Community</Badge>}
</li>
</a>
</Link>
@@ -179,7 +179,6 @@ const Content: React.FC<INavigationMenuRefList> = ({ id, lib, commonSections, sp
if (!sections) console.error('no common sections imported')
const menu = NavItems[id]
//console.log(menu)
const databaseFunctions = find(sections, { title: 'Database' })
? find(sections, { title: 'Database' }).items
: []
+16 -5
View File
@@ -8,13 +8,24 @@ hideTitle: true
<img src="/docs/img/icons/menu/reference-csharp.svg" className="w-8 h-8 rounded" />
<div className="flex flex-col gap-2">
<h1 className="text-3xl text-scale-1200 m-0">C# Client Library</h1>
<h2 className="text-base font-mono text-scale-1100">supabase-csharp</h2>
<h2 className="text-base font-mono text-scale-1100">@supabase-community/supabase-csharp</h2>
</div>
</div>
<div className="max-w-xl">
This reference documents every object and method available in Supabase's C#
library, [supabase-csharp](https://www.nuget.org/packages/supabase-csharp). You can
use supabase-csharp to interact with your Postgres database, listen to database changes, invoke
Deno Edge Functions, build login and user management functionality, and manage large files.
This reference documents every object and method available in Supabase's C# library,
[supabase-csharp](https://www.nuget.org/packages/supabase-csharp). You can use supabase-csharp to
interact with your Postgres database, listen to database changes, invoke Deno Edge Functions,
build login and user management functionality, and manage large files.
</div>
<div className="max-w-xl bg-slate-300 px-4 py-2 rounded-md">
<p>The C# client library is created and maintained by the Supabase community, and is not an official library. Please be tolerant of areas where the library is still being developed, and — as with all the libraries — feel free to contribute wherever you find issues.</p>
<p>
Huge thanks to official maintainer, [Joseph Schultz](https://github.com/acupofjose), and to [Ben
Randall](https://github.com/veleek) and [Rhuan Barros](https://github.com/rhuanbarros) for their
help.
</p>
</div>
@@ -24,7 +24,7 @@ Let's start building the Svelte app from scratch.
### Initialize a Svelte app
We can use the [SvelteKit Skeleton Project](https://kit.svelte.dev/docs) to initialize
an app called `supabase-sveltekit` (for this tutorial you do not need TypeScript, ESLint, Prettier, or Playwright):
an app called `supabase-sveltekit` (for this tutorial we will be using TypeScript):
```bash
npm init svelte@next supabase-sveltekit
@@ -14,6 +14,15 @@ Custom Domains are currently in beta, and are being slowly made available to pro
Custom domains allow you to present a branded experience to your users. Custom domains are available as a [add-on for projects on a paid tier](https://app.supabase.com/project/_/settings/billing/update). Setting up a custom domain requires [Owner or Admin permissions](/docs/guides/platform/access-control#manage-team-members) for the project. Currently, you must use a subdomain (e.g., `api.example.com`, rather than `example.com`) for the purposes of this guide.
<div className="video-container">
<iframe
src="https://www.youtube-nocookie.com/embed/6rcGnW_Mh-0"
frameBorder="1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
></iframe>
</div>
## Configure a Custom Domain using the Supabase Dashboard
Follow the **Custom Domains** steps in the [General Settings](https://app.supabase.com/project/_/settings/general) page in the Dashboard to set up a custom domain for your project.
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

+45
View File
@@ -20305,6 +20305,23 @@
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
"dev": true
},
"node_modules/ip-address": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-8.1.0.tgz",
"integrity": "sha512-Wz91gZKpNKoXtqvY8ScarKYwhXoK4r/b5QuT+uywe/azv0/nUCo7Bh0IRRI7F9DHR06kJNWtzMGLIbXavngbKA==",
"dependencies": {
"jsbn": "1.1.0",
"sprintf-js": "1.1.2"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/ip-address/node_modules/sprintf-js": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
},
"node_modules/ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -22939,6 +22956,11 @@
"node": ">=4.0.0"
}
},
"node_modules/jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
},
"node_modules/jsdom": {
"version": "16.7.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
@@ -38330,6 +38352,7 @@
"generate-password": "^1.7.0",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"ip-address": "^8.1.0",
"json-logic-js": "^2.0.2",
"lodash": "^4.17.21",
"lottie-react": "^2.2.1",
@@ -54251,6 +54274,22 @@
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
"dev": true
},
"ip-address": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-8.1.0.tgz",
"integrity": "sha512-Wz91gZKpNKoXtqvY8ScarKYwhXoK4r/b5QuT+uywe/azv0/nUCo7Bh0IRRI7F9DHR06kJNWtzMGLIbXavngbKA==",
"requires": {
"jsbn": "1.1.0",
"sprintf-js": "1.1.2"
},
"dependencies": {
"sprintf-js": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz",
"integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug=="
}
}
},
"ipaddr.js": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
@@ -56342,6 +56381,11 @@
}
}
},
"jsbn": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="
},
"jsdom": {
"version": "16.7.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz",
@@ -64863,6 +64907,7 @@
"generate-password": "^1.7.0",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"ip-address": "^8.1.0",
"jest": "^27.4.0",
"jest-canvas-mock": "^2.3.1",
"json-logic-js": "^2.0.2",
+1 -1
View File
@@ -97,7 +97,7 @@ function Input({
// }, [errors, touched])
function onCopy(value: any) {
navigator.clipboard.writeText(value).then(
navigator.clipboard.writeText(value)?.then(
function () {
/* clipboard successfully set */
setCopyLabel('Copied')
+8 -3
View File
@@ -28,7 +28,8 @@
"reference_javascript_v1",
"reference_dart_v0",
"reference_dart_v1",
"reference_python_v2"
"reference_python_v2",
"reference_csharp_v0"
]
},
{
@@ -36,7 +37,12 @@
"id": "upgrade-guide",
"slug": "upgrade-guide",
"type": "markdown",
"excludes": ["reference_javascript_v2", "reference_dart_v1", "reference_python_v2"]
"excludes": [
"reference_javascript_v2",
"reference_dart_v1",
"reference_python_v2",
"reference_csharp_v0"
]
},
{
"title": "Database",
@@ -842,7 +848,6 @@
{
"title": "Misc",
"excludes": ["reference_dart_v1", "reference_dart_v0"],
"items": [
{
"title": "Release Notes",
@@ -15,7 +15,7 @@ import PublicationsTables from './Publications/PublicationsTables'
import BackupsList from './Backups/BackupsList'
import ConnectionPooling from './Pooling/ConnectionPooling'
import ConnectionPooling from '../Settings/Database/ConnectionPooling'
import CreateTrigger from './Triggers/CreateTrigger'
import DeleteTrigger from './Triggers/DeleteTrigger'
@@ -0,0 +1,244 @@
import { FC, useState } from 'react'
import { Address4 } from 'ip-address'
import { Button, Form, IconHelpCircle, Input, Modal } from 'ui'
import * as Tooltip from '@radix-ui/react-tooltip'
import { useStore, useParams } from 'hooks'
import InformationBox from 'components/ui/InformationBox'
import { checkIfPrivate, getAddressEndRange } from './NetworkRestrictions.utils'
import { useNetworkRestrictionsApplyMutation } from 'data/network-restrictions/network-retrictions-apply-mutation'
import { get } from 'lib/common/fetch'
interface Props {
restrictedIps: string[]
visible: boolean
hasOverachingRestriction: boolean
onClose: () => void
}
const AddRestrictionModal: FC<Props> = ({
restrictedIps,
visible,
hasOverachingRestriction,
onClose,
}) => {
const formId = 'add-restriction-form'
const { ui } = useStore()
const { ref } = useParams()
const { mutateAsync: applyNetworkRestrictions } = useNetworkRestrictionsApplyMutation()
const validate = (values: any) => {
const errors: any = {}
const { ipAddress, cidrBlockSize } = values
// Validate CIDR block size
const isOutOfCidrSizeRange = cidrBlockSize < 0 || cidrBlockSize > 32
if (isOutOfCidrSizeRange) errors.cidrBlockSize = 'Size has to be between 0 to 32'
// Validate IP address
const isValid = Address4.isValid(ipAddress)
if (!isValid) {
errors.ipAddress = 'Please enter a valid IP address'
return errors
}
const isPrivate = checkIfPrivate(ipAddress)
if (isPrivate) errors.ipAddress = 'Private IP addresses are not supported'
return errors
}
const onSubmit = async (values: any, { setSubmitting }: any) => {
if (!ref) return console.error('Project ref is required')
setSubmitting(true)
const cidr = `${values.ipAddress}/${values.cidrBlockSize}`
const alreadyExists = restrictedIps.includes(cidr)
if (alreadyExists) {
return ui.setNotification({
category: 'info',
message: `The address ${cidr} is already restricted`,
})
}
const dbAllowedCidrs = hasOverachingRestriction ? [cidr] : [...restrictedIps, cidr]
try {
await applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs })
onClose()
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to add restriction: ${error.message}`,
})
} finally {
setSubmitting(false)
}
}
return (
<Modal
closable
hideFooter
size="medium"
visible={visible}
onCancel={onClose}
header="Add a new restriction"
>
<Form
validateOnBlur
id={formId}
className="!border-t-0"
initialValues={{ ipAddress: '', cidrBlockSize: undefined }}
validate={validate}
onSubmit={onSubmit}
>
{({ isSubmitting, values, resetForm }: any) => {
const isPrivate = Address4.isValid(values.ipAddress)
? checkIfPrivate(values.ipAddress)
: false
const isValidBlockSize =
values.cidrBlockSize !== '' && values.cidrBlockSize >= 0 && values.cidrBlockSize <= 32
const availableAddresses = Math.pow(2, 32 - (values?.cidrBlockSize ?? 0))
const addressRange = getAddressEndRange(`${values.ipAddress}/${values.cidrBlockSize}`)
const isValidCIDR = isValidBlockSize && !isPrivate && addressRange !== undefined
const [isFetchingAddress, setIsFetchingAddress] = useState(false)
const getClientIpAddress = async () => {
setIsFetchingAddress(true)
const res = await fetch('http://www.geoplugin.net/json.gp', { method: 'GET' })
const { geoplugin_request } = await res.json()
if (geoplugin_request) {
const updatedValues = { ...values, ipAddress: geoplugin_request, cidrBlockSize: 32 }
resetForm({ initialValues: updatedValues, values: updatedValues })
} else {
ui.setNotification({
category: 'error',
duration: 4000,
message: 'Failed to retrieve client IP address, please enter address manually',
})
}
setIsFetchingAddress(false)
}
return (
<>
<Modal.Content>
<div className="py-6 space-y-4">
<p className="text-sm text-scale-1100">
This will add an IP address range to a list of allowed ranges that can access
your database. Only IPv4 addresses are supported at the moment.
</p>
<InformationBox
title="Note: Restrictions only apply to your database and PgBouncer"
description="They do not currently apply to Supabase services such as PostgREST, Storage, or Authentication"
/>
<div className="flex space-x-4">
<div className="w-[55%]">
<Input
label="IPv4 address"
id="ipAddress"
name="ipAddress"
placeholder="0.0.0.0"
className=""
/>
</div>
<div className="flex-grow">
<Input
label={
<div className="flex items-center space-x-2">
<p>CIDR Block Size</p>
<Tooltip.Root delayDuration={0}>
<Tooltip.Trigger>
<IconHelpCircle size="tiny" strokeWidth={2} />
</Tooltip.Trigger>
<Tooltip.Content side="bottom">
<Tooltip.Arrow className="radix-tooltip-arrow" />
<div
className={[
'rounded bg-scale-100 py-1 px-2 leading-none shadow',
'border border-scale-200 w-[300px]',
].join(' ')}
>
<span className="text-xs text-scale-1200">
Classless inter-domain routing (CIDR) notation is the notation
used to identify networks and hosts in the networks. The block
size tells us how many bits we need to take for the network
prefix, and is a value between 0 to 32.
</span>
</div>
</Tooltip.Content>
</Tooltip.Root>
</div>
}
id="cidrBlockSize"
name="cidrBlockSize"
type="number"
placeholder="0"
min={0}
max={32}
/>
</div>
</div>
<Button
type="default"
loading={isFetchingAddress}
disabled={isFetchingAddress}
onClick={() => getClientIpAddress()}
>
Use my IP address
</Button>
</div>
</Modal.Content>
<Modal.Separator />
{isValidCIDR ? (
<Modal.Content>
<div className="space-y-1 pt-2 pb-4">
<p className="text-sm">
The address range{' '}
<code className="text-xs">
{values.ipAddress}/{values.cidrBlockSize}
</code>{' '}
will be restricted
</p>
<p className="text-sm text-scale-1000">
Selected address space: <code className="text-xs">{addressRange.start}</code>{' '}
to <code className="text-xs">{addressRange.end}</code>{' '}
</p>
<p className="text-sm text-scale-1000">
Number of addresses: {availableAddresses}
</p>
</div>
</Modal.Content>
) : (
<Modal.Content>
<div className="pt-2 pb-4">
<div className="h-[68px] flex items-center">
<p className="text-sm text-scale-1000">
A summary of your restriction will be shown here after entering a valid IP
address and CIDR block size
</p>
</div>
</div>
</Modal.Content>
)}
<div className="flex items-center justify-end px-6 py-4 border-t space-x-2">
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
Cancel
</Button>
<Button htmlType="submit" loading={isSubmitting} disabled={isSubmitting}>
Save restriction
</Button>
</div>
</>
)
}}
</Form>
</Modal>
)
}
export default AddRestrictionModal
@@ -0,0 +1,63 @@
import { FC, useState } from 'react'
import { Modal, Button } from 'ui'
import { useStore, useParams } from 'hooks'
import { useNetworkRestrictionsApplyMutation } from 'data/network-restrictions/network-retrictions-apply-mutation'
interface Props {
visible: boolean
onClose: () => void
}
const AllowAllModal: FC<Props> = ({ visible, onClose }) => {
const { ui } = useStore()
const { ref } = useParams()
const [isSubmitting, setIsSubmitting] = useState(false)
const { mutateAsync: applyNetworkRestrictions } = useNetworkRestrictionsApplyMutation()
const onSubmit = async () => {
if (!ref) return console.error('Project ref is required')
setIsSubmitting(true)
try {
await applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs: ['0.0.0.0/0'] })
onClose()
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to update restriction: ${error.message}`,
})
} finally {
setIsSubmitting(false)
}
}
return (
<Modal
closable
hideFooter
size="small"
visible={visible}
onCancel={onClose}
header="Allow access from all IP addresses"
>
<Modal.Content>
<div className="py-6 space-y-4">
<p className="text-sm text-scale-1100">
This will allow any IP address to access your project's database. Are you sure?
</p>
</div>
</Modal.Content>
<div className="flex items-center justify-end px-6 py-4 border-t space-x-2">
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
Cancel
</Button>
<Button loading={isSubmitting} disabled={isSubmitting} onClick={() => onSubmit()}>
Confirm
</Button>
</div>
</Modal>
)
}
export default AllowAllModal
@@ -0,0 +1,71 @@
import { FC, useState } from 'react'
import { Modal, Button } from 'ui'
import { useStore, useParams } from 'hooks'
import { useNetworkRestrictionsApplyMutation } from 'data/network-restrictions/network-retrictions-apply-mutation'
import InformationBox from 'components/ui/InformationBox'
interface Props {
visible: boolean
onClose: () => void
}
const DisallowAllModal: FC<Props> = ({ visible, onClose }) => {
const { ui } = useStore()
const { ref } = useParams()
const [isSubmitting, setIsSubmitting] = useState(false)
const { mutateAsync: applyNetworkRestrictions } = useNetworkRestrictionsApplyMutation()
const onSubmit = async () => {
if (!ref) return console.error('Project ref is required')
setIsSubmitting(true)
try {
await applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs: ['127.0.0.1/32'] })
onClose()
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to update restriction: ${error.message}`,
})
} finally {
setIsSubmitting(false)
}
}
return (
<Modal
closable
hideFooter
size="medium"
visible={visible}
onCancel={onClose}
header="Restrict access from all IP addresses"
>
<Modal.Content>
<div className="py-6 space-y-4">
<p className="text-sm text-scale-1100">
This will prevent any external IP addresses from accessing your project's database. Are
you sure?
</p>
<InformationBox
defaultVisibility
hideCollapse
title="Note: Restrictions only apply to your database and PgBouncer"
description="They do not currently apply to APIs offered over HTTPS, such as PostgREST, Storage, or Authentication"
/>
</div>
</Modal.Content>
<div className="flex items-center justify-end px-6 py-4 border-t space-x-2">
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
Cancel
</Button>
<Button loading={isSubmitting} disabled={isSubmitting} onClick={() => onSubmit()}>
Confirm
</Button>
</div>
</Modal>
)
}
export default DisallowAllModal
@@ -0,0 +1,195 @@
import Link from 'next/link'
import { useState } from 'react'
import { Button, IconAlertCircle, IconExternalLink, IconGlobe, IconLock } from 'ui'
import { useParams } from 'hooks'
import Panel from 'components/ui/Panel'
import { FormPanel, FormHeader } from 'components/ui/Forms'
import ShimmeringLoader from 'components/ui/ShimmeringLoader'
import AddRestrictionModal from './AddRestrictionModal'
import RemoveRestrictionModal from './RemoveRestrictionModal'
import DisallowAllModal from './DisallowAllModal'
import AllowAllModal from './AllowAllModal'
import { useNetworkRestrictionsQuery } from 'data/network-restrictions/network-restrictions-query'
const NetworkRestrictions = ({}) => {
const { ref } = useParams()
const [isAddingAddress, setIsAddingAddress] = useState(false)
const [isAllowingAll, setIsAllowingAll] = useState(false)
const [isDisallowingAll, setIsDisallowingAll] = useState(false)
const [selectedRestrictionToRemove, setSelectedRestrictionToRemove] = useState<string>()
const { data, isLoading } = useNetworkRestrictionsQuery({ projectRef: ref })
const hasAccessToRestrictions = data?.entitlement === 'allowed'
const restrictedIps = data?.config.dbAllowedCidrs ?? []
const restrictionStatus = data?.status ?? ''
const hasApplyError = restrictedIps.length === 0 && restrictionStatus === 'stored'
const isUninitialized = restrictedIps.length === 0 && restrictionStatus.length === 0
const isAllowedAll = restrictedIps.includes('0.0.0.0/0')
const isDisallowedAll = restrictedIps.length === 1 && restrictedIps[0] === '127.0.0.1/32'
if (!hasAccessToRestrictions) return <></>
return (
<>
<section>
<div className="flex items-center justify-between">
<FormHeader
title="Network Restrictions"
description="Allow specific IP ranges to have access to your database."
/>
<div className="flex items-center space-x-2">
<Link href="https://supabase.com/docs/guides/platform/network-restrictions">
<a target="_blank">
<Button type="default" icon={<IconExternalLink />}>
Documentation
</Button>
</a>
</Link>
<Button onClick={() => setIsAddingAddress(true)}>Add restriction</Button>
</div>
</div>
{isLoading ? (
<Panel>
<Panel.Content>
<div className="space-y-2">
<ShimmeringLoader />
<ShimmeringLoader className="w-[70%]" />
<ShimmeringLoader className="w-[50%]" />
</div>
</Panel.Content>
</Panel>
) : hasApplyError ? (
<Panel>
<Panel.Content>
<div className="flex items-center justify-between">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<IconAlertCircle strokeWidth={1.5} className="text-scale-1000" />
<p className="text-sm">Your network restrictions were not applied correctly</p>
</div>
<p className="text-sm text-scale-1000">
Please try to add your network restrictions again
</p>
</div>
<div className="flex items-center space-x-2">
<Button type="default" onClick={() => setIsAllowingAll(true)}>
Allow all access
</Button>
<Button type="default" onClick={() => setIsDisallowingAll(true)}>
Restrict all access
</Button>
</div>
</div>
</Panel.Content>
</Panel>
) : (
<FormPanel>
{isUninitialized || isAllowedAll ? (
<div className="px-8 py-8 flex items-center justify-between">
<div className="flex items-start space-x-4">
<div className="space-y-1">
<p className="text-scale-1100 text-sm">
Your database can be accessed by all IP addresses
</p>
<p className="text-scale-1000 text-sm">
You may start limiting access to your database by adding a network
restriction.
</p>
</div>
</div>
<div>
<Button type="default" onClick={() => setIsDisallowingAll(true)}>
Restrict all access
</Button>
</div>
</div>
) : isDisallowedAll ? (
<div className="px-8 py-8 flex items-center justify-between">
<div className="flex items-start space-x-4">
<IconLock className="text-scale-1100" strokeWidth={1.5} />
<div className="space-y-1">
<p className="text-scale-1100 text-sm">
Your database <span className="text-amber-900 opacity-80">cannot</span> be
accessed externally
</p>
<p className="text-scale-1000 text-sm">
All external IP addresses have been disallowed from accessing your project's
database.
</p>
<p className="text-scale-1000 text-sm">
Note: Restrictions only apply to your database, and not to Supabase services
</p>
</div>
</div>
<div>
<Button type="default" onClick={() => setIsAllowingAll(true)}>
Allow all access
</Button>
</div>
</div>
) : (
<div className="divide-y">
<div className="px-8 py-3 flex items-center justify-between">
<div>
<p className="text-scale-1100 text-sm">
Only the following IP addresses have access to your database
</p>
<p className="text-scale-1000 text-sm">
You may remove all of them to allow all IP addresses to have access to your
database
</p>
<p className="text-scale-1000 text-sm">
Note: Restrictions only apply to your database, and not to Supabase services
</p>
</div>
<div className="flex items-center space-x-2">
<Button type="default" onClick={() => setIsAllowingAll(true)}>
Allow all access
</Button>
<Button type="default" onClick={() => setIsDisallowingAll(true)}>
Restrict all access
</Button>
</div>
</div>
{restrictedIps.map((ip) => {
return (
<div key={ip} className="px-8 py-4 flex items-center justify-between">
<div className="flex items-center space-x-5">
<IconGlobe size={16} className="text-scale-1000" />
<p className="text-sm font-mono">{ip}</p>
</div>
<Button type="default" onClick={() => setSelectedRestrictionToRemove(ip)}>
Remove
</Button>
</div>
)
})}
</div>
)}
</FormPanel>
)}
</section>
<AllowAllModal visible={isAllowingAll} onClose={() => setIsAllowingAll(false)} />
<DisallowAllModal visible={isDisallowingAll} onClose={() => setIsDisallowingAll(false)} />
<AddRestrictionModal
visible={isAddingAddress}
restrictedIps={restrictedIps}
hasOverachingRestriction={isAllowedAll || isDisallowedAll}
onClose={() => setIsAddingAddress(false)}
/>
<RemoveRestrictionModal
visible={selectedRestrictionToRemove !== undefined}
restrictedIps={restrictedIps}
selectedRestriction={selectedRestrictionToRemove}
onClose={() => setSelectedRestrictionToRemove(undefined)}
/>
</>
)
}
export default NetworkRestrictions
@@ -0,0 +1,22 @@
import { Address4 } from 'ip-address'
const privateIPRanges = [
new Address4('10.0.0.0/8'),
new Address4('172.16.0.0/12'),
new Address4('192.168.0.0/16'),
]
export const checkIfPrivate = (cidr: string) => {
const address = new Address4(cidr)
const res = privateIPRanges.map((range) => address.isInSubnet(range))
return res.includes(true)
}
export const getAddressEndRange = (cidr: string) => {
try {
const address = new Address4(cidr)
return { start: address.startAddress().address, end: address.endAddress().address }
} catch (error: any) {
return undefined
}
}
@@ -0,0 +1,89 @@
import { FC, useState } from 'react'
import { Alert, Button, Form, Input, Modal } from 'ui'
import { useStore, useParams } from 'hooks'
import { useNetworkRestrictionsApplyMutation } from 'data/network-restrictions/network-retrictions-apply-mutation'
interface Props {
visible: boolean
restrictedIps: string[]
selectedRestriction?: string
onClose: () => void
}
const RemoveRestrictionModal: FC<Props> = ({
visible,
restrictedIps,
selectedRestriction,
onClose,
}) => {
const { ui } = useStore()
const { ref } = useParams()
const [isSubmitting, setIsSubmitting] = useState(false)
const { mutateAsync: applyNetworkRestrictions } = useNetworkRestrictionsApplyMutation()
const isRemovingOnlyRestriction =
restrictedIps.length === 1 && restrictedIps[0] === selectedRestriction
const onSubmit = async () => {
if (!ref) return console.error('Project ref is required')
setIsSubmitting(true)
const dbAllowedCidrs = restrictedIps.filter((ip) => ip !== selectedRestriction)
try {
if (dbAllowedCidrs.length === 0) {
await applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs: ['0.0.0.0/0'] })
} else {
await applyNetworkRestrictions({ projectRef: ref, dbAllowedCidrs })
}
onClose()
} catch (error: any) {
ui.setNotification({
category: 'error',
message: `Failed to remove restriction: ${error.message}`,
})
} finally {
setIsSubmitting(false)
}
}
return (
<Modal
closable
hideFooter
size="medium"
visible={visible}
onCancel={onClose}
header="Confirm to remove restriction"
>
<Modal.Content>
<div className="py-6 space-y-4">
<p className="text-sm text-scale-1100">
The IPv4 address <code className="text-xs">{selectedRestriction}</code> will be removed
from your list of network restrictions
{isRemovingOnlyRestriction
? '.'
: ", and no longer have access to your project's database."}
</p>
{isRemovingOnlyRestriction && (
<Alert withIcon variant="warning" title="Database access will no longer be restricted">
Removing all network restrictions will default to your database being accessible from
all IP addresses.
</Alert>
)}
</div>
</Modal.Content>
<div className="flex items-center justify-end px-6 py-4 border-t space-x-2">
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
Cancel
</Button>
<Button loading={isSubmitting} disabled={isSubmitting} onClick={() => onSubmit()}>
Remove restriction
</Button>
</div>
</Modal>
)
}
export default RemoveRestrictionModal
@@ -0,0 +1,4 @@
import ConnectionPooling from './ConnectionPooling'
import NetworkRestrictions from './NetworkRestrictions/NetworkRestrictions'
export { ConnectionPooling, NetworkRestrictions }
@@ -1,6 +1,7 @@
import General from './General'
import Infrastructure from './Infrastructure/Infrastructure'
import CustomDomainConfig from './CustomDomainConfig/CustomDomainConfig'
import DeleteProjectPanel from './DeleteProjectPanel/DeleteProjectPanel'
import DeleteProjectButton from './DeleteProjectPanel/DeleteProjectButton'
export { General, Infrastructure, DeleteProjectPanel, DeleteProjectButton }
export { General, Infrastructure, CustomDomainConfig, DeleteProjectPanel, DeleteProjectButton }
+4
View File
@@ -0,0 +1,4 @@
export const networkRestrictionKeys = {
list: (projectRef: string | undefined) =>
['projects', projectRef, 'network-restrictions'] as const,
}
@@ -0,0 +1,75 @@
import { useCallback } from 'react'
import { useQuery, useQueryClient, UseQueryOptions } from '@tanstack/react-query'
import { networkRestrictionKeys } from './keys'
import { get } from 'lib/common/fetch'
import { API_ADMIN_URL } from 'lib/constants'
export type NetworkRestrictionsVariables = { projectRef?: string }
export type NetworkRestrictionsResponse = {
entitlement: 'disallowed' | 'allowed'
status: '' | 'stored' | 'applied'
config: { dbAllowedCidrs: string[] }
old_config?: { dbAllowedCidrs: string[] }
error?: any
}
export async function getNetworkRestrictions(
{ projectRef }: NetworkRestrictionsVariables,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
const response = (await get(`${API_ADMIN_URL}/projects/${projectRef}/network-restrictions`, {
signal,
})) as NetworkRestrictionsResponse
// Not allowed error is a valid response to denote if a project
// has access to the network restrictions UI, so we'll handle it here
if (response.error) {
const isNotAllowedError =
(response.error as any)?.code === 400 &&
(response.error as any)?.message?.includes('not allowed to set up network restrictions')
if (isNotAllowedError) {
return {
entitlement: 'disallowed',
config: { dbAllowedCidrs: [] },
status: '',
} as NetworkRestrictionsResponse
} else {
throw response.error
}
}
return response
}
export type NetworkRestrictionsData = Awaited<ReturnType<typeof getNetworkRestrictions>>
export type NetworkRestrictionsError = unknown
export const useNetworkRestrictionsQuery = <TData = NetworkRestrictionsData>(
{ projectRef }: NetworkRestrictionsVariables,
{
enabled = true,
...options
}: UseQueryOptions<NetworkRestrictionsData, NetworkRestrictionsError, TData> = {}
) =>
useQuery<NetworkRestrictionsData, NetworkRestrictionsError, TData>(
networkRestrictionKeys.list(projectRef),
({ signal }) => getNetworkRestrictions({ projectRef }, signal),
{ enabled: enabled && typeof projectRef !== 'undefined', ...options }
)
export const useNetworkRestrictionsPrefetch = ({ projectRef }: NetworkRestrictionsVariables) => {
const client = useQueryClient()
return useCallback(() => {
if (projectRef) {
client.prefetchQuery(networkRestrictionKeys.list(projectRef), ({ signal }) =>
getNetworkRestrictions({ projectRef }, signal)
)
}
}, [projectRef])
}
@@ -0,0 +1,48 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { post } from 'lib/common/fetch'
import { API_ADMIN_URL } from 'lib/constants'
import { networkRestrictionKeys } from './keys'
export type NetworkRestrictionsApplyVariables = {
projectRef: string
dbAllowedCidrs: string[]
}
export async function applyNetworkRestrictions({
projectRef,
dbAllowedCidrs,
}: NetworkRestrictionsApplyVariables) {
if (!projectRef) throw new Error('projectRef is required')
const response = await post(
`${API_ADMIN_URL}/projects/${projectRef}/network-restrictions/apply`,
{ dbAllowedCidrs }
)
if (response.error) throw response.error
return response
}
type NetworkRestrictionsApplyData = Awaited<ReturnType<typeof applyNetworkRestrictions>>
export const useNetworkRestrictionsApplyMutation = ({
onSuccess,
...options
}: Omit<
UseMutationOptions<NetworkRestrictionsApplyData, unknown, NetworkRestrictionsApplyVariables>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<NetworkRestrictionsApplyData, unknown, NetworkRestrictionsApplyVariables>(
(vars) => applyNetworkRestrictions(vars),
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(networkRestrictionKeys.list(projectRef))
await onSuccess?.(data, variables, context)
},
...options,
}
)
}
@@ -1485,7 +1485,7 @@ class StorageExplorerStore {
.list(formattedPathToFolder, options)
folderContents = folderContents.concat(data)
options.offset += options.limit
if (data.length < options.limit) {
if ((data || []).length < options.limit) {
break
}
}
+1
View File
@@ -45,6 +45,7 @@
"generate-password": "^1.7.0",
"html-to-image": "^1.10.8",
"immutability-helper": "^3.1.1",
"ip-address": "^8.1.0",
"json-logic-js": "^2.0.2",
"lodash": "^4.17.21",
"lottie-react": "^2.2.1",
@@ -1,6 +1,6 @@
import { toJS } from 'mobx'
import * as Tooltip from '@radix-ui/react-tooltip'
import { FC, useState, useRef, useEffect } from 'react'
import { toJS } from 'mobx'
import { observer } from 'mobx-react-lite'
import { debounce } from 'lodash'
import generator from 'generate-password'
@@ -8,7 +8,7 @@ import { Input, Button, IconDownload, Tabs, Modal } from 'ui'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { NextPageWithLayout } from 'types'
import { checkPermissions, useParams, useStore } from 'hooks'
import { checkPermissions, useFlag, useParams, useStore } from 'hooks'
import { patch } from 'lib/common/fetch'
import { pluckObjectFields, passwordStrength } from 'lib/helpers'
import { API_URL, DEFAULT_MINIMUM_PASSWORD_STRENGTH } from 'lib/constants'
@@ -17,16 +17,19 @@ import { useProjectSettingsQuery } from 'data/config/project-settings-query'
import { SettingsLayout } from 'components/layouts'
import PasswordStrengthBar from 'components/ui/PasswordStrengthBar'
import Panel from 'components/ui/Panel'
import ConnectionPooling from 'components/interfaces/Database/Pooling/ConnectionPooling'
import { ConnectionPooling, NetworkRestrictions } from 'components/interfaces/Settings/Database'
const ProjectSettings: NextPageWithLayout = () => {
const { ref: projectRef } = useParams()
const networkRestrictions = useFlag('networkRestrictions')
return (
<div className="1xl:px-28 mx-auto flex flex-col gap-4 px-5 py-6 lg:px-16 xl:px-24 2xl:px-32">
<div className="1xl:px-28 mx-auto flex flex-col gap-4 px-5 pt-6 pb-14 lg:px-16 xl:px-24 2xl:px-32">
<div className="content h-full w-full overflow-y-auto">
<GeneralSettings projectRef={projectRef} />
<ConnectionPooling />
{networkRestrictions && <NetworkRestrictions />}
</div>
</div>
)
@@ -1,12 +1,18 @@
import { observer } from 'mobx-react-lite'
import { NextPageWithLayout } from 'types'
import { SettingsLayout } from 'components/layouts'
import { General, Infrastructure, DeleteProjectPanel } from 'components/interfaces/Settings/General'
import CustomDomainConfig from 'components/interfaces/Settings/General/CustomDomainConfig/CustomDomainConfig'
import {
General,
Infrastructure,
CustomDomainConfig,
DeleteProjectPanel,
} from 'components/interfaces/Settings/General'
const ProjectSettings: NextPageWithLayout = () => {
// [Joshen] Opting for larger gap instead of gap-8 as compared to other pages for better grouping of content
return (
<div className="1xl:px-28 mx-auto flex flex-col gap-8 px-5 py-6 lg:px-16 xl:px-24 2xl:px-32 ">
<div className="1xl:px-28 mx-auto flex flex-col gap-10 px-5 py-6 lg:px-16 xl:px-24 2xl:px-32 ">
<General />
<Infrastructure />
<CustomDomainConfig />
+3 -4
View File
@@ -10,8 +10,7 @@ import Divider from 'components/ui/Divider'
import { Dictionary } from 'components/grid'
import { useStore, withAuth } from 'hooks'
import { API_URL, IS_PLATFORM } from 'lib/constants'
import { PROJECT_ENDPOINT_PROTOCOL } from 'pages/api/constants'
import { API_URL } from 'lib/constants'
import { get } from 'lib/common/fetch'
import {
VERCEL_INTEGRATION_CONFIGS,
@@ -370,8 +369,8 @@ const ProjectLinks: FC = observer(() => {
// Then create env for vercel project with supabase project
const vercelEnvs = prepareVercelEvns(defaultVercelEnvs, {
endpoint: `${projectDetails.apiService.protocol ?? 'https'}://${
projectDetails.apiService.endpoint ?? '-'
endpoint: `${projectDetails.autoApiService.protocol ?? 'https'}://${
projectDetails.autoApiService.endpoint ?? '-'
}`,
anon_key: projectDetails.autoApiService.defaultApiKey,
service_key: projectDetails.autoApiService.serviceApiKey,
+1 -1
View File
@@ -38,7 +38,7 @@ Sentry.init({
// the URL params instead of the store, but we cannot repro locally, staging nor on prod
// Safe to ignore since it's not a user-facing issue + we've not received any user feedback/report about it
// Ref: https://github.com/supabase/supabase/pull/9729
'The provided `href` (/org/[slug]/settings) value is missing query values (slug)',
'The provided `href` (/org/[slug]/general) value is missing query values (slug)',
],
beforeSend: (event) => filterConsoleErrors(event),
})