| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- import { Check, ChevronDown } from 'lucide-react'
- import { toast } from 'sonner'
- import {
- Alert,
- AlertDescription,
- AlertTitle,
- Collapsible,
- CollapsibleContent,
- CollapsibleTrigger,
- Dialog,
- DialogContent,
- DialogDescription,
- DialogHeader,
- DialogSection,
- DialogSectionSeparator,
- DialogTitle,
- WarningIcon,
- } from 'ui'
- import { CodeBlock } from 'ui-patterns/CodeBlock'
- import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
- import { DocsButton } from '@/components/ui/DocsButton'
- import InformationBox from '@/components/ui/InformationBox'
- import { useCreateAndExposeAPISchemaMutation } from '@/data/api-settings/create-and-expose-api-schema-mutation'
- import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
- import { useProjectPostgrestConfigUpdateMutation } from '@/data/config/project-postgrest-config-update-mutation'
- import { useSchemasQuery } from '@/data/database/schemas-query'
- import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
- import { DOCS_URL } from '@/lib/constants'
- interface HardenAPIModalProps {
- visible: boolean
- onClose: () => void
- }
- export const HardenAPIModal = ({ visible, onClose }: HardenAPIModalProps) => {
- const { data: project } = useSelectedProjectQuery()
- const { data: schemas } = useSchemasQuery({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- })
- const { data: config } = useProjectPostgrestConfigQuery({ projectRef: project?.ref })
- const hasAPISchema = (schemas ?? []).find((schema) => schema.name === 'api')
- const exposedSchemas = config?.db_schema.split(',').map((x) => x.trim()) ?? []
- const isAPISchemaExposed = exposedSchemas.includes('api')
- const isPublicSchemaExposed = exposedSchemas.includes('public')
- const { mutate: createAndExposeAPISchema, isPending: isCreatingAPISchema } =
- useCreateAndExposeAPISchemaMutation({
- onSuccess: () => {
- toast.success(`Successfully created api schema and exposed via Data API`)
- },
- })
- const { mutate: updatePostgrestConfig, isPending: isUpdatingConfig } =
- useProjectPostgrestConfigUpdateMutation({
- onSuccess: () => {
- toast.success('Success removed public schema from exposed schemas')
- },
- })
- const onSelectCreateAndExposeAPISchema = () => {
- if (project === undefined) return console.error('Project is required')
- if (config === undefined) return console.error('Postgrest config is required')
- createAndExposeAPISchema({
- projectRef: project?.ref,
- connectionString: project?.connectionString,
- existingPostgrestConfig: {
- max_rows: config.max_rows,
- db_pool: config.db_pool,
- db_schema: config.db_schema,
- db_extra_search_path: config?.db_extra_search_path,
- },
- })
- }
- const onSelectRemovePublicSchema = () => {
- if (project === undefined) return console.error('Project is required')
- if (config === undefined) return console.error('Postgrest config is required')
- const updatedDbExtraSearchPath = config.db_extra_search_path
- .split(',')
- .map((x) => x.trim())
- .filter((x) => x !== 'public')
- .join(', ')
- const updatedDbSchema = config.db_schema
- .split(',')
- .map((x) => x.trim())
- .filter((x) => x !== 'public')
- .join(', ')
- updatePostgrestConfig({
- projectRef: project.ref,
- maxRows: config.max_rows,
- dbPool: config.db_pool,
- dbSchema: updatedDbSchema,
- dbExtraSearchPath: updatedDbExtraSearchPath,
- })
- }
- return (
- <Dialog open={visible} onOpenChange={onClose}>
- <DialogContent size="large">
- <DialogHeader>
- <DialogTitle>Switch the default API schema</DialogTitle>
- <DialogDescription>
- Expose a custom schema instead of the <code className="text-code-inline">public</code>{' '}
- schema
- </DialogDescription>
- </DialogHeader>
- <DialogSectionSeparator />
- <DialogSection className="text-sm text-foreground-light">
- <p>
- By default, the <code className="text-code-inline">public</code> schema is used to
- generate API routes. In some cases, it's better to use a custom schema. This is
- important if you use tools that generate tables in the{' '}
- <code className="text-code-inline">public</code> schema to{' '}
- <span className="text-brand">prevent accidental exposure of data</span>.
- </p>
- <DocsButton
- abbrev={false}
- className="w-min mt-4"
- href={`${DOCS_URL}/guides/api/using-custom-schemas`}
- />
- </DialogSection>
- <DialogSectionSeparator />
- <Collapsible>
- <CollapsibleTrigger className="py-4 px-5 w-full flex items-center justify-between text-sm">
- <p>
- 1. Create a custom <code className="text-code-inline">api</code> schema and expose it
- </p>
- {hasAPISchema && isAPISchemaExposed ? (
- <Check size={16} className="text-brand" />
- ) : (
- <ChevronDown
- size={16}
- className="transition data-open-parent:rotate-180 data-closed-parent:rotate-0"
- />
- )}
- </CollapsibleTrigger>
- <CollapsibleContent className="text-sm text-foreground-light flex flex-col gap-y-4">
- <p className="mx-5">
- Click the button below to create a new schema named{' '}
- <code className="text-code-inline">api</code> and grant the{' '}
- <code className="text-code-inline">anon</code> and{' '}
- <code className="text-code-inline">authenticated</code> roles usage privileges on this
- schema. This schema will thereafter also be exposed to the Data API.
- </p>
- <div className="px-5">
- <InformationBox
- title="How is the schema created?"
- description={
- <div className="flex flex-col gap-y-2">
- <p>
- The following query will be run to create the{' '}
- <code className="text-code-inline">api</code> schema , as well as to grant the
- necessary privileges to the respective roles
- </p>
- <CodeBlock
- language="sql"
- className="p-1 language-bash prose dark:prose-dark max-w-[68.3ch]"
- >
- {`create schema if not exists api;\ngrant usage on schema api to anon, authenticated;`}
- </CodeBlock>
- </div>
- }
- />
- </div>
- <ButtonTooltip
- type="primary"
- className="w-min mx-5"
- onClick={onSelectCreateAndExposeAPISchema}
- disabled={hasAPISchema && isAPISchemaExposed}
- loading={isCreatingAPISchema}
- tooltip={{
- content: {
- side: 'right',
- text:
- hasAPISchema && isAPISchemaExposed
- ? 'Schema has already been created and exposed'
- : undefined,
- },
- }}
- >
- Create and expose schema to Data API
- </ButtonTooltip>
- <div className="flex flex-col gap-y-4 px-5 pb-4">
- <p>
- Under these new settings, the <code className="text-code-inline">anon</code> and{' '}
- <code className="text-code-inline">authenticated</code> roles can execute functions
- defined in the <code className="text-code-inline">api</code> schema, but they have
- no automatic permissions on any tables. On a table-by-table basis, you can grant
- them permissions by running the following command:
- </p>
- <CodeBlock
- language="sql"
- className="p-1 language-bash prose dark:prose-dark max-w-[68.3ch]"
- >
- {`grant select on table api.<your_table> to anon;\ngrant select, insert, update, delete on table api.<your_table> to authenticated;`}
- </CodeBlock>
- </div>
- </CollapsibleContent>
- </Collapsible>
- <DialogSectionSeparator />
- <Collapsible>
- <CollapsibleTrigger className="py-4 px-5 w-full flex items-center justify-between text-sm">
- <p>
- 2. Remove the <code className="text-code-inline">public</code> schema from the exposed
- schemas
- </p>
- {!isPublicSchemaExposed ? (
- <Check size={16} className="text-brand" />
- ) : (
- <ChevronDown
- size={16}
- className="transition data-open-parent:rotate-180 data-closed-parent:rotate-0"
- />
- )}
- </CollapsibleTrigger>
- <CollapsibleContent className="text-sm text-foreground-light">
- <div className="px-5 pb-4 flex flex-col gap-y-4">
- <Alert variant="warning">
- <WarningIcon />
- <AlertTitle className="text-foreground">
- Ensure that your app is no longer using the{' '}
- <code className="text-code-inline">public</code> schema
- </AlertTitle>
- <AlertDescription>
- The <code className="text-code-inline">public</code> schema will not be accessible
- via the API once its not exposed. You should be using the{' '}
- <code className="text-code-inline">api</code> schema instead.
- </AlertDescription>
- </Alert>
- <p>
- Click the button below to remove the{' '}
- <code className="text-code-inline">public</code> schema from both Exposed schemas
- and Extra search path in your API configuration.
- </p>
- <ButtonTooltip
- type="primary"
- className="w-min"
- disabled={!isPublicSchemaExposed}
- loading={isUpdatingConfig}
- tooltip={{
- content: {
- side: 'right',
- text: !isPublicSchemaExposed ? 'Public schema no longer exposed' : undefined,
- },
- }}
- onClick={onSelectRemovePublicSchema}
- >
- Remove public schema from exposed schemas
- </ButtonTooltip>
- </div>
- </CollapsibleContent>
- </Collapsible>
- </DialogContent>
- </Dialog>
- )
- }
|