HardenAPIModal.tsx 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. import { Check, ChevronDown } from 'lucide-react'
  2. import { toast } from 'sonner'
  3. import {
  4. Alert,
  5. AlertDescription,
  6. AlertTitle,
  7. Collapsible,
  8. CollapsibleContent,
  9. CollapsibleTrigger,
  10. Dialog,
  11. DialogContent,
  12. DialogDescription,
  13. DialogHeader,
  14. DialogSection,
  15. DialogSectionSeparator,
  16. DialogTitle,
  17. WarningIcon,
  18. } from 'ui'
  19. import { CodeBlock } from 'ui-patterns/CodeBlock'
  20. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  21. import { DocsButton } from '@/components/ui/DocsButton'
  22. import InformationBox from '@/components/ui/InformationBox'
  23. import { useCreateAndExposeAPISchemaMutation } from '@/data/api-settings/create-and-expose-api-schema-mutation'
  24. import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
  25. import { useProjectPostgrestConfigUpdateMutation } from '@/data/config/project-postgrest-config-update-mutation'
  26. import { useSchemasQuery } from '@/data/database/schemas-query'
  27. import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
  28. import { DOCS_URL } from '@/lib/constants'
  29. interface HardenAPIModalProps {
  30. visible: boolean
  31. onClose: () => void
  32. }
  33. export const HardenAPIModal = ({ visible, onClose }: HardenAPIModalProps) => {
  34. const { data: project } = useSelectedProjectQuery()
  35. const { data: schemas } = useSchemasQuery({
  36. projectRef: project?.ref,
  37. connectionString: project?.connectionString,
  38. })
  39. const { data: config } = useProjectPostgrestConfigQuery({ projectRef: project?.ref })
  40. const hasAPISchema = (schemas ?? []).find((schema) => schema.name === 'api')
  41. const exposedSchemas = config?.db_schema.split(',').map((x) => x.trim()) ?? []
  42. const isAPISchemaExposed = exposedSchemas.includes('api')
  43. const isPublicSchemaExposed = exposedSchemas.includes('public')
  44. const { mutate: createAndExposeAPISchema, isPending: isCreatingAPISchema } =
  45. useCreateAndExposeAPISchemaMutation({
  46. onSuccess: () => {
  47. toast.success(`Successfully created api schema and exposed via Data API`)
  48. },
  49. })
  50. const { mutate: updatePostgrestConfig, isPending: isUpdatingConfig } =
  51. useProjectPostgrestConfigUpdateMutation({
  52. onSuccess: () => {
  53. toast.success('Success removed public schema from exposed schemas')
  54. },
  55. })
  56. const onSelectCreateAndExposeAPISchema = () => {
  57. if (project === undefined) return console.error('Project is required')
  58. if (config === undefined) return console.error('Postgrest config is required')
  59. createAndExposeAPISchema({
  60. projectRef: project?.ref,
  61. connectionString: project?.connectionString,
  62. existingPostgrestConfig: {
  63. max_rows: config.max_rows,
  64. db_pool: config.db_pool,
  65. db_schema: config.db_schema,
  66. db_extra_search_path: config?.db_extra_search_path,
  67. },
  68. })
  69. }
  70. const onSelectRemovePublicSchema = () => {
  71. if (project === undefined) return console.error('Project is required')
  72. if (config === undefined) return console.error('Postgrest config is required')
  73. const updatedDbExtraSearchPath = config.db_extra_search_path
  74. .split(',')
  75. .map((x) => x.trim())
  76. .filter((x) => x !== 'public')
  77. .join(', ')
  78. const updatedDbSchema = config.db_schema
  79. .split(',')
  80. .map((x) => x.trim())
  81. .filter((x) => x !== 'public')
  82. .join(', ')
  83. updatePostgrestConfig({
  84. projectRef: project.ref,
  85. maxRows: config.max_rows,
  86. dbPool: config.db_pool,
  87. dbSchema: updatedDbSchema,
  88. dbExtraSearchPath: updatedDbExtraSearchPath,
  89. })
  90. }
  91. return (
  92. <Dialog open={visible} onOpenChange={onClose}>
  93. <DialogContent size="large">
  94. <DialogHeader>
  95. <DialogTitle>Switch the default API schema</DialogTitle>
  96. <DialogDescription>
  97. Expose a custom schema instead of the <code className="text-code-inline">public</code>{' '}
  98. schema
  99. </DialogDescription>
  100. </DialogHeader>
  101. <DialogSectionSeparator />
  102. <DialogSection className="text-sm text-foreground-light">
  103. <p>
  104. By default, the <code className="text-code-inline">public</code> schema is used to
  105. generate API routes. In some cases, it's better to use a custom schema. This is
  106. important if you use tools that generate tables in the{' '}
  107. <code className="text-code-inline">public</code> schema to{' '}
  108. <span className="text-brand">prevent accidental exposure of data</span>.
  109. </p>
  110. <DocsButton
  111. abbrev={false}
  112. className="w-min mt-4"
  113. href={`${DOCS_URL}/guides/api/using-custom-schemas`}
  114. />
  115. </DialogSection>
  116. <DialogSectionSeparator />
  117. <Collapsible>
  118. <CollapsibleTrigger className="py-4 px-5 w-full flex items-center justify-between text-sm">
  119. <p>
  120. 1. Create a custom <code className="text-code-inline">api</code> schema and expose it
  121. </p>
  122. {hasAPISchema && isAPISchemaExposed ? (
  123. <Check size={16} className="text-brand" />
  124. ) : (
  125. <ChevronDown
  126. size={16}
  127. className="transition data-open-parent:rotate-180 data-closed-parent:rotate-0"
  128. />
  129. )}
  130. </CollapsibleTrigger>
  131. <CollapsibleContent className="text-sm text-foreground-light flex flex-col gap-y-4">
  132. <p className="mx-5">
  133. Click the button below to create a new schema named{' '}
  134. <code className="text-code-inline">api</code> and grant the{' '}
  135. <code className="text-code-inline">anon</code> and{' '}
  136. <code className="text-code-inline">authenticated</code> roles usage privileges on this
  137. schema. This schema will thereafter also be exposed to the Data API.
  138. </p>
  139. <div className="px-5">
  140. <InformationBox
  141. title="How is the schema created?"
  142. description={
  143. <div className="flex flex-col gap-y-2">
  144. <p>
  145. The following query will be run to create the{' '}
  146. <code className="text-code-inline">api</code> schema , as well as to grant the
  147. necessary privileges to the respective roles
  148. </p>
  149. <CodeBlock
  150. language="sql"
  151. className="p-1 language-bash prose dark:prose-dark max-w-[68.3ch]"
  152. >
  153. {`create schema if not exists api;\ngrant usage on schema api to anon, authenticated;`}
  154. </CodeBlock>
  155. </div>
  156. }
  157. />
  158. </div>
  159. <ButtonTooltip
  160. type="primary"
  161. className="w-min mx-5"
  162. onClick={onSelectCreateAndExposeAPISchema}
  163. disabled={hasAPISchema && isAPISchemaExposed}
  164. loading={isCreatingAPISchema}
  165. tooltip={{
  166. content: {
  167. side: 'right',
  168. text:
  169. hasAPISchema && isAPISchemaExposed
  170. ? 'Schema has already been created and exposed'
  171. : undefined,
  172. },
  173. }}
  174. >
  175. Create and expose schema to Data API
  176. </ButtonTooltip>
  177. <div className="flex flex-col gap-y-4 px-5 pb-4">
  178. <p>
  179. Under these new settings, the <code className="text-code-inline">anon</code> and{' '}
  180. <code className="text-code-inline">authenticated</code> roles can execute functions
  181. defined in the <code className="text-code-inline">api</code> schema, but they have
  182. no automatic permissions on any tables. On a table-by-table basis, you can grant
  183. them permissions by running the following command:
  184. </p>
  185. <CodeBlock
  186. language="sql"
  187. className="p-1 language-bash prose dark:prose-dark max-w-[68.3ch]"
  188. >
  189. {`grant select on table api.<your_table> to anon;\ngrant select, insert, update, delete on table api.<your_table> to authenticated;`}
  190. </CodeBlock>
  191. </div>
  192. </CollapsibleContent>
  193. </Collapsible>
  194. <DialogSectionSeparator />
  195. <Collapsible>
  196. <CollapsibleTrigger className="py-4 px-5 w-full flex items-center justify-between text-sm">
  197. <p>
  198. 2. Remove the <code className="text-code-inline">public</code> schema from the exposed
  199. schemas
  200. </p>
  201. {!isPublicSchemaExposed ? (
  202. <Check size={16} className="text-brand" />
  203. ) : (
  204. <ChevronDown
  205. size={16}
  206. className="transition data-open-parent:rotate-180 data-closed-parent:rotate-0"
  207. />
  208. )}
  209. </CollapsibleTrigger>
  210. <CollapsibleContent className="text-sm text-foreground-light">
  211. <div className="px-5 pb-4 flex flex-col gap-y-4">
  212. <Alert variant="warning">
  213. <WarningIcon />
  214. <AlertTitle className="text-foreground">
  215. Ensure that your app is no longer using the{' '}
  216. <code className="text-code-inline">public</code> schema
  217. </AlertTitle>
  218. <AlertDescription>
  219. The <code className="text-code-inline">public</code> schema will not be accessible
  220. via the API once its not exposed. You should be using the{' '}
  221. <code className="text-code-inline">api</code> schema instead.
  222. </AlertDescription>
  223. </Alert>
  224. <p>
  225. Click the button below to remove the{' '}
  226. <code className="text-code-inline">public</code> schema from both Exposed schemas
  227. and Extra search path in your API configuration.
  228. </p>
  229. <ButtonTooltip
  230. type="primary"
  231. className="w-min"
  232. disabled={!isPublicSchemaExposed}
  233. loading={isUpdatingConfig}
  234. tooltip={{
  235. content: {
  236. side: 'right',
  237. text: !isPublicSchemaExposed ? 'Public schema no longer exposed' : undefined,
  238. },
  239. }}
  240. onClick={onSelectRemovePublicSchema}
  241. >
  242. Remove public schema from exposed schemas
  243. </ButtonTooltip>
  244. </div>
  245. </CollapsibleContent>
  246. </Collapsible>
  247. </DialogContent>
  248. </Dialog>
  249. )
  250. }