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