SecretAPIKeys.tsx 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { useFlag, useParams } from 'common'
  3. import dayjs from 'dayjs'
  4. import { parseAsString, useQueryState } from 'nuqs'
  5. import { useEffect, useMemo, useRef } from 'react'
  6. import { toast } from 'sonner'
  7. import { Card } from 'ui'
  8. import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
  9. import {
  10. Table,
  11. TableBody,
  12. TableHead,
  13. TableHeader,
  14. TableRow,
  15. } from 'ui/src/components/shadcn/ui/table'
  16. import { APIKeyRow } from './APIKeyRow'
  17. import { CreateSecretAPIKeyDialog } from './CreateSecretAPIKeyDialog'
  18. import { AlertError } from '@/components/ui/AlertError'
  19. import { FormHeader } from '@/components/ui/Forms/FormHeader'
  20. import { NoPermission } from '@/components/ui/NoPermission'
  21. import { useAPIKeyDeleteMutation } from '@/data/api-keys/api-key-delete-mutation'
  22. import type { APIKeysData } from '@/data/api-keys/api-keys-query'
  23. import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
  24. import { useLogsQuery } from '@/hooks/analytics/useLogsQuery'
  25. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  26. interface LastSeenData {
  27. [hash: string]: { timestamp: number; relative: string }
  28. }
  29. function useLastSeen({ projectRef, enabled }: { projectRef: string; enabled?: boolean }): {
  30. data?: LastSeenData
  31. isLoading: boolean
  32. } {
  33. const now = useRef(new Date()).current
  34. const query = useLogsQuery(
  35. projectRef,
  36. {
  37. iso_timestamp_start: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(),
  38. iso_timestamp_end: now.toISOString(),
  39. sql: "-- last-used-secret-api-keys\nSELECT unix_millis(max(timestamp)) as timestamp, apikey.`hash` FROM edge_logs cross join unnest(metadata) as m cross join unnest(m.request) as request cross join unnest(request.sb) as sb cross join unnest(sb.apikey) as sbapikey cross join unnest(sbapikey.apikey) as apikey WHERE apikey.error is null and apikey.`hash` is not null and apikey.prefix like 'sb_secret_%' GROUP BY apikey.`hash`",
  40. },
  41. enabled
  42. )
  43. return useMemo(() => {
  44. if (query.isLoading || !query.logData) {
  45. return { data: undefined, isLoading: query.isLoading }
  46. }
  47. const now = dayjs()
  48. const lastSeen = (query.logData as unknown as { timestamp: number; hash: string }[]).reduce(
  49. (a, i) => {
  50. a[i.hash] = {
  51. timestamp: i.timestamp,
  52. relative: `${dayjs.duration(now.diff(dayjs(i.timestamp))).humanize(false)} ago`,
  53. }
  54. return a
  55. },
  56. {} as LastSeenData
  57. )
  58. return { data: lastSeen, isLoading: query.isLoading }
  59. }, [query])
  60. }
  61. export const SecretAPIKeys = () => {
  62. const { ref: projectRef } = useParams()
  63. const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
  64. PermissionAction.SECRETS_READ,
  65. '*'
  66. )
  67. const {
  68. data: apiKeysData,
  69. error,
  70. isSuccess: isSuccessApiKeys,
  71. isPending: isLoadingApiKeys,
  72. isError: isErrorApiKeys,
  73. } = useAPIKeysQuery({ projectRef, reveal: false }, { enabled: canReadAPIKeys })
  74. const showApiKeysLastUsed = useFlag('showApiKeysLastUsed')
  75. const { data: lastSeen, isLoading: isLoadingLastSeen } = useLastSeen({
  76. projectRef: projectRef ?? '',
  77. enabled: showApiKeysLastUsed,
  78. })
  79. const secretApiKeys = useMemo(
  80. () =>
  81. apiKeysData?.filter(
  82. (key): key is Extract<APIKeysData[number], { type: 'secret' }> => key.type === 'secret'
  83. ) ?? [],
  84. [apiKeysData]
  85. )
  86. const empty = secretApiKeys?.length === 0 && !isLoadingApiKeys && !isLoadingPermissions
  87. const [deleteId, setDeleteId] = useQueryState('deleteSecretKey', parseAsString)
  88. const apiKeyToDelete = secretApiKeys?.find((key) => key.id === deleteId)
  89. const {
  90. mutate: deleteAPIKey,
  91. isPending: isDeletingAPIKey,
  92. isSuccess: isDeleteSuccess,
  93. } = useAPIKeyDeleteMutation({
  94. onSuccess: () => {
  95. toast.success('Successfully deleted secret key')
  96. setDeleteId(null)
  97. },
  98. })
  99. const onDeleteAPIKey = (apiKey: Extract<APIKeysData[number], { type: 'secret' }>) => {
  100. if (!projectRef) return console.error('Project ref is required')
  101. if (!apiKey.id) return console.error('API key ID is required')
  102. deleteAPIKey({ projectRef, id: apiKey.id })
  103. }
  104. useEffect(() => {
  105. if (isSuccessApiKeys && !!deleteId && !apiKeyToDelete && !isDeleteSuccess) {
  106. toast('Unable to find secret key')
  107. setDeleteId(null)
  108. }
  109. }, [apiKeyToDelete, deleteId, isDeleteSuccess, isSuccessApiKeys, setDeleteId])
  110. return (
  111. <div className="pb-30">
  112. <FormHeader
  113. title="Secret keys"
  114. description="These API keys allow privileged access to your project's APIs. Use in servers, functions, workers or other backend components of your application."
  115. actions={<CreateSecretAPIKeyDialog />}
  116. />
  117. {!canReadAPIKeys && !isLoadingPermissions ? (
  118. <NoPermission resourceText="view API keys" />
  119. ) : isLoadingApiKeys || isLoadingPermissions ? (
  120. <GenericSkeletonLoader />
  121. ) : isErrorApiKeys ? (
  122. <AlertError error={error} subject="Failed to load secret API keys" />
  123. ) : empty ? (
  124. <Card>
  125. <div className="rounded-b-md! overflow-hidden py-12 flex flex-col gap-1 items-center justify-center">
  126. <p className="text-sm text-foreground">No secret API keys found</p>
  127. <p className="text-sm text-foreground-light">
  128. Your project is not accessible via secret keys—there are no active secret keys
  129. created.
  130. </p>
  131. </div>
  132. </Card>
  133. ) : (
  134. <Card className="bg-surface-100">
  135. <Table>
  136. <TableHeader>
  137. <TableRow className="bg-200">
  138. <TableHead>Name</TableHead>
  139. <TableHead>API Key</TableHead>
  140. {showApiKeysLastUsed && (
  141. <TableHead className="hidden lg:table-cell">Last Used</TableHead>
  142. )}
  143. <TableHead />
  144. </TableRow>
  145. </TableHeader>
  146. <TableBody>
  147. {secretApiKeys.map((apiKey) => (
  148. <APIKeyRow
  149. key={apiKey.id}
  150. apiKey={apiKey}
  151. lastSeen={lastSeen?.[apiKey.hash]}
  152. isLoadingLastSeen={isLoadingLastSeen}
  153. isDeleting={apiKeyToDelete?.id === apiKey.id && isDeletingAPIKey}
  154. onDelete={() => onDeleteAPIKey(apiKey)}
  155. setKeyToDelete={setDeleteId}
  156. isDeleteModalOpen={apiKeyToDelete?.id === apiKey.id}
  157. />
  158. ))}
  159. </TableBody>
  160. </Table>
  161. </Card>
  162. )}
  163. </div>
  164. )
  165. }