DataApiEnableSwitch.tsx163 lines · main
| 1 | import { zodResolver } from '@hookform/resolvers/zod' |
| 2 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 3 | import { useParams } from 'common' |
| 4 | import { useCallback, useEffect, useReducer } from 'react' |
| 5 | import { useForm } from 'react-hook-form' |
| 6 | import { toast } from 'sonner' |
| 7 | import { Card } from 'ui' |
| 8 | |
| 9 | import { dataApiFormSchema, type DataApiFormValues } from './DataApiEnableSwitch.types' |
| 10 | import { |
| 11 | enableCheckReducer, |
| 12 | getDefaultSchemas, |
| 13 | queryUnsafeEntitiesInApi, |
| 14 | } from './DataApiEnableSwitch.utils' |
| 15 | import { DataApiEnableSwitchForm } from './DataApiEnableSwitchForm' |
| 16 | import { DataApiEnableSwitchError, DataApiEnableSwitchLoading } from './DataApiEnableSwitchStates' |
| 17 | import { UnsafeEntitiesConfirmModal } from './UnsafeEntitiesConfirmModal' |
| 18 | import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query' |
| 19 | import { useProjectPostgrestConfigUpdateMutation } from '@/data/config/project-postgrest-config-update-mutation' |
| 20 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 21 | import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled' |
| 22 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 23 | import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent' |
| 24 | |
| 25 | export 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 | } |