diff --git a/apps/studio/components/interfaces/Settings/API/ServiceList.tsx b/apps/studio/components/interfaces/Settings/API/ServiceList.tsx
index f6d0086fca5..56b402af109 100644
--- a/apps/studio/components/interfaces/Settings/API/ServiceList.tsx
+++ b/apps/studio/components/interfaces/Settings/API/ServiceList.tsx
@@ -8,7 +8,6 @@ import { useParams } from 'common'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import DatabaseSelector from 'components/ui/DatabaseSelector'
import Panel from 'components/ui/Panel'
-import { DisplayApiSettings } from 'components/ui/ProjectSettings'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
import { configKeys } from 'data/config/keys'
@@ -19,9 +18,9 @@ import { PROJECT_STATUS } from 'lib/constants'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { Badge, Input } from 'ui'
import { JWT_SECRET_UPDATE_ERROR_MESSAGES } from './API.constants'
+import { ApiKeysMoved } from './ApiKeysMoved'
import JWTSettings from './JWTSettings'
import { PostgrestConfig } from './PostgrestConfig'
-import { ApiKeysMoved } from './ApiKeysMoved'
const ServiceList = () => {
const client = useQueryClient()
diff --git a/apps/studio/components/layouts/ProjectLayout/BuildingState.tsx b/apps/studio/components/layouts/ProjectLayout/BuildingState.tsx
index d0044b2823b..e6e1589c44b 100644
--- a/apps/studio/components/layouts/ProjectLayout/BuildingState.tsx
+++ b/apps/studio/components/layouts/ProjectLayout/BuildingState.tsx
@@ -122,8 +122,8 @@ const BuildingState = () => {
-
diff --git a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx
index 02161bf0e4a..d6367fc2a7b 100644
--- a/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx
+++ b/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx
@@ -1,23 +1,27 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
+import { AlertCircle, Loader2 } from 'lucide-react'
+import { useMemo } from 'react'
+import { toast } from 'sonner'
+
import { useParams } from 'common'
import Panel from 'components/ui/Panel'
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useFlag } from 'hooks/ui/useFlag'
-import { AlertCircle, Loader2 } from 'lucide-react'
-import { useMemo } from 'react'
-import { toast } from 'sonner'
import { Input } from 'ui'
import { getLastUsedAPIKeys, useLastUsedAPIKeysLogQuery } from './DisplayApiSettings.utils'
+import { ToggleLegacyApiKeysPanel } from './ToggleLegacyApiKeys'
-const DisplayApiSettings = ({
- legacy,
+export const DisplayApiSettings = ({
+ showTitle = true,
showNotice = true,
+ showLegacyText = true,
}: {
- legacy?: boolean
+ showTitle?: boolean
showNotice?: boolean
+ showLegacyText?: boolean
}) => {
const { ref: projectRef } = useParams()
@@ -35,13 +39,16 @@ const DisplayApiSettings = ({
} = useJwtSecretUpdatingStatusQuery({ projectRef })
const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus
- const { isLoading, can: canReadAPIKeys } = useAsyncCheckProjectPermissions(
+ const { isLoading: isLoadingPermissions, can: canReadAPIKeys } = useAsyncCheckProjectPermissions(
PermissionAction.READ,
'service_api_keys'
)
+
+ const isLoading = isProjectSettingsLoading || isLoadingPermissions
+
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
- const apiKeys = settings?.service_api_keys ?? []
+ const apiKeys = useMemo(() => settings?.service_api_keys ?? [], [settings])
// api keys should not be empty. However it can be populated with a delay on project creation
const isApiKeysEmpty = apiKeys.length === 0
@@ -66,8 +73,9 @@ const DisplayApiSettings = ({
return (
<>
Project API Keys
@@ -159,9 +167,9 @@ const DisplayApiSettings = ({
descriptionText={
x.tags === 'service_role'
? 'This key has the ability to bypass Row Level Security. Never share it publicly. If leaked, generate a new JWT secret immediately. ' +
- (legacy ? 'Prefer using Publishable API keys instead.' : '')
+ (showLegacyText ? 'Prefer using Publishable API keys instead.' : '')
: 'This key is safe to use in a browser if you have enabled Row Level Security for your tables and configured policies. ' +
- (legacy ? 'Prefer using Secret API keys instead.' : '')
+ (showLegacyText ? 'Prefer using Secret API keys instead.' : '')
}
/>
@@ -201,7 +209,7 @@ const DisplayApiSettings = ({
)
) : null}
+ {newApiKeysInRollOut && !showNotice && }
>
)
}
-export default DisplayApiSettings
diff --git a/apps/studio/components/ui/ProjectSettings/ToggleLegacyApiKeys.tsx b/apps/studio/components/ui/ProjectSettings/ToggleLegacyApiKeys.tsx
new file mode 100644
index 00000000000..0c17e4a676f
--- /dev/null
+++ b/apps/studio/components/ui/ProjectSettings/ToggleLegacyApiKeys.tsx
@@ -0,0 +1,130 @@
+import { useState } from 'react'
+import { toast } from 'sonner'
+
+import { PermissionAction } from '@supabase/shared-types/out/constants'
+import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
+import { ButtonTooltip } from 'components/ui/ButtonTooltip'
+import { useToggleLegacyAPIKeysMutation } from 'data/api-keys/legacy-api-key-toggle-mutation'
+import { useLegacyAPIKeysStatusQuery } from 'data/api-keys/legacy-api-keys-status-query'
+import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
+import { Alert_Shadcn_, AlertDescription_Shadcn_, AlertTitle_Shadcn_, CriticalIcon } from 'ui'
+import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
+
+export const ToggleLegacyApiKeysPanel = () => {
+ const { project } = useProjectContext()
+ const [isConfirmOpen, setIsConfirmOpen] = useState(false)
+
+ const { data: legacyAPIKeysStatusData, isSuccess: isLegacyAPIKeysStatusSuccess } =
+ useLegacyAPIKeysStatusQuery({ projectRef: project!.ref })
+
+ const { can: canUpdateAPIKeys, isSuccess: isPermissionsSuccess } =
+ useAsyncCheckProjectPermissions(PermissionAction.SECRETS_WRITE, '*')
+
+ if (!(isLegacyAPIKeysStatusSuccess && isPermissionsSuccess)) {
+ return null
+ }
+
+ return (
+
+
+
+
+ {legacyAPIKeysStatusData.enabled
+ ? 'Disabling your legacy API keys may cause your applications to break.'
+ : 'Re-enabling your legacy API keys may expose your applications to security risks.'}
+
+
+ {legacyAPIKeysStatusData.enabled
+ ? 'Make sure you are no longer using your legacy API keys before proceeding.'
+ : "Make sure you've tested your RLS policies."}
+
+
+ setIsConfirmOpen(true)}
+ disabled={!canUpdateAPIKeys}
+ tooltip={{
+ content: {
+ side: 'bottom',
+ text: !canUpdateAPIKeys
+ ? 'You need additional permissions to enable or disable JWT-based API keys'
+ : undefined,
+ },
+ }}
+ >
+ {legacyAPIKeysStatusData.enabled
+ ? 'Disable JWT-based API keys'
+ : 'Re-enable JWT-based API keys'}
+
+
+
+
+ setIsConfirmOpen(false)}
+ legacyAPIKeysStatusData={legacyAPIKeysStatusData}
+ />
+
+ )
+}
+
+const ToggleApiKeysModal = ({
+ visible,
+ onClose,
+ legacyAPIKeysStatusData,
+}: {
+ visible: boolean
+ onClose: () => void
+ legacyAPIKeysStatusData: { enabled: boolean }
+}) => {
+ const { project } = useProjectContext()
+
+ const { mutate: toggleLegacyAPIKey, isLoading: isTogglingLegacyAPIKey } =
+ useToggleLegacyAPIKeysMutation()
+
+ const onToggleLegacyAPIKeysEnabled = () => {
+ const enabled = !legacyAPIKeysStatusData?.enabled
+
+ toggleLegacyAPIKey(
+ { projectRef: project!.ref, enabled },
+ {
+ onSuccess: () => {
+ toast.success(
+ enabled
+ ? 'Your anon and service_role keys have been re-enabled!'
+ : 'Your anon and service_role keys have been disabled!'
+ )
+ onClose()
+ },
+ }
+ )
+ }
+
+ return (
+ onClose()}
+ onConfirm={onToggleLegacyAPIKeysEnabled}
+ title={
+ legacyAPIKeysStatusData.enabled ? 'Disable JWT-based keys' : 'Re-enable JWT-based keys'
+ }
+ confirmString={legacyAPIKeysStatusData.enabled ? 'disable' : 're-enable'}
+ confirmLabel={`Yes, ${legacyAPIKeysStatusData.enabled ? 'disable' : 're-enable'} anon and service_role`}
+ confirmPlaceholder={legacyAPIKeysStatusData.enabled ? 'disable' : 're-enable'}
+ loading={isTogglingLegacyAPIKey}
+ variant={legacyAPIKeysStatusData.enabled ? 'destructive' : 'default'}
+ alert={
+ legacyAPIKeysStatusData.enabled
+ ? {
+ title: 'Disabling can cause downtime!',
+ description: `If you disable your anon and service_role keys while they are in use, your applications will stop functioning. All API endpoints will receive HTTP 401 Unauthorized. Make sure you are no longer using them before proceeding.`,
+ }
+ : {
+ title: 'Prefer publishable and secret keys',
+ description:
+ 'While re-enabling anon and service_role keys makes sense in some cases, a better and more secure alternative is the publishable or secret key. Consider using those before proceeding!',
+ }
+ }
+ />
+ )
+}
diff --git a/apps/studio/components/ui/ProjectSettings/index.tsx b/apps/studio/components/ui/ProjectSettings/index.tsx
index 37e2dcc5b8b..2d41ad6aaf5 100644
--- a/apps/studio/components/ui/ProjectSettings/index.tsx
+++ b/apps/studio/components/ui/ProjectSettings/index.tsx
@@ -1,4 +1,4 @@
-import DisplayApiSettings from './DisplayApiSettings'
+import { DisplayApiSettings } from './DisplayApiSettings'
import DisplayConfigSettings from './DisplayConfigSettings'
export { DisplayApiSettings, DisplayConfigSettings }
diff --git a/apps/studio/data/api-keys/api-keys-query.ts b/apps/studio/data/api-keys/api-keys-query.ts
index 2ade5311c9d..de87ed9c42b 100644
--- a/apps/studio/data/api-keys/api-keys-query.ts
+++ b/apps/studio/data/api-keys/api-keys-query.ts
@@ -43,12 +43,12 @@ type PublishableKeys = {
updated_at?: string
}
-export interface APIKeysVariables {
+interface APIKeysVariables {
projectRef?: string
reveal: boolean
}
-export async function getAPIKeys({ projectRef, reveal }: APIKeysVariables, signal?: AbortSignal) {
+async function getAPIKeys({ projectRef, reveal }: APIKeysVariables, signal?: AbortSignal) {
if (!projectRef) throw new Error('projectRef is required')
const { data, error } = await get(`/v1/projects/{ref}/api-keys`, {
diff --git a/apps/studio/data/api-keys/keys.ts b/apps/studio/data/api-keys/keys.ts
index 96d50623291..84d9c41d1d1 100644
--- a/apps/studio/data/api-keys/keys.ts
+++ b/apps/studio/data/api-keys/keys.ts
@@ -1,4 +1,5 @@
export const apiKeysKeys = {
list: (projectRef?: string) => ['projects', projectRef, 'api-keys'] as const,
single: (projectRef?: string, id?: string) => ['projects', projectRef, 'api-keys', id] as const,
+ status: (projectRef?: string) => ['projects', projectRef, 'api-keys', 'legacy'] as const,
}
diff --git a/apps/studio/data/api-keys/legacy-api-key-toggle-mutation.ts b/apps/studio/data/api-keys/legacy-api-key-toggle-mutation.ts
new file mode 100644
index 00000000000..0f0a07c43e3
--- /dev/null
+++ b/apps/studio/data/api-keys/legacy-api-key-toggle-mutation.ts
@@ -0,0 +1,60 @@
+import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
+import { handleError, put } from 'data/fetchers'
+import { toast } from 'sonner'
+import type { ResponseError } from 'types'
+import { apiKeysKeys } from './keys'
+
+export type ToggleLegacyAPIKeysVariables = {
+ projectRef?: string
+ enabled: boolean
+}
+
+export async function toggleLegacyAPIKeys(payload: ToggleLegacyAPIKeysVariables) {
+ if (!payload.projectRef) throw new Error('projectRef is required')
+
+ const { data, error } = await put('/v1/projects/{ref}/api-keys/legacy', {
+ params: {
+ path: { ref: payload.projectRef },
+ query: { enabled: payload.enabled },
+ },
+ })
+
+ if (error) handleError(error)
+ return data
+}
+
+type ToggleLegacyAPIKeysData = Awaited>
+
+export const useToggleLegacyAPIKeysMutation = ({
+ onSuccess,
+ onError,
+ ...options
+}: Omit<
+ UseMutationOptions,
+ 'mutationFn'
+> = {}) => {
+ const queryClient = useQueryClient()
+
+ return useMutation(
+ (vars) => toggleLegacyAPIKeys(vars),
+ {
+ async onSuccess(data, variables, context) {
+ const { projectRef } = variables
+
+ await queryClient.invalidateQueries(apiKeysKeys.status(projectRef))
+
+ await onSuccess?.(data, variables, context)
+ },
+ async onError(data, variables, context) {
+ if (onError === undefined) {
+ toast.error(
+ `Failed to ${variables.enabled ? 're-enable' : 'disable'} JWT-based API keys: ${data.message}`
+ )
+ } else {
+ onError(data, variables, context)
+ }
+ },
+ ...options,
+ }
+ )
+}
diff --git a/apps/studio/data/api-keys/legacy-api-keys-status-query.ts b/apps/studio/data/api-keys/legacy-api-keys-status-query.ts
new file mode 100644
index 00000000000..964f2f49d54
--- /dev/null
+++ b/apps/studio/data/api-keys/legacy-api-keys-status-query.ts
@@ -0,0 +1,41 @@
+import { useQuery, UseQueryOptions } from '@tanstack/react-query'
+import { get, handleError } from 'data/fetchers'
+import { ResponseError } from 'types'
+import { apiKeysKeys } from './keys'
+
+interface LegacyAPIKeysStatusVariables {
+ projectRef?: string
+}
+
+async function getLegacyAPIKeysStatus(
+ { projectRef }: LegacyAPIKeysStatusVariables,
+ signal?: AbortSignal
+) {
+ if (!projectRef) throw new Error('projectRef is required')
+
+ const { data, error } = await get(`/v1/projects/{ref}/api-keys/legacy`, {
+ params: { path: { ref: projectRef } },
+ signal,
+ })
+
+ if (error) {
+ handleError(error)
+ }
+
+ return data
+}
+
+type LegacyAPIKeysStatusData = Awaited>
+
+export const useLegacyAPIKeysStatusQuery = (
+ { projectRef }: LegacyAPIKeysStatusVariables,
+ { ...options }: UseQueryOptions = {}
+) =>
+ useQuery(
+ apiKeysKeys.status(projectRef),
+ ({ signal }) => getLegacyAPIKeysStatus({ projectRef }, signal),
+ {
+ enabled: !!projectRef,
+ ...options,
+ }
+ )
diff --git a/apps/studio/pages/project/[ref]/settings/api-keys/index.tsx b/apps/studio/pages/project/[ref]/settings/api-keys/index.tsx
index 723cc7fb139..89db964ba92 100644
--- a/apps/studio/pages/project/[ref]/settings/api-keys/index.tsx
+++ b/apps/studio/pages/project/[ref]/settings/api-keys/index.tsx
@@ -5,7 +5,7 @@ import { DisplayApiSettings } from 'components/ui/ProjectSettings'
import type { NextPageWithLayout } from 'types'
const ApiKeysLegacyPage: NextPageWithLayout = () => {
- return
+ return
}
ApiKeysLegacyPage.getLayout = (page) => (
diff --git a/packages/api-types/types/api.d.ts b/packages/api-types/types/api.d.ts
index e1ec2f4d4ba..8a43fd88679 100644
--- a/packages/api-types/types/api.d.ts
+++ b/packages/api-types/types/api.d.ts
@@ -395,6 +395,24 @@ export interface paths {
patch: operations['updateApiKey']
trace?: never
}
+ '/v1/projects/{ref}/api-keys/legacy': {
+ parameters: {
+ query?: never
+ header?: never
+ path?: never
+ cookie?: never
+ }
+ /** [Beta] Check whether JWT based legacy (anon, service_role) API keys are enabled. This API endpoint will be removed in the future, check for HTTP 404 Not Found. */
+ get: operations['checkLegacyApiKeys']
+ /** [Beta] Disable or re-enable JWT based legacy (anon, service_role) API keys. This API endpoint will be removed in the future, check for HTTP 404 Not Found. */
+ put: operations['updateLegacyApiKeys']
+ post?: never
+ delete?: never
+ options?: never
+ head?: never
+ patch?: never
+ trace?: never
+ }
'/v1/projects/{ref}/billing/addons': {
parameters: {
query?: never
@@ -2062,6 +2080,9 @@ export interface components {
}
updated_at?: string
}
+ LegacyApiKeysResponse: {
+ enabled: boolean
+ }
ListProjectAddonsResponse: {
available_addons: {
name: string
@@ -2967,7 +2988,11 @@ export interface components {
allowed_release_channels: ('internal' | 'alpha' | 'beta' | 'ga' | 'withdrawn' | 'preview')[]
id: string
name: string
- opt_in_tags: 'AI_SQL_GENERATOR_OPT_IN'[]
+ opt_in_tags: (
+ | 'AI_SQL_GENERATOR_OPT_IN'
+ | 'AI_DATA_GENERATOR_OPT_IN'
+ | 'AI_LOG_GENERATOR_OPT_IN'
+ )[]
/** @enum {string} */
plan?: 'free' | 'pro' | 'team' | 'enterprise'
}
@@ -3955,8 +3980,11 @@ export interface operations {
deleteApiKey: {
parameters: {
query?: {
+ reason?: string
/** @description Boolean string, true or false */
reveal?: boolean
+ /** @description Boolean string, true or false */
+ was_compromised?: boolean
}
header?: never
path: {
@@ -4020,6 +4048,65 @@ export interface operations {
}
}
}
+ checkLegacyApiKeys: {
+ parameters: {
+ query?: never
+ header?: never
+ path: {
+ /** @description Project ref */
+ ref: string
+ }
+ cookie?: never
+ }
+ requestBody?: never
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown
+ }
+ content: {
+ 'application/json': components['schemas']['LegacyApiKeysResponse']
+ }
+ }
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ }
+ }
+ updateLegacyApiKeys: {
+ parameters: {
+ query: {
+ /** @description Boolean string, true or false */
+ enabled: boolean
+ }
+ header?: never
+ path: {
+ /** @description Project ref */
+ ref: string
+ }
+ cookie?: never
+ }
+ requestBody?: never
+ responses: {
+ 200: {
+ headers: {
+ [name: string]: unknown
+ }
+ content: {
+ 'application/json': components['schemas']['LegacyApiKeysResponse']
+ }
+ }
+ 403: {
+ headers: {
+ [name: string]: unknown
+ }
+ content?: never
+ }
+ }
+ }
'v1-list-project-addons': {
parameters: {
query?: never
diff --git a/packages/api-types/types/platform.d.ts b/packages/api-types/types/platform.d.ts
index db25f31163c..5b677bd4128 100644
--- a/packages/api-types/types/platform.d.ts
+++ b/packages/api-types/types/platform.d.ts
@@ -4251,7 +4251,7 @@ export interface components {
/** @default 0 */
recoveryTimeTarget?: number
}
- ConfirmSubscriptionChangeBody: {
+ ConfirmCreateSubscriptionChangeBody: {
kind?: string
name: string
payment_intent_id: string
@@ -4554,6 +4554,7 @@ export interface components {
is_owner: boolean
name: string
opt_in_tags: string[]
+ organization_requires_mfa: boolean
plan: {
/** @enum {string} */
id: 'free' | 'pro' | 'team' | 'enterprise'
@@ -6151,6 +6152,7 @@ export interface components {
is_owner: boolean
name: string
opt_in_tags: string[]
+ organization_requires_mfa: boolean
plan: {
/** @enum {string} */
id: 'free' | 'pro' | 'team' | 'enterprise'
@@ -7274,8 +7276,9 @@ export interface components {
}[]
}
RunQueryBody: {
- query: string
+ /** @default false */
disable_statement_timeout?: boolean
+ query: string
}
SearchProfileBody: {
keywords: string
@@ -8032,7 +8035,11 @@ export interface components {
/** Format: email */
billing_email?: string
name?: string
- opt_in_tags?: 'AI_SQL_GENERATOR_OPT_IN'[]
+ opt_in_tags?: (
+ | 'AI_SQL_GENERATOR_OPT_IN'
+ | 'AI_DATA_GENERATOR_OPT_IN'
+ | 'AI_LOG_GENERATOR_OPT_IN'
+ )[]
}
UpdateOrganizationResponse: {
billing_email?: string
@@ -12047,7 +12054,7 @@ export interface operations {
}
requestBody: {
content: {
- 'application/json': components['schemas']['ConfirmSubscriptionChangeBody']
+ 'application/json': components['schemas']['ConfirmCreateSubscriptionChangeBody']
}
}
responses: {