Files
supabase/apps/studio/components/interfaces/APIKeys/ApiKeyPill.tsx

157 lines
4.4 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { InputVariants } from '@ui/components/shadcn/ui/input'
import { useParams } from 'common'
import { Eye, EyeOff } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import { useRevealedSecret } from './useRevealedSecret'
import CopyButton from '@/components/ui/CopyButton'
import { APIKeysData } from '@/data/api-keys/api-keys-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
export function ApiKeyPill({
apiKey,
}: {
apiKey: Extract<APIKeysData[number], { type: 'secret' | 'publishable' }>
}) {
const { ref: projectRef } = useParams()
const [show, setShow] = useState(false)
const isSecret = apiKey.type === 'secret'
const { can: canManageSecretKeys, isLoading: isLoadingPermission } = useAsyncCheckPermissions(
PermissionAction.READ,
'service_api_keys'
)
const {
data: revealedKey,
isLoading,
reveal,
clear,
} = useRevealedSecret({
projectRef,
id: apiKey.id as string,
})
// Auto-hide timer for the API key (security feature)
useEffect(() => {
if (show && revealedKey) {
const timer = setTimeout(() => {
setShow(false)
clear()
}, 10000)
return () => clearTimeout(timer)
}
}, [show, revealedKey, clear])
async function onToggleShow() {
if (isSecret && !canManageSecretKeys) return
if (isLoadingPermission) return
if (show) {
setShow(false)
clear()
} else {
setShow(true)
try {
await reveal()
} catch {
toast.error('Failed to reveal secret API key')
setShow(false)
}
}
}
async function onCopy() {
if (!isSecret) return apiKey.api_key
if (revealedKey) return revealedKey
try {
const key = await reveal()
clear()
return key ?? ''
} catch {
toast.error('Failed to copy secret API key')
return ''
}
}
const isRestricted = isSecret && !canManageSecretKeys
return (
<>
<div
className={cn(
InputVariants({ size: 'tiny' }),
'w-[100px] sm:w-[140px] md:w-[180px] lg:w-[340px] gap-0 font-mono rounded-full',
isSecret ? 'overflow-hidden' : '',
show ? 'ring-1 ring-foreground-lighter/50' : 'ring-0 ring-foreground-lighter/0',
'transition-all cursor-text relative'
)}
style={{ userSelect: 'all' }}
>
{isSecret ? (
<>
<span>{apiKey?.api_key.slice(0, 15)}</span>
<span>{show && revealedKey ? revealedKey.slice(15) : '••••••••••••••••'}</span>
</>
) : (
<span title={apiKey.api_key} className="truncate">
{apiKey.api_key}
</span>
)}
</div>
{/* Toggle button */}
{isSecret && (
<Tooltip>
<TooltipTrigger asChild>
<Button
type="outline"
aria-label={show ? 'Hide API key' : 'Reveal API key'}
className="rounded-full px-2 pointer-events-auto"
loading={show && isLoading}
icon={show ? <EyeOff strokeWidth={2} /> : <Eye strokeWidth={2} />}
onClick={onToggleShow}
disabled={isRestricted}
/>
</TooltipTrigger>
<TooltipContent side="bottom">
{isRestricted
? 'You need additional permissions to reveal secret API keys'
: isLoadingPermission
? 'Loading permissions...'
: show
? 'Hide API key'
: 'Reveal API key'}
</TooltipContent>
</Tooltip>
)}
<Tooltip>
<TooltipTrigger asChild>
<CopyButton
type="default"
aria-label="Copy API key"
asyncText={onCopy}
iconOnly
className="rounded-full px-2 pointer-events-auto"
disabled={isRestricted || isLoadingPermission}
/>
</TooltipTrigger>
<TooltipContent side="bottom">
{isRestricted
? 'You need additional permissions to copy secret API keys'
: isLoadingPermission
? 'Loading permissions...'
: 'Copy API key'}
</TooltipContent>
</Tooltip>
</>
)
}