Files
supabase/apps/studio/components/ui/ProjectSettings/ToggleLegacyApiKeys.tsx
Gildas Garcia 96d43099bb chore: refactor Button API so that it can be used a standard button (#46880)
## Problem

Our `<Button>` component breaks the default `button` contract by
redefining the `type` prop to set its variant (`primary`, `default`,
etc) instead of the button type (`submit`, `button`, etc).
This is confusing and forces to write more code when using it with
shadcn components that expect/inject the standard button props.

## Solution

- rename the `type` prop to `variant`
- rename the `htmlType` prop to `type`
- propagate the changes where necessary
- format code

## How to test

As this is just prop renaming, if it builds it's ok

---------

Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
2026-06-16 23:59:58 +02:00

239 lines
8.6 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { useState } from 'react'
import { toast } from 'sonner'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from 'ui'
import Panel from '../Panel'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
import { useToggleLegacyAPIKeysMutation } from '@/data/api-keys/legacy-api-key-toggle-mutation'
import { useLegacyAPIKeysStatusQuery } from '@/data/api-keys/legacy-api-keys-status-query'
import { useLegacyJWTSigningKeyQuery } from '@/data/jwt-signing-keys/legacy-jwt-signing-key-query'
import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
export const ToggleLegacyApiKeysPanel = () => {
const { ref: projectRef } = useParams()
const { data: org } = useSelectedOrganizationQuery()
const [isConfirmOpen, setIsConfirmOpen] = useState(false)
const [isAppsWarningOpen, setIsAppsWarningOpen] = useState(false)
const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*')
const { can: canUpdateAPIKeys, isSuccess: isPermissionsSuccess } = useAsyncCheckPermissions(
PermissionAction.SECRETS_WRITE,
'*'
)
const { data: legacyAPIKeysStatusData, isSuccess: isLegacyAPIKeysStatusSuccess } =
useLegacyAPIKeysStatusQuery({ projectRef }, { enabled: canReadAPIKeys })
const { data: legacyJWTSecret } = useLegacyJWTSigningKeyQuery(
{ projectRef },
{ enabled: canReadAPIKeys }
)
const { data: authorizedApps = [], isError: isAuthorizedAppsError } = useAuthorizedAppsQuery({
slug: org?.slug,
})
const { enabled: isLegacyKeysEnabled } = legacyAPIKeysStatusData || {}
const oauthAppsLink = (
<a
href={`/dashboard/org/${org?.slug}/apps`}
target="_blank"
rel="noreferrer"
className="underline"
>
OAuth apps
</a>
)
const appsWarning = isAuthorizedAppsError
? {
title: 'Check your OAuth apps before continuing',
description: (
<>
Disabling legacy API keys can break apps that integrate with Supabase. Before
continuing, check your organization's {oauthAppsLink} to ensure none of them depend on
the legacy API keys.
</>
),
}
: {
title: 'Apps using Supabase may break',
description: (
<>
Your project uses apps that integrate with Supabase. Disabling the legacy API keys is a
brand new feature and the apps you're using may not have added support for this yet. It
can cause them to stop functioning. Check your {oauthAppsLink} before continuing.
</>
),
}
if (!(isLegacyAPIKeysStatusSuccess && isPermissionsSuccess)) {
return null
}
return (
<section>
<Panel>
<Panel.Content>
<div className="flex justify-between">
<div className="flex flex-col gap-2">
<p className="text-sm">
{isLegacyKeysEnabled ? 'Disable legacy API keys' : 'Re-enabling legacy API keys'}
</p>
<p className="text-foreground-light text-sm">
{isLegacyKeysEnabled
? 'Make sure you are no longer using your legacy API keys before proceeding.'
: 'We recommend you use the new API keys whenever possible, but re-enabling is an option.'}
</p>
</div>
<div className="flex items-center">
<ButtonTooltip
variant="default"
onClick={
isLegacyKeysEnabled && (authorizedApps?.length || isAuthorizedAppsError)
? () => setIsAppsWarningOpen(true)
: () => setIsConfirmOpen(true)
}
disabled={
!canUpdateAPIKeys ||
(!isLegacyKeysEnabled && legacyJWTSecret?.status === 'revoked')
}
tooltip={{
content: {
side: 'bottom',
text: !canUpdateAPIKeys
? 'You need additional permissions to enable or disable JWT-based API keys'
: !isLegacyKeysEnabled && legacyJWTSecret?.status === 'revoked'
? 'The legacy JWT secret is revoked. Re-enabling is not possible until it is at least moved to previously used.'
: undefined,
},
}}
>
{legacyAPIKeysStatusData.enabled
? 'Disable JWT-based API keys'
: 'Re-enable JWT-based API keys'}
</ButtonTooltip>
</div>
</div>
</Panel.Content>
</Panel>
<ToggleApiKeysModal
visible={isConfirmOpen}
onClose={() => setIsConfirmOpen(false)}
legacyAPIKeysStatusData={legacyAPIKeysStatusData}
/>
<AlertDialog open={isAppsWarningOpen} onOpenChange={(value) => setIsAppsWarningOpen(value)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{appsWarning.title}</AlertDialogTitle>
<AlertDialogDescription>{appsWarning.description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction variant="danger" onClick={() => setIsConfirmOpen(true)}>
Disable API keys
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</section>
)
}
const ToggleApiKeysModal = ({
visible,
onClose,
legacyAPIKeysStatusData,
}: {
visible: boolean
onClose: () => void
legacyAPIKeysStatusData: { enabled: boolean }
}) => {
const { ref: projectRef } = useParams()
const { enabled: isLegacyKeysEnabled } = legacyAPIKeysStatusData || {}
const { mutate: toggleLegacyAPIKey, isPending: isTogglingLegacyAPIKey } =
useToggleLegacyAPIKeysMutation()
const onToggleLegacyAPIKeysEnabled = () => {
const enabled = !legacyAPIKeysStatusData?.enabled
toggleLegacyAPIKey(
{ projectRef, 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 (
<TextConfirmModal
size="medium"
visible={visible}
onCancel={() => onClose()}
onConfirm={onToggleLegacyAPIKeysEnabled}
title={isLegacyKeysEnabled ? 'Disable JWT-based keys' : 'Re-enable JWT-based keys'}
confirmString={isLegacyKeysEnabled ? 'disable' : 're-enable'}
confirmLabel={`Confirm to ${isLegacyKeysEnabled ? 'disable' : 're-enable'} anon and service_role`}
confirmPlaceholder={isLegacyKeysEnabled ? 'disable' : 're-enable'}
loading={isTogglingLegacyAPIKey}
variant={isLegacyKeysEnabled ? 'destructive' : 'default'}
alert={
isLegacyKeysEnabled
? {
title: 'Ensure legacy keys are no longer in use before disabling',
description: (
<span className="prose text-sm">
Disabling <code>anon</code> and <code>service_role</code> keys while they are in
use will cause downtime for your application. Ensure they are no longer in use
before proceeding. If you have not created a publishable and at least one secret
API key, some dashboard functionality may become unavailable.
<br />
<br />
<span className="text-danger">
This disables API keys when used in the <code>apikey</code> header. They remain
valid as a JWT.
</span>
</span>
),
}
: {
title: 'Publishable and secret keys are preferred',
description: (
<span className="prose text-sm">
Re-enabling <code>anon</code> and <code>service_role</code> keys may be
appropriate in certain cases, but using a publishable and secret key is more
secure. We recommend against re-enabling legacy API keys.
</span>
),
}
}
/>
)
}