Files
Ali Waseem e8f9359a98 fix(studio): hide legacy API keys on High Availability projects (#49552)
Multigres (High Availability) projects ship with legacy API keys
disabled and the management API rejects re-enabling them, so the
Dashboard should not surface them at all.

Hides the "Legacy anon, service_role API keys" tab, renders an empty
state on the legacy page for direct URL access, and drops the "Copy
anonymous API key" / "Copy service API key" commands from the command
menu. Non-HA projects are unchanged.

Fixes FE-4276

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* High Availability projects now hide legacy API key navigation and
commands.
* Legacy API key settings display an unavailable message for High
Availability projects.
  * Publishable and secret key options remain available where supported.
* Other projects continue to provide access to supported legacy API key
options.

* **Bug Fixes**
* Prevented unsupported legacy API key controls from appearing on High
Availability projects.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-08-26 10:17:28 +00:00

185 lines
5.5 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Key } from 'lucide-react'
import { useMemo } from 'react'
import { toast } from 'sonner'
import { Badge, copyToClipboard } from 'ui'
import type { ICommand } from 'ui-patterns/CommandMenu'
import {
PageType,
useRegisterCommands,
useRegisterPage,
useResetCommandMenu,
useSetCommandMenuOpen,
useSetPage,
} from 'ui-patterns/CommandMenu'
import { COMMAND_MENU_SECTIONS } from './CommandMenu.utils'
import { orderCommandSectionsByPriority } from './ordering'
import { useAPIKeys } from '@/data/api-keys/api-keys-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useHighAvailability } from '@/hooks/misc/useHighAvailability'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
const API_KEYS_PAGE_NAME = 'API Keys'
export function useApiKeysCommands() {
const setIsOpen = useSetCommandMenuOpen()
const resetCommandMenu = useResetCommandMenu()
const setPage = useSetPage()
const { data: project } = useSelectedProjectQuery()
const ref = project?.ref || '_'
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
const { isHighAvailability } = useHighAvailability()
const { data: apiKeysData } = useAPIKeys(
{ projectRef: project?.ref, reveal: true },
{ enabled: canReadAPIKeys }
)
const commands = useMemo(() => {
const {
anonKey: legacyAnonKey,
serviceKey: legacyServiceKey,
publishableKey,
allSecretKeys,
} = canReadAPIKeys ? (apiKeysData ?? {}) : {}
const anonKey = isHighAvailability ? undefined : legacyAnonKey
const serviceKey = isHighAvailability ? undefined : legacyServiceKey
return [
project &&
publishableKey && {
id: 'publishable-key',
name: `Copy publishable key`,
action: () => {
copyToClipboard(publishableKey.api_key ?? '', () => {
toast.success('Publishable key copied to clipboard')
})
setIsOpen(false)
resetCommandMenu()
},
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge>{publishableKey.type}</Badge>
</span>
),
icon: () => <Key />,
},
...(project && allSecretKeys
? allSecretKeys.map((key) => ({
id: key.id,
name: `Copy secret key (${key.name})`,
action: () => {
copyToClipboard(key.api_key ?? '', () => {
toast.success('Secret key copied to clipboard')
})
setIsOpen(false)
resetCommandMenu()
},
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge>{key.type}</Badge>
</span>
),
icon: () => <Key />,
}))
: []),
project &&
anonKey && {
id: 'anon-key',
name: `Copy anonymous API key`,
action: () => {
copyToClipboard(anonKey.api_key ?? '', () => {
toast.success('Anonymous API key copied to clipboard')
})
setIsOpen(false)
resetCommandMenu()
},
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge>Public</Badge>
<Badge>{anonKey.type}</Badge>
</span>
),
icon: () => <Key />,
},
project &&
serviceKey && {
id: 'service-key',
name: `Copy service API key`,
action: () => {
copyToClipboard(serviceKey.api_key ?? '', () => {
toast.success('Service key copied to clipboard')
})
setIsOpen(false)
resetCommandMenu()
},
badge: () => (
<span className="flex items-center gap-x-1">
<Badge>Project: {project?.name}</Badge>
<Badge variant="destructive">Secret</Badge>
<Badge>{serviceKey.type}</Badge>
</span>
),
icon: () => <Key />,
},
!(anonKey || serviceKey) && {
id: 'api-keys-project-settings',
name: 'See API keys in Project Settings',
route: `/project/${ref}/settings/api-keys`,
icon: () => <Key />,
},
].filter(Boolean) as ICommand[]
}, [canReadAPIKeys, apiKeysData, isHighAvailability, project, ref, resetCommandMenu, setIsOpen])
useRegisterPage(
API_KEYS_PAGE_NAME,
{
type: PageType.Commands,
sections: [
{
id: 'api-keys',
name: 'API keys',
commands,
},
],
},
{ deps: [commands], enabled: !!project && commands.length > 0 }
)
useRegisterCommands(
COMMAND_MENU_SECTIONS.ACTIONS,
[
{
id: 'api-keys',
name: 'Get API keys...',
action: () => setPage(API_KEYS_PAGE_NAME),
icon: () => <Key />,
},
],
{
enabled: !!project && commands.length > 0,
orderSection: orderCommandSectionsByPriority,
sectionMeta: { priority: 3 },
}
)
useRegisterCommands(
COMMAND_MENU_SECTIONS.NAVIGATE,
[
{
id: 'nav-api-keys',
name: 'Go to API Keys',
route: `/project/${ref}/settings/api-keys`,
icon: () => <Key />,
},
],
{ enabled: !!project }
)
}