Files
supabase/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx
Jordi Enric 4096267623 feat(api-keys): migrate last-used indicator to ClickHouse endpoint (#47458)
## Problem

The "last used" indicator for the legacy `anon` / `service_role` API
keys (Project API keys settings) was disabled because it ran a BigQuery
`edge_logs` query. It is now re-enabled against the ClickHouse-backed
`api_keys.last_used.otel` analytics endpoint.

## Current behavior

- The `anon` / `service_role` "last used" indicator is off (the
BigQuery-backed query was disabled).

## New behavior

- New `useApiKeysLastUsedQuery` hook calls the `api_keys.last_used.otel`
endpoint (timestamp params only, no SQL sent), plus its query key and
the generated platform API type.
- `DisplayApiSettings` reads last-used from this hook instead of posting
BigQuery `edge_logs` SQL. The pure `getLastUsedAPIKeys` shaper is kept
and unit-tested. Still gated by the `showApiKeysLastUsed` flag.
- Removed the disabled secret-keys (`sb_secret_`) BigQuery last-used
path, which has no ClickHouse endpoint to migrate to: drops the dead
`useLastSeen` query, the `APIKeyRow` "Last Used" column, and the unused
`showLastSeen` prop.
- Reworded the delete-confirmation copy to be accurate for both secret
and publishable keys.

## Additional context

- Backed by the platform endpoint in supabase/platform#34892 (merged and
deployed).
- Scope: `anon` / `service_role` legacy keys. Secret/publishable and JWT
signing-key "last used" are follow-ups, pending the endpoint returning
those key types.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Improvements**
* Updated API key settings to show “last used” activity for the past 24
hours using a dedicated data source and time window.
  * Added clearer messaging when recent API key activity fails to load.
  * Removed the “Last Used” column from API key management tables.
* **Bug Fixes**
* Improved mapping so “last used” values correctly match the intended
key and role.
* Updated API key deletion confirmation to explain required backend
changes and resulting unauthorized behavior.
* **Tests**
* Added unit tests to validate “last used” computation and edge-case
filtering.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 14:35:32 +02:00

130 lines
4.6 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { IS_PLATFORM, useParams } from 'common'
import { parseAsString, useQueryState } from 'nuqs'
import { useEffect, useMemo } 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 { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
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 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={IS_PLATFORM ? <CreateSecretAPIKeyDialog /> : null}
/>
{!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>
{IS_PLATFORM && <TableHead />}
</TableRow>
</TableHeader>
<TableBody>
{secretApiKeys.map((apiKey) => (
<APIKeyRow
key={apiKey.id}
apiKey={apiKey}
isDeleting={apiKeyToDelete?.id === apiKey.id && isDeletingAPIKey}
onDelete={() => onDeleteAPIKey(apiKey)}
setKeyToDelete={setDeleteId}
isDeleteModalOpen={apiKeyToDelete?.id === apiKey.id}
/>
))}
</TableBody>
</Table>
</Card>
)}
</div>
)
}