Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useParams } from 'common'
import { AlertCircle } from 'lucide-react'
import { Alert_Shadcn_, AlertTitle_Shadcn_, cn } from 'ui'

import { IntegrationOverviewTab } from '../Integration/IntegrationOverviewTab'
import { DataApiEnableSwitch } from '@/components/interfaces/Settings/API/DataApiEnableSwitch'
import { DataApiProjectUrlCard } from '@/components/interfaces/Settings/API/DataApiProjectUrlCard'
import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { PROJECT_STATUS } from '@/lib/constants'

export const DataApiOverviewTab = () => {
const { ref: projectRef } = useParams()
const { data: project, isPending: isProjectLoading } = useSelectedProjectQuery()
const { isEnabled, isPending: isConfigLoading } = useIsDataApiEnabled({ projectRef })

const isLoading = isProjectLoading || isConfigLoading

return (
<IntegrationOverviewTab>
<div className="px-10 max-w-4xl flex flex-col">
{!isProjectLoading && project?.status !== PROJECT_STATUS.ACTIVE_HEALTHY ? (
<Alert_Shadcn_ variant="destructive">
<AlertCircle size={16} />
<AlertTitle_Shadcn_>
API settings are unavailable as the project is not active
</AlertTitle_Shadcn_>
</Alert_Shadcn_>
) : (
<>
<div className={cn((isLoading || !isEnabled) && 'opacity-50 pointer-events-none')}>
<DataApiProjectUrlCard />
</div>
<DataApiEnableSwitch />
</>
)}
</div>
</IntegrationOverviewTab>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { useParams } from 'common'
import { AlertCircle } from 'lucide-react'
import Link from 'next/link'
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_ } from 'ui'
import { PageContainer } from 'ui-patterns'

import { ServiceList } from '@/components/interfaces/Settings/API/ServiceList'
import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled'

export const DataApiSettingsTab = () => {
const { ref: projectRef } = useParams()
const { isEnabled, isPending } = useIsDataApiEnabled({ projectRef })

if (!isPending && !isEnabled) {
return (
<div className="flex w-full flex-1 items-center justify-center p-10">
<Alert_Shadcn_ className="max-w-md">
<AlertCircle size={16} />
<AlertTitle_Shadcn_>Data API is disabled</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
Enable the Data API in the{' '}
<Link
href={`/project/${projectRef}/integrations/data_api/overview`}
className="text-foreground underline hover:decoration-foreground-muted"
>
Overview
</Link>{' '}
tab to configure settings.
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
</div>
)
}

return (
<PageContainer size="default" className="ml-0">
<ServiceList />
</PageContainer>
)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Clock5, Layers, Timer, Vault, Webhook, Receipt } from 'lucide-react'
import { Clock5, Code2, Layers, Timer, Vault, Webhook } from 'lucide-react'
import dynamic from 'next/dynamic'
import Image from 'next/image'
import { ComponentType, ReactNode } from 'react'
Expand Down Expand Up @@ -260,6 +260,53 @@ const SUPABASE_INTEGRATIONS: IntegrationDefinition[] = [
return null
},
},
{
id: 'data_api',
type: 'custom' as const,
requiredExtensions: [],
name: `Data API`,
icon: ({ className, ...props } = {}) => (
<Code2 className={cn('inset-0 p-2 text-black w-full h-full', className)} {...props} />
),
description: 'Auto-generate an API directly from your database schema',
docsUrl: `${DOCS_URL}/guides/api`,
author: authorSupabase,
navigation: [
{
route: 'overview',
label: 'Overview',
},
{
route: 'settings',
label: 'Settings',
},
],
navigate: (_id: string, pageId: string = 'overview', _childId: string | undefined) => {
switch (pageId) {
case 'overview':
return dynamic(
() =>
import('components/interfaces/Integrations/DataApi/OverviewTab').then(
(mod) => mod.DataApiOverviewTab
),
{
loading: Loading,
}
)
case 'settings':
return dynamic(
() =>
import('components/interfaces/Integrations/DataApi/SettingsTab').then(
(mod) => mod.DataApiSettingsTab
),
{
loading: Loading,
}
)
}
return null
},
},
{
id: 'graphiql',
type: 'postgres_extension' as const,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { useFDWsQuery } from 'data/fdw/fdws-query'
import { useFlag } from 'common'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { IS_PLATFORM } from 'lib/constants'
import { EMPTY_ARR } from 'lib/void'
import {
INSTALLATION_INSTALLED_SUFFIX,
Expand All @@ -30,6 +31,9 @@ export const useInstalledIntegrations = () => {
if (!stripeSyncEnabled && integration.id === 'stripe_sync_engine') {
return false
}
if (!IS_PLATFORM && integration.id === 'data_api') {
return false
}
return true
})
}, [integrationsWrappers, stripeSyncEnabled])
Expand Down Expand Up @@ -76,6 +80,9 @@ export const useInstalledIntegrations = () => {
if (integration.id === 'webhooks') {
return isHooksEnabled
}
if (integration.id === 'data_api') {
return true
}
if (integration.id === 'stripe_sync_engine') {
const stripeSchema = schemas?.find(({ name }) => name === 'stripe')
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, WarningIcon } from 'ui'

export const DataApiDisabledAlert = () => {
return (
<Alert_Shadcn_ variant="warning">
<WarningIcon />
<AlertTitle_Shadcn_>No schemas can be queried</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
<p>
With this setting disabled, you will not be able to query any schemas via the Data API.
</p>
<p>
You will see errors from the Postgrest endpoint{' '}
<code className="text-code-inline">/rest/v1/</code>.
</p>
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)
}
163 changes: 163 additions & 0 deletions apps/studio/components/interfaces/Settings/API/DataApiEnableSwitch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { useCallback, useEffect, useReducer } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { Card } from 'ui'

import { DataApiEnableSwitchForm } from './DataApiEnableSwitchForm'
import { DataApiEnableSwitchError, DataApiEnableSwitchLoading } from './DataApiEnableSwitchStates'
import { dataApiFormSchema, type DataApiFormValues } from './DataApiEnableSwitch.types'
import {
enableCheckReducer,
getDefaultSchemas,
queryUnsafeEntitiesInApi,
} from './DataApiEnableSwitch.utils'
import { UnsafeEntitiesConfirmModal } from './UnsafeEntitiesConfirmModal'
import { useProjectPostgrestConfigQuery } from '@/data/config/project-postgrest-config-query'
import { useProjectPostgrestConfigUpdateMutation } from '@/data/config/project-postgrest-config-update-mutation'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useIsDataApiEnabled } from '@/hooks/misc/useIsDataApiEnabled'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { useStaticEffectEvent } from '@/hooks/useStaticEffectEvent'

