import { zodResolver } from '@hookform/resolvers/zod' import { PermissionAction } from '@supabase/shared-types/out/constants' import { indexOf } from 'lodash' import { Lock } from 'lucide-react' import Link from 'next/link' import { useEffect, useState } from 'react' import { useForm } from 'react-hook-form' import { toast } from 'sonner' import { z } from 'zod' import { useParams } from 'common' import { DocsButton } from 'components/ui/DocsButton' import { FormActions } from 'components/ui/Forms/FormActions' import { useProjectPostgrestConfigQuery } from 'data/config/project-postgrest-config-query' import { useProjectPostgrestConfigUpdateMutation } from 'data/config/project-postgrest-config-update-mutation' import { useDatabaseExtensionsQuery } from 'data/database-extensions/database-extensions-query' import { useSchemasQuery } from 'data/database/schemas-query' import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' import { DOCS_URL } from 'lib/constants' import { AlertDescription_Shadcn_, AlertTitle_Shadcn_, Alert_Shadcn_, Button, Card, CardContent, CardFooter, CardHeader, CollapsibleContent_Shadcn_, Collapsible_Shadcn_, FormControl_Shadcn_, FormField_Shadcn_, FormItem_Shadcn_, Form_Shadcn_, Input_Shadcn_, PrePostTab, Skeleton, Switch, WarningIcon, } from 'ui' import { GenericSkeletonLoader } from 'ui-patterns' import { Admonition } from 'ui-patterns/admonition' import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' import { MultiSelector, MultiSelectorContent, MultiSelectorItem, MultiSelectorList, MultiSelectorTrigger, } from 'ui-patterns/multi-select' import { HardenAPIModal } from './HardenAPIModal' const formSchema = z .object({ dbSchema: z.array(z.string()), dbExtraSearchPath: z.array(z.string()), maxRows: z.number().max(1000000, "Can't be more than 1,000,000"), dbPool: z .number() .min(0, 'Must be more than 0') .max(1000, "Can't be more than 1000") .optional() .nullable(), enableDataApi: z.boolean(), }) .refine( (data) => { if (data.enableDataApi && data.dbSchema.length === 0) { return false } return true }, { message: 'Must have at least one schema if Data API is enabled', path: ['dbSchema'], } ) export const PostgrestConfig = () => { const { ref: projectRef } = useParams() const { data: project } = useSelectedProjectQuery() const [showModal, setShowModal] = useState(false) const { data: config, isError, isPending: isLoadingConfig, } = useProjectPostgrestConfigQuery({ projectRef }) const { data: extensions } = useDatabaseExtensionsQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const { data: allSchemas = [], isPending: isLoadingSchemas, isSuccess: isSuccessSchemas, } = useSchemasQuery({ projectRef: project?.ref, connectionString: project?.connectionString, }) const isLoading = isLoadingConfig || isLoadingSchemas const { mutate: updatePostgrestConfig, isPending: isUpdating } = useProjectPostgrestConfigUpdateMutation({ onSuccess: () => { toast.success('Successfully saved settings') }, }) const formId = 'project-postgres-config' const hiddenSchema = [ 'auth', 'pgbouncer', 'hooks', 'extensions', 'vault', 'storage', 'realtime', 'pgsodium', 'pgsodium_masks', ] const { can: canUpdatePostgrestConfig, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions(PermissionAction.UPDATE, 'custom_config_postgrest') const isGraphqlExtensionEnabled = (extensions ?? []).find((ext) => ext.name === 'pg_graphql')?.installed_version !== null const dbSchema = config?.db_schema ? config?.db_schema.replace(/ /g, '').split(',') : [] const defaultValues = { dbSchema, maxRows: config?.max_rows, dbExtraSearchPath: (config?.db_extra_search_path ?? '') .split(',') .map((x) => x.trim()) .filter((x) => x.length > 0 && allSchemas.find((y) => y.name === x)), dbPool: config?.db_pool, } const form = useForm>({ resolver: zodResolver(formSchema), mode: 'onChange', defaultValues, }) const schemas = allSchemas .filter((x) => { const find = indexOf(hiddenSchema, x.name) if (find < 0) return x }) .map((x) => { return { id: x.id, value: x.name, name: x.name, disabled: false, } }) ?? [] function resetForm() { const enableDataApi = config?.db_schema ? true : false form.reset({ ...defaultValues, enableDataApi }) } const onSubmit = async (values: z.infer) => { if (!projectRef) return console.error('Project ref is required') // is this needed ? updatePostgrestConfig({ projectRef, dbSchema: values.dbSchema.join(', '), maxRows: values.maxRows, dbExtraSearchPath: values.dbExtraSearchPath.join(','), dbPool: values.dbPool ? values.dbPool : null, }) } useEffect(() => { if (config && isSuccessSchemas) { /** * Checks if enableDataApi should be enabled or disabled * based on the db_schema value being empty string */ resetForm() } }, [config, isSuccessSchemas]) const isDataApiEnabledInForm = form.getValues('enableDataApi') return ( Data API Settings
{isLoading ? ( ) : isError ? ( ) : ( <> ( { field.onChange(value) if (!value) { form.setValue('enableDataApi', false) form.setValue('dbSchema', []) } else { form.setValue('enableDataApi', true) form.setValue('dbSchema', dbSchema) } }} /> {!field.value && ( No schemas can be queried

With this setting disabled, you will not be able to query any schemas via the Data API.

You will see errors from the Postgrest endpoint /rest/v1/.

)}
)} />
( {isLoadingSchemas ? (
) : ( {schemas.length <= 0 ? ( no ) : ( schemas.map((x) => ( {x.name} )) )} )} {!field.value.includes('public') && field.value.length > 0 && (

You will not be able to query tables and views in the{' '} public schema via supabase-js or HTTP clients.

{isGraphqlExtensionEnabled && ( <>

Tables in the{' '} public schema are still exposed over our GraphQL endpoints.

)} } /> )}
)} />
( {isLoadingSchemas ? (
) : ( {allSchemas.length <= 0 ? ( no ) : ( allSchemas.map((x) => ( {x.name} )) )} )}
)} />
( field.onChange(Number(e.target.value))} /> )} /> ( field.onChange( e.target.value === '' ? null : Number(e.target.value) ) } value={field.value === null ? '' : field.value} /> )} />
)}
setShowModal(false)} />
) }