Files
supabase/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx
Ivan Vasilov 56de26fe22 chore: Migrate the monorepo to use Tailwind v4 (#45318)
This PR migrates the whole monorepo to use Tailwind v4:
- Removed `@tailwindcss/container-queries` plugin since it's included by
default in v4,
- Bump all instances of Tailwind to v4. Made minimal changes to the
shared config to remove non-supported features (`alpha` mentions),
- Migrate all apps to be compatible with v4 configs,
- Fix the `typography.css` import in 3 apps,
- Add missing rules which were included by default in v3,
- Run `pnpm dlx @tailwindcss/upgrade` on all apps, which renames a lot
of classes
- Rename all misnamed classes according to
https://tailwindcss.com/docs/upgrade-guide#renamed-utilities in all
apps.

---------

Co-authored-by: Jordi Enric <jordi.err@gmail.com>
2026-04-30 10:53:24 +00:00

185 lines
6.5 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useFlag, useParams } from 'common'
import dayjs from 'dayjs'
import { parseAsString, useQueryState } from 'nuqs'
import { useEffect, useMemo, useRef } from 'react'
import { toast } from 'sonner'
import { Card } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import {
Table,
TableBody,
TableHead,
TableHeader,
TableRow,
} from 'ui/src/components/shadcn/ui/table'
import { APIKeyRow } from './APIKeyRow'
import { CreateSecretAPIKeyDialog } from './CreateSecretAPIKeyDialog'
import { AlertError } from '@/components/ui/AlertError'
import { FormHeader } from '@/components/ui/Forms/FormHeader'
import { NoPermission } from '@/components/ui/NoPermission'
import { useAPIKeyDeleteMutation } from '@/data/api-keys/api-key-delete-mutation'
import type { APIKeysData } from '@/data/api-keys/api-keys-query'
import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
import { useLogsQuery } from '@/hooks/analytics/useLogsQuery'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
interface LastSeenData {
[hash: string]: { timestamp: number; relative: string }
}
function useLastSeen({ projectRef, enabled }: { projectRef: string; enabled?: boolean }): {
data?: LastSeenData
isLoading: boolean
} {
const now = useRef(new Date()).current
const query = useLogsQuery(
projectRef,
{
iso_timestamp_start: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(),
iso_timestamp_end: now.toISOString(),
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`",
},
enabled
)
return useMemo(() => {
if (query.isLoading || !query.logData) {
return { data: undefined, isLoading: query.isLoading }
}
const now = dayjs()
const lastSeen = (query.logData as unknown as { timestamp: number; hash: string }[]).reduce(
(a, i) => {
a[i.hash] = {
timestamp: i.timestamp,
relative: `${dayjs.duration(now.diff(dayjs(i.timestamp))).humanize(false)} ago`,
}
return a
},
{} as LastSeenData
)
return { data: lastSeen, isLoading: query.isLoading }
}, [query])
}
export const SecretAPIKeys = () => {
const { ref: projectRef } = useParams()
const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
PermissionAction.SECRETS_READ,
'*'
)
const {
data: apiKeysData,
error,
isSuccess: isSuccessApiKeys,
isPending: isLoadingApiKeys,
isError: isErrorApiKeys,
} = useAPIKeysQuery({ projectRef, reveal: false }, { enabled: canReadAPIKeys })
const showApiKeysLastUsed = useFlag('showApiKeysLastUsed')
const { data: lastSeen, isLoading: isLoadingLastSeen } = useLastSeen({
projectRef: projectRef ?? '',
enabled: showApiKeysLastUsed,
})
const secretApiKeys = useMemo(
() =>
apiKeysData?.filter(
(key): key is Extract<APIKeysData[number], { type: 'secret' }> => key.type === 'secret'
) ?? [],
[apiKeysData]
)
const empty = secretApiKeys?.length === 0 && !isLoadingApiKeys && !isLoadingPermissions
const [deleteId, setDeleteId] = useQueryState('deleteSecretKey', parseAsString)
const apiKeyToDelete = secretApiKeys?.find((key) => key.id === deleteId)
const {
mutate: deleteAPIKey,
isPending: isDeletingAPIKey,
isSuccess: isDeleteSuccess,
} = useAPIKeyDeleteMutation({
onSuccess: () => {
toast.success('Successfully deleted secret key')
setDeleteId(null)
},
})
const onDeleteAPIKey = (apiKey: Extract<APIKeysData[number], { type: 'secret' }>) => {
if (!projectRef) return console.error('Project ref is required')
if (!apiKey.id) return console.error('API key ID is required')
deleteAPIKey({ projectRef, id: apiKey.id })
}
useEffect(() => {
if (isSuccessApiKeys && !!deleteId && !apiKeyToDelete && !isDeleteSuccess) {
toast('Unable to find secret key')
setDeleteId(null)
}
}, [apiKeyToDelete, deleteId, isDeleteSuccess, isSuccessApiKeys, setDeleteId])
return (
<div className="pb-30">
<FormHeader
title="Secret keys"
description="These API keys allow privileged access to your project's APIs. Use in servers, functions, workers or other backend components of your application."
actions={<CreateSecretAPIKeyDialog />}
/>
{!canReadAPIKeys && !isLoadingPermissions ? (
<NoPermission resourceText="view API keys" />
) : isLoadingApiKeys || isLoadingPermissions ? (
<GenericSkeletonLoader />
) : isErrorApiKeys ? (
<AlertError error={error} subject="Failed to load secret API keys" />
) : empty ? (
<Card>
<div className="rounded-b-md! overflow-hidden py-12 flex flex-col gap-1 items-center justify-center">
<p className="text-sm text-foreground">No secret API keys found</p>
<p className="text-sm text-foreground-light">
Your project is not accessible via secret keysthere are no active secret keys
created.
</p>
</div>
</Card>
) : (
<Card className="bg-surface-100">
<Table>
<TableHeader>
<TableRow className="bg-200">
<TableHead>Name</TableHead>
<TableHead>API Key</TableHead>
{showApiKeysLastUsed && (
<TableHead className="hidden lg:table-cell">Last Used</TableHead>
)}
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{secretApiKeys.map((apiKey) => (
<APIKeyRow
key={apiKey.id}
apiKey={apiKey}
lastSeen={lastSeen?.[apiKey.hash]}
isLoadingLastSeen={isLoadingLastSeen}
isDeleting={apiKeyToDelete?.id === apiKey.id && isDeletingAPIKey}
onDelete={() => onDeleteAPIKey(apiKey)}
setKeyToDelete={setDeleteId}
isDeleteModalOpen={apiKeyToDelete?.id === apiKey.id}
/>
))}
</TableBody>
</Table>
</Card>
)}
</div>
)
}