DataApiEnableSwitch.tsx 5.4 KB

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