DataApiEnableSwitch.tsx163 lines · main
1import { zodResolver } from '@hookform/resolvers/zod'
2import { PermissionAction } from '@supabase/shared-types/out/constants'
3import { useParams } from 'common'
4import { useCallback, useEffect, useReducer } from 'react'
5import { useForm } from 'react-hook-form'
6import { toast } from 'sonner'
7import { Card } from 'ui'
8
9import { dataApiFormSchema, type DataApiFormValues } from './DataApiEnableSwitch.types'
10import {
11 enableCheckReducer,
12 getDefaultSchemas,
13 queryUnsafeEntitiesInApi,
14} from './DataApiEnableSwitch.utils'
15import { DataApiEnableSwitchForm } from './DataApiEnableSwitchForm'
16import { DataApiEnableSwitchError, DataApiEnableSwitchLoading } from './DataApiEnableSwitchStates'
17import { UnsafeEntitiesConfirmModal } from './UnsafeEntitiesConfirmModal'
18import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
19import { useProjectPostgrestConfigUpdateMutation } from '@/data/config/project-postgrest-config-update-mutation'
20import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
21import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled'
22import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
23import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'
24
25export const DataApiEnableSwitch = () => {
26 const { ref: projectRef } = useParams()
27 const { data: project } = useSelectedProjectQuery()
28 const { can: canUpdatePostgrestConfig, isSuccess: isPermissionsLoaded } =
29 useAsyncCheckPermissions(PermissionAction.UPDATE, 'custom_config_postgrest')
30
31 const {
32 data: config,
33 isError,
34 isPending: isLoadingConfig,
35 } = useProjectPostgrestConfigQuery({ projectRef })
36 const { isEnabled, isPending: isEnabledCheckPending } = useIsDataApiEnabled({ projectRef })
37
38 const { mutate: updatePostgrestConfig, isPending: isUpdating } =
39 useProjectPostgrestConfigUpdateMutation({
40 onSuccess: (_data, variables) => {
41 toast.success(variables.dbSchema ? 'Data API enabled' : 'Data API disabled')
42 },
43 })
44
45 const [enableCheck, dispatchEnableCheck] = useReducer(enableCheckReducer, { status: 'idle' })
46
47 const formId = 'data-api-enable-form'
48 const isLoading = isLoadingConfig || !projectRef
49
50 const form = useForm<DataApiFormValues>({
51 resolver: zodResolver(dataApiFormSchema as any),
52 mode: 'onChange',
53 defaultValues: {
54 enableDataApi: false,
55 },
56 })
57
58 const syncForm = useStaticEffectEvent(() => {
59 if (!isEnabledCheckPending) {
60 form.reset({ enableDataApi: isEnabled })
61 }
62 })
63 useEffect(() => {
64 syncForm()
65 }, [syncForm, isEnabled])
66
67 const doUpdate = useCallback(
68 (enableDataApi: boolean) => {
69 if (!projectRef || !config) return
70
71 const dbSchema = enableDataApi ? getDefaultSchemas(config.db_schema).join(', ') : ''
72
73 updatePostgrestConfig({
74 projectRef,
75 dbSchema,
76 maxRows: config.max_rows,
77 dbExtraSearchPath: config.db_extra_search_path ?? '',
78 dbPool: config.db_pool ?? null,
79 })
80 },
81 [projectRef, config, updatePostgrestConfig]
82 )
83
84 const onSubmit = useCallback(
85 async ({ enableDataApi }: DataApiFormValues) => {
86 if (!projectRef) return
87
88 if (!enableDataApi || isEnabled) {
89 doUpdate(enableDataApi)
90 return
91 }
92
93 // Enabling — check for entities with security issues in the target schemas
94 const targetSchemas = getDefaultSchemas(config?.db_schema)
95
96 dispatchEnableCheck({ type: 'START_CHECK' })
97 try {
98 const entities = await queryUnsafeEntitiesInApi({
99 projectRef,
100 connectionString: project?.connectionString,
101 schemas: targetSchemas,
102 })
103
104 if (entities.length > 0) {
105 dispatchEnableCheck({ type: 'ENTITIES_FOUND', unsafeEntities: entities })
106 } else {
107 dispatchEnableCheck({ type: 'DISMISS' })
108 doUpdate(true)
109 }
110 } catch (error) {
111 console.error('Failed to check for exposed entities', error)
112 dispatchEnableCheck({ type: 'DISMISS' })
113 toast.error('Failed to check for exposed entities')
114 }
115 },
116 [projectRef, isEnabled, config?.db_schema, project?.connectionString, doUpdate]
117 )
118
119 const handleReset = useCallback(() => {
120 if (isEnabledCheckPending) return
121 form.reset({ enableDataApi: isEnabled })
122 }, [isEnabledCheckPending, isEnabled, form])
123
124 const isBusy = isUpdating || enableCheck.status === 'checking'
125 const disabled = !canUpdatePostgrestConfig || isBusy
126 const permissionsHelper =
127 isPermissionsLoaded && !canUpdatePostgrestConfig
128 ? "You need additional permissions to update your project's API settings"
129 : undefined
130
131 const cardContent = isLoading ? (
132 <DataApiEnableSwitchLoading />
133 ) : isError || !config ? (
134 <DataApiEnableSwitchError />
135 ) : (
136 <DataApiEnableSwitchForm
137 form={form}
138 formId={formId}
139 disabled={disabled}
140 isBusy={isBusy}
141 permissionsHelper={permissionsHelper}
142 onSubmit={onSubmit}
143 handleReset={handleReset}
144 />
145 )
146
147 return (
148 <>
149 <Card>{cardContent}</Card>
150
151 <UnsafeEntitiesConfirmModal
152 visible={enableCheck.status === 'confirming'}
153 loading={isUpdating}
154 unsafeEntities={enableCheck.status === 'confirming' ? enableCheck.unsafeEntities : []}
155 onCancel={() => dispatchEnableCheck({ type: 'DISMISS' })}
156 onConfirm={() => {
157 dispatchEnableCheck({ type: 'DISMISS' })
158 doUpdate(true)
159 }}
160 />
161 </>
162 )
163}