ToggleLegacyApiKeys.tsx238 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useParams } from 'common'
3import { useState } from 'react'
4import { toast } from 'sonner'
5import {
6 AlertDialog,
7 AlertDialogAction,
8 AlertDialogCancel,
9 AlertDialogContent,
10 AlertDialogDescription,
11 AlertDialogFooter,
12 AlertDialogHeader,
13 AlertDialogTitle,
14} from 'ui'
15
16import Panel from '../Panel'
17import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
18import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper'
19import { useToggleLegacyAPIKeysMutation } from '@/data/api-keys/legacy-api-key-toggle-mutation'
20import { useLegacyAPIKeysStatusQuery } from '@/data/api-keys/legacy-api-keys-status-query'
21import { useLegacyJWTSigningKeyQuery } from '@/data/jwt-signing-keys/legacy-jwt-signing-key-query'
22import { useAuthorizedAppsQuery } from '@/data/oauth/authorized-apps-query'
23import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
24import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
25
26export 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
161const 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}