ToggleLegacyApiKeys.tsx238 lines · main
| 1 | import { PermissionAction } from '@supabase/shared-types/out/constants' |
| 2 | import { useParams } from 'common' |
| 3 | import { useState } from 'react' |
| 4 | import { toast } from 'sonner' |
| 5 | import { |
| 6 | AlertDialog, |
| 7 | AlertDialogAction, |
| 8 | AlertDialogCancel, |
| 9 | AlertDialogContent, |
| 10 | AlertDialogDescription, |
| 11 | AlertDialogFooter, |
| 12 | AlertDialogHeader, |
| 13 | AlertDialogTitle, |
| 14 | } from 'ui' |
| 15 | |
| 16 | import Panel from '../Panel' |
| 17 | import { ButtonTooltip } from '@/components/ui/ButtonTooltip' |
| 18 | import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' |
| 19 | import { useToggleLegacyAPIKeysMutation } from '@/data/api-keys/legacy-api-key-toggle-mutation' |
| 20 | import { useLegacyAPIKeysStatusQuery } from '@/data/api-keys/legacy-api-keys-status-query' |
| 21 | import { useLegacyJWTSigningKeyQuery } from '@/data/jwt-signing-keys/legacy-jwt-signing-key-query' |
| 22 | import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query' |
| 23 | import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' |
| 24 | import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization' |
| 25 | |
| 26 | export const ToggleLegacyApiKeysPanel = () => { |
| 27 | const { ref: projectRef } = useParams() |
| 28 | const { data: org } = useSelectedOrganizationQuery() |
| 29 | |
| 30 | const [isConfirmOpen, setIsConfirmOpen] = useState(false) |
| 31 | const [isAppsWarningOpen, setIsAppsWarningOpen] = useState(false) |
| 32 | |
| 33 | const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.SECRETS_READ, '*') |
| 34 | const { can: canUpdateAPIKeys, isSuccess: isPermissionsSuccess } = useAsyncCheckPermissions( |
| 35 | PermissionAction.SECRETS_WRITE, |
| 36 | '*' |
| 37 | ) |
| 38 | |
| 39 | const { data: legacyAPIKeysStatusData, isSuccess: isLegacyAPIKeysStatusSuccess } = |
| 40 | useLegacyAPIKeysStatusQuery({ projectRef }, { enabled: canReadAPIKeys }) |
| 41 | |
| 42 | const { data: legacyJWTSecret } = useLegacyJWTSigningKeyQuery( |
| 43 | { projectRef }, |
| 44 | { enabled: canReadAPIKeys } |
| 45 | ) |
| 46 | |
| 47 | const { data: authorizedApps = [], isError: isAuthorizedAppsError } = useAuthorizedAppsQuery({ |
| 48 | slug: org?.slug, |
| 49 | }) |
| 50 | |
| 51 | const { enabled: isLegacyKeysEnabled } = legacyAPIKeysStatusData || {} |
| 52 | |
| 53 | const oauthAppsLink = ( |
| 54 | <a |
| 55 | href={`/dashboard/org/${org?.slug}/apps`} |
| 56 | target="_blank" |
| 57 | rel="noreferrer" |
| 58 | className="underline" |
| 59 | > |
| 60 | OAuth apps |
| 61 | </a> |
| 62 | ) |
| 63 | |
| 64 | const appsWarning = isAuthorizedAppsError |
| 65 | ? { |
| 66 | title: 'Check your OAuth apps before continuing', |
| 67 | description: ( |
| 68 | <> |
| 69 | Disabling legacy API keys can break apps that integrate with Briven. Before |
| 70 | continuing, check your organization's {oauthAppsLink} to ensure none of them depend on |
| 71 | the legacy API keys. |
| 72 | </> |
| 73 | ), |
| 74 | } |
| 75 | : { |
| 76 | title: 'Apps using Briven may break', |
| 77 | description: ( |
| 78 | <> |
| 79 | Your project uses apps that integrate with Briven. Disabling the legacy API keys is a |
| 80 | brand new feature and the apps you're using may not have added support for this yet. It |
| 81 | can cause them to stop functioning. Check your {oauthAppsLink} before continuing. |
| 82 | </> |
| 83 | ), |
| 84 | } |
| 85 | |
| 86 | if (!(isLegacyAPIKeysStatusSuccess && isPermissionsSuccess)) { |
| 87 | return null |
| 88 | } |
| 89 | |
| 90 | return ( |
| 91 | <section> |
| 92 | <Panel> |
| 93 | <Panel.Content> |
| 94 | <div className="flex justify-between"> |
| 95 | <div className="flex flex-col gap-2"> |
| 96 | <p className="text-sm"> |
| 97 | {isLegacyKeysEnabled ? 'Disable legacy API keys' : 'Re-enabling legacy API keys'} |
| 98 | </p> |
| 99 | <p className="text-foreground-light text-sm"> |
| 100 | {isLegacyKeysEnabled |
| 101 | ? 'Make sure you are no longer using your legacy API keys before proceeding.' |
| 102 | : 'We recommend you use the new API keys whenever possible, but re-enabling is an option.'} |
| 103 | </p> |
| 104 | </div> |
| 105 | <div className="flex items-center"> |
| 106 | <ButtonTooltip |
| 107 | type="default" |
| 108 | onClick={ |
| 109 | isLegacyKeysEnabled && (authorizedApps?.length || isAuthorizedAppsError) |
| 110 | ? () => setIsAppsWarningOpen(true) |
| 111 | : () => setIsConfirmOpen(true) |
| 112 | } |
| 113 | disabled={ |
| 114 | !canUpdateAPIKeys || |
| 115 | (!isLegacyKeysEnabled && legacyJWTSecret?.status === 'revoked') |
| 116 | } |
| 117 | tooltip={{ |
| 118 | content: { |
| 119 | side: 'bottom', |
| 120 | text: !canUpdateAPIKeys |
| 121 | ? 'You need additional permissions to enable or disable JWT-based API keys' |
| 122 | : !isLegacyKeysEnabled && legacyJWTSecret?.status === 'revoked' |
| 123 | ? 'The legacy JWT secret is revoked. Re-enabling is not possible until it is at least moved to previously used.' |
| 124 | : undefined, |
| 125 | }, |
| 126 | }} |
| 127 | > |
| 128 | {legacyAPIKeysStatusData.enabled |
| 129 | ? 'Disable JWT-based API keys' |
| 130 | : 'Re-enable JWT-based API keys'} |
| 131 | </ButtonTooltip> |
| 132 | </div> |
| 133 | </div> |
| 134 | </Panel.Content> |
| 135 | </Panel> |
| 136 | |
| 137 | <ToggleApiKeysModal |
| 138 | visible={isConfirmOpen} |
| 139 | onClose={() => setIsConfirmOpen(false)} |
| 140 | legacyAPIKeysStatusData={legacyAPIKeysStatusData} |
| 141 | /> |
| 142 | |
| 143 | <AlertDialog open={isAppsWarningOpen} onOpenChange={(value) => setIsAppsWarningOpen(value)}> |
| 144 | <AlertDialogContent> |
| 145 | <AlertDialogHeader> |
| 146 | <AlertDialogTitle>{appsWarning.title}</AlertDialogTitle> |
| 147 | <AlertDialogDescription>{appsWarning.description}</AlertDialogDescription> |
| 148 | </AlertDialogHeader> |
| 149 | <AlertDialogFooter> |
| 150 | <AlertDialogCancel>Cancel</AlertDialogCancel> |
| 151 | <AlertDialogAction variant="danger" onClick={() => setIsConfirmOpen(true)}> |
| 152 | Disable API keys |
| 153 | </AlertDialogAction> |
| 154 | </AlertDialogFooter> |
| 155 | </AlertDialogContent> |
| 156 | </AlertDialog> |
| 157 | </section> |
| 158 | ) |
| 159 | } |
| 160 | |
| 161 | const ToggleApiKeysModal = ({ |
| 162 | visible, |
| 163 | onClose, |
| 164 | legacyAPIKeysStatusData, |
| 165 | }: { |
| 166 | visible: boolean |
| 167 | onClose: () => void |
| 168 | legacyAPIKeysStatusData: { enabled: boolean } |
| 169 | }) => { |
| 170 | const { ref: projectRef } = useParams() |
| 171 | const { enabled: isLegacyKeysEnabled } = legacyAPIKeysStatusData || {} |
| 172 | |
| 173 | const { mutate: toggleLegacyAPIKey, isPending: isTogglingLegacyAPIKey } = |
| 174 | useToggleLegacyAPIKeysMutation() |
| 175 | |
| 176 | const onToggleLegacyAPIKeysEnabled = () => { |
| 177 | const enabled = !legacyAPIKeysStatusData?.enabled |
| 178 | |
| 179 | toggleLegacyAPIKey( |
| 180 | { projectRef, enabled }, |
| 181 | { |
| 182 | onSuccess: () => { |
| 183 | toast.success( |
| 184 | enabled |
| 185 | ? 'Your anon and service_role keys have been re-enabled!' |
| 186 | : 'Your anon and service_role keys have been disabled!' |
| 187 | ) |
| 188 | onClose() |
| 189 | }, |
| 190 | } |
| 191 | ) |
| 192 | } |
| 193 | |
| 194 | return ( |
| 195 | <TextConfirmModal |
| 196 | size="medium" |
| 197 | visible={visible} |
| 198 | onCancel={() => onClose()} |
| 199 | onConfirm={onToggleLegacyAPIKeysEnabled} |
| 200 | title={isLegacyKeysEnabled ? 'Disable JWT-based keys' : 'Re-enable JWT-based keys'} |
| 201 | confirmString={isLegacyKeysEnabled ? 'disable' : 're-enable'} |
| 202 | confirmLabel={`Confirm to ${isLegacyKeysEnabled ? 'disable' : 're-enable'} anon and service_role`} |
| 203 | confirmPlaceholder={isLegacyKeysEnabled ? 'disable' : 're-enable'} |
| 204 | loading={isTogglingLegacyAPIKey} |
| 205 | variant={isLegacyKeysEnabled ? 'destructive' : 'default'} |
| 206 | alert={ |
| 207 | isLegacyKeysEnabled |
| 208 | ? { |
| 209 | title: 'Ensure legacy keys are no longer in use before disabling', |
| 210 | description: ( |
| 211 | <span className="prose text-sm"> |
| 212 | Disabling <code>anon</code> and <code>service_role</code> keys while they are in |
| 213 | use will cause downtime for your application. Ensure they are no longer in use |
| 214 | before proceeding. If you have not created a publishable and at least one secret |
| 215 | API key, some dashboard functionality may become unavailable. |
| 216 | <br /> |
| 217 | <br /> |
| 218 | <span className="text-danger"> |
| 219 | This disables API keys when used in the <code>apikey</code> header. They remain |
| 220 | valid as a JWT. |
| 221 | </span> |
| 222 | </span> |
| 223 | ), |
| 224 | } |
| 225 | : { |
| 226 | title: 'Publishable and secret keys are preferred', |
| 227 | description: ( |
| 228 | <span className="prose text-sm"> |
| 229 | Re-enabling <code>anon</code> and <code>service_role</code> keys may be |
| 230 | appropriate in certain cases, but using a publishable and secret key is more |
| 231 | secure. We recommend against re-enabling legacy API keys. |
| 232 | </span> |
| 233 | ), |
| 234 | } |
| 235 | } |
| 236 | /> |
| 237 | ) |
| 238 | } |