PublishableAPIKeys.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { useParams } from 'common'
  3. import { parseAsString, useQueryState } from 'nuqs'
  4. import { useEffect, useMemo } from 'react'
  5. import { toast } from 'sonner'
  6. import {
  7. Card,
  8. Table,
  9. TableBody,
  10. TableCell,
  11. TableFooter,
  12. TableHead,
  13. TableHeader,
  14. TableRow,
  15. } from 'ui'
  16. import { Admonition, GenericSkeletonLoader } from 'ui-patterns'
  17. import { APIKeyRow } from './APIKeyRow'
  18. import { CreatePublishableAPIKeyDialog } from './CreatePublishableAPIKeyDialog'
  19. import { AlertError } from '@/components/ui/AlertError'
  20. import { FormHeader } from '@/components/ui/Forms/FormHeader'
  21. import { NoPermission } from '@/components/ui/NoPermission'
  22. import { useAPIKeyDeleteMutation } from '@/data/api-keys/api-key-delete-mutation'
  23. import { APIKeysData, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
  24. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  25. export const PublishableAPIKeys = () => {
  26. const { ref: projectRef } = useParams()
  27. const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
  28. PermissionAction.SECRETS_READ,
  29. '*'
  30. )
  31. const {
  32. data: apiKeysData = [],
  33. error,
  34. isSuccess: isSuccessApiKeys,
  35. isPending: isLoadingApiKeys,
  36. isError: isErrorApiKeys,
  37. } = useAPIKeysQuery({ projectRef, reveal: false }, { enabled: canReadAPIKeys })
  38. const newApiKeys = useMemo(
  39. () => apiKeysData.filter(({ type }) => type === 'publishable' || type === 'secret') ?? [],
  40. [apiKeysData]
  41. )
  42. const hasApiKeys = newApiKeys.length > 0
  43. const publishableApiKeys = useMemo(
  44. () =>
  45. apiKeysData?.filter(
  46. (key): key is Extract<APIKeysData[number], { type: 'publishable' }> =>
  47. key.type === 'publishable'
  48. ) ?? [],
  49. [apiKeysData]
  50. )
  51. const [deleteId, setDeleteId] = useQueryState('deletePublishableKey', parseAsString)
  52. const apiKeyToDelete = publishableApiKeys?.find((key) => key.id === deleteId)
  53. const {
  54. mutate: deleteAPIKey,
  55. isPending: isDeletingAPIKey,
  56. isSuccess: isDeleteSuccess,
  57. } = useAPIKeyDeleteMutation({
  58. onSuccess: () => {
  59. toast.success('Successfully deleted publishable key')
  60. setDeleteId(null)
  61. },
  62. })
  63. const onDeleteAPIKey = (apiKey: Extract<APIKeysData[number], { type: 'publishable' }>) => {
  64. if (!projectRef) return console.error('Project ref is required')
  65. if (!apiKey.id) return console.error('API key ID is required')
  66. deleteAPIKey({ projectRef, id: apiKey.id })
  67. }
  68. useEffect(() => {
  69. if (isSuccessApiKeys && !!deleteId && !apiKeyToDelete && !isDeleteSuccess) {
  70. toast('Unable to find publishable key')
  71. setDeleteId(null)
  72. }
  73. }, [apiKeyToDelete, deleteId, isDeleteSuccess, isSuccessApiKeys, setDeleteId])
  74. return (
  75. <div>
  76. <FormHeader
  77. title="Publishable key"
  78. description="This key is safe to use in a browser if you have enabled Row Level Security (RLS) for your tables and configured policies."
  79. actions={<CreatePublishableAPIKeyDialog />}
  80. />
  81. {!canReadAPIKeys && !isLoadingPermissions ? (
  82. <NoPermission resourceText="view API keys" />
  83. ) : isLoadingApiKeys || isLoadingPermissions ? (
  84. <GenericSkeletonLoader />
  85. ) : isErrorApiKeys ? (
  86. <AlertError error={error} subject="Failed to load API keys" />
  87. ) : (
  88. <Card className="bg-surface-100">
  89. <Table>
  90. <TableHeader>
  91. <TableRow className="bg-200">
  92. <TableHead>Name</TableHead>
  93. <TableHead>API Key</TableHead>
  94. <TableHead />
  95. </TableRow>
  96. </TableHeader>
  97. <TableBody>
  98. {hasApiKeys && publishableApiKeys.length === 0 && (
  99. <TableRow>
  100. <TableCell colSpan={3} className="p-0">
  101. <Admonition showIcon={false} type="default" className="border-0 rounded-none">
  102. <p className="text-foreground-light">No publishable keys created yet</p>
  103. </Admonition>
  104. </TableCell>
  105. </TableRow>
  106. )}
  107. {publishableApiKeys.map((apiKey) => (
  108. <APIKeyRow
  109. showLastSeen={false}
  110. key={apiKey.id}
  111. apiKey={apiKey}
  112. isDeleting={apiKeyToDelete?.id === apiKey.id && isDeletingAPIKey}
  113. isDeleteModalOpen={apiKeyToDelete?.id === apiKey.id}
  114. onDelete={() => onDeleteAPIKey(apiKey)}
  115. setKeyToDelete={setDeleteId}
  116. />
  117. ))}
  118. </TableBody>
  119. <TableFooter className="border-t">
  120. <TableRow className="border-b-0">
  121. <TableCell colSpan={3} className="py-2">
  122. <p className="text-xs text-foreground-lighter font-normal">
  123. Publishable keys can be safely shared publicly
  124. </p>
  125. </TableCell>
  126. </TableRow>
  127. </TableFooter>
  128. </Table>
  129. </Card>
  130. )}
  131. </div>
  132. )
  133. }