ToggleLegacyApiKeys.tsx 8.6 KB

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