mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## Summary - Fix `ToggleLegacyApiKeysPanel` being permanently hidden for users with project-level admin access but no org-level access - The `useAuthorizedAppsQuery` calls an org-level endpoint (`/platform/organizations/{slug}/oauth/apps`) which returns 403 for project-only admins, causing `isAuthorizedAppsSuccess` to never become `true` and the entire panel to never render - When the authorized apps query fails, show a fallback warning directing users to verify their org's OAuth apps before disabling legacy keys ## What changed - Removed `isAuthorizedAppsSuccess` from the rendering guard, the panel now renders once legacy keys status and permissions resolve - When the authorized apps query errors (e.g. 403), the button still opens a warning dialog with appropriate copy before proceeding to the confirmation modal ## Behavior | User type | Authorized apps query | Button click | |---|---|---| | Org-level access, has apps | Success, apps > 0 | Warning → confirm modal | | Org-level access, no apps | Success, apps = 0 | Confirm modal directly | | Project-only admin | 403 error | Fallback warning → confirm modal | ## Current behavior <img width="1461" height="707" alt="image-IcxHfCX0" src="https://github.com/user-attachments/assets/2fd124cd-02eb-46c0-816e-178fe3ce99b0" /> Project admins can't view the button ## Changed behaaviour <img width="1455" height="704" alt="image-YW1k6GQe" src="https://github.com/user-attachments/assets/8c428c63-f1de-4b84-a1f2-6af7ff064e50" /> Projects admins can view the disable button and when clicked views a warning about oauth apps: <img width="451" height="250" alt="image-yu4bux3l" src="https://github.com/user-attachments/assets/5a314329-350b-4207-b8e4-311d0c827e6f" /> and when they visit oauth apps, there is warning to contact with project owner <img width="1453" height="578" alt="image-pppzfksn" src="https://github.com/user-attachments/assets/489ba3ba-c94e-4efb-923b-a989eebb2fc4" />
239 lines
8.6 KiB
TypeScript
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
|
|
type="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>
|
|
),
|
|
}
|
|
}
|
|
/>
|
|
)
|
|
}
|