ApiKeyPill.tsx 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. import { PermissionAction } from '@supabase/shared-types/out/constants'
  2. import { InputVariants } from '@ui/components/shadcn/ui/input'
  3. import { useParams } from 'common'
  4. import { Eye, EyeOff } from 'lucide-react'
  5. import { useEffect, useState } from 'react'
  6. import { toast } from 'sonner'
  7. import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
  8. import { useRevealedSecret } from './useRevealedSecret'
  9. import CopyButton from '@/components/ui/CopyButton'
  10. import { APIKeysData } from '@/data/api-keys/api-keys-query'
  11. import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
  12. export function ApiKeyPill({
  13. apiKey,
  14. }: {
  15. apiKey: Extract<APIKeysData[number], { type: 'secret' | 'publishable' }>
  16. }) {
  17. const { ref: projectRef } = useParams()
  18. const [show, setShow] = useState(false)
  19. const isSecret = apiKey.type === 'secret'
  20. const { can: canManageSecretKeys, isLoading: isLoadingPermission } = useAsyncCheckPermissions(
  21. PermissionAction.READ,
  22. 'service_api_keys'
  23. )
  24. const {
  25. data: revealedKey,
  26. isLoading,
  27. reveal,
  28. clear,
  29. } = useRevealedSecret({
  30. projectRef,
  31. id: apiKey.id as string,
  32. })
  33. // Auto-hide timer for the API key (security feature)
  34. useEffect(() => {
  35. if (show && revealedKey) {
  36. const timer = setTimeout(() => {
  37. setShow(false)
  38. clear()
  39. }, 10000)
  40. return () => clearTimeout(timer)
  41. }
  42. }, [show, revealedKey, clear])
  43. async function onToggleShow() {
  44. if (isSecret && !canManageSecretKeys) return
  45. if (isLoadingPermission) return
  46. if (show) {
  47. setShow(false)
  48. clear()
  49. } else {
  50. setShow(true)
  51. try {
  52. await reveal()
  53. } catch {
  54. toast.error('Failed to reveal secret API key')
  55. setShow(false)
  56. }
  57. }
  58. }
  59. async function onCopy() {
  60. if (!isSecret) return apiKey.api_key
  61. if (revealedKey) return revealedKey
  62. try {
  63. const key = await reveal()
  64. clear()
  65. return key ?? ''
  66. } catch {
  67. toast.error('Failed to copy secret API key')
  68. return ''
  69. }
  70. }
  71. const isRestricted = isSecret && !canManageSecretKeys
  72. return (
  73. <>
  74. <div
  75. className={cn(
  76. InputVariants({ size: 'tiny' }),
  77. 'w-[100px] sm:w-[140px] md:w-[180px] lg:w-[340px] gap-0 font-mono rounded-full',
  78. isSecret ? 'overflow-hidden' : '',
  79. show ? 'ring-1 ring-foreground-lighter/50' : 'ring-0 ring-foreground-lighter/0',
  80. 'transition-all cursor-text relative'
  81. )}
  82. style={{ userSelect: 'all' }}
  83. >
  84. {isSecret ? (
  85. <>
  86. <span>{apiKey?.api_key.slice(0, 15)}</span>
  87. <span>{show && revealedKey ? revealedKey.slice(15) : '••••••••••••••••'}</span>
  88. </>
  89. ) : (
  90. <span title={apiKey.api_key} className="truncate">
  91. {apiKey.api_key}
  92. </span>
  93. )}
  94. </div>
  95. {/* Toggle button */}
  96. {isSecret && (
  97. <Tooltip>
  98. <TooltipTrigger asChild>
  99. <Button
  100. type="outline"
  101. className="rounded-full px-2 pointer-events-auto"
  102. loading={show && isLoading}
  103. icon={show ? <EyeOff strokeWidth={2} /> : <Eye strokeWidth={2} />}
  104. onClick={onToggleShow}
  105. disabled={isRestricted}
  106. />
  107. </TooltipTrigger>
  108. <TooltipContent side="bottom">
  109. {isRestricted
  110. ? 'You need additional permissions to reveal secret API keys'
  111. : isLoadingPermission
  112. ? 'Loading permissions...'
  113. : show
  114. ? 'Hide API key'
  115. : 'Reveal API key'}
  116. </TooltipContent>
  117. </Tooltip>
  118. )}
  119. <Tooltip>
  120. <TooltipTrigger asChild>
  121. <CopyButton
  122. type="default"
  123. asyncText={onCopy}
  124. iconOnly
  125. className="rounded-full px-2 pointer-events-auto"
  126. disabled={isRestricted || isLoadingPermission}
  127. />
  128. </TooltipTrigger>
  129. <TooltipContent side="bottom">
  130. {isRestricted
  131. ? 'You need additional permissions to copy secret API keys'
  132. : isLoadingPermission
  133. ? 'Loading permissions...'
  134. : 'Copy API key'}
  135. </TooltipContent>
  136. </Tooltip>
  137. </>
  138. )
  139. }