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