export const DataApiEnableSwitch = () => {
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
const { can: canUpdatePostgrestConfig, isSuccess: isPermissionsLoaded } =
useAsyncCheckPermissions(PermissionAction.UPDATE, 'custom_config_postgrest')

const {
data: config,
isError,
isPending: isLoadingConfig,
} = useProjectPostgrestConfigQuery({ projectRef })
const { isEnabled, isPending: isEnabledCheckPending } = useIsDataApiEnabled({ projectRef })

const { mutate: updatePostgrestConfig, isPending: isUpdating } =
useProjectPostgrestConfigUpdateMutation({
onSuccess: (_data, variables) => {
toast.success(variables.dbSchema ? 'Data API enabled' : 'Data API disabled')
},
})

const [enableCheck, dispatchEnableCheck] = useReducer(enableCheckReducer, { status: 'idle' })

const formId = 'data-api-enable-form'
const isLoading = isLoadingConfig || !projectRef

const form = useForm<DataApiFormValues>({
resolver: zodResolver(dataApiFormSchema),
mode: 'onChange',
defaultValues: {
enableDataApi: false,
},
})

const syncForm = useStaticEffectEvent(() => {
if (!isEnabledCheckPending) {
form.reset({ enableDataApi: isEnabled })
}
})
useEffect(() => {
syncForm()
}, [syncForm, isEnabled])

const doUpdate = useCallback(
(enableDataApi: boolean) => {
if (!projectRef || !config) return

const dbSchema = enableDataApi ? getDefaultSchemas(config.db_schema).join(', ') : ''

updatePostgrestConfig({
projectRef,
dbSchema,
maxRows: config.max_rows,
dbExtraSearchPath: config.db_extra_search_path ?? '',
dbPool: config.db_pool ?? null,
})
},
[projectRef, config, updatePostgrestConfig]
)

const onSubmit = useCallback(
async ({ enableDataApi }: DataApiFormValues) => {
if (!projectRef) return

if (!enableDataApi || isEnabled) {
doUpdate(enableDataApi)
return
}

// Enabling — check for entities with security issues in the target schemas
const targetSchemas = getDefaultSchemas(config?.db_schema)

dispatchEnableCheck({ type: 'START_CHECK' })
try {
const entities = await queryUnsafeEntitiesInApi({
projectRef,
connectionString: project?.connectionString,
schemas: targetSchemas,
})

if (entities.length > 0) {
dispatchEnableCheck({ type: 'ENTITIES_FOUND', unsafeEntities: entities })
} else {
dispatchEnableCheck({ type: 'DISMISS' })
doUpdate(true)
}
} catch (error) {
console.error('Failed to check for exposed entities', error)
dispatchEnableCheck({ type: 'DISMISS' })
toast.error('Failed to check for exposed entities')
}
},
[projectRef, isEnabled, config?.db_schema, project?.connectionString, doUpdate]
)

const handleReset = useCallback(() => {
if (isEnabledCheckPending) return
form.reset({ enableDataApi: isEnabled })
}, [isEnabledCheckPending, isEnabled, form])

const isBusy = isUpdating || enableCheck.status === 'checking'
const disabled = !canUpdatePostgrestConfig || isBusy
const permissionsHelper =
isPermissionsLoaded && !canUpdatePostgrestConfig
? "You need additional permissions to update your project's API settings"
: undefined

const cardContent = isLoading ? (
<DataApiEnableSwitchLoading />
) : isError || !config ? (
<DataApiEnableSwitchError />
) : (
<DataApiEnableSwitchForm
form={form}
formId={formId}
disabled={disabled}
isBusy={isBusy}
permissionsHelper={permissionsHelper}
onSubmit={onSubmit}
handleReset={handleReset}
/>
)

return (
<>
<Card>{cardContent}</Card>

<UnsafeEntitiesConfirmModal
visible={enableCheck.status === 'confirming'}
loading={isUpdating}
unsafeEntities={enableCheck.status === 'confirming' ? enableCheck.unsafeEntities : []}
onCancel={() => dispatchEnableCheck({ type: 'DISMISS' })}
onConfirm={() => {
dispatchEnableCheck({ type: 'DISMISS' })
doUpdate(true)
}}
/>
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { z } from 'zod'

import type { ExposedEntity } from './DataApiEnableSwitch.utils'

export const dataApiFormSchema = z.object({
enableDataApi: z.boolean(),
})

export type DataApiFormValues = z.infer<typeof dataApiFormSchema>

export type EnableCheckState =
| { status: 'idle' }
| { status: 'checking' }
| { status: 'confirming'; unsafeEntities: Array<ExposedEntity> }

export type EnableCheckAction =
| { type: 'START_CHECK' }
| { type: 'ENTITIES_FOUND'; unsafeEntities: Array<ExposedEntity> }
| { type: 'DISMISS' }
Loading
Loading