mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## 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>
159 lines
5.7 KiB
TypeScript
159 lines
5.7 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,
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableFooter,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from 'ui'
|
|
import { Admonition } from 'ui-patterns/admonition'
|
|
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { APIKeyRow } from './APIKeyRow'
|
|
import { CreatePublishableAPIKeyDialog } from './CreatePublishableAPIKeyDialog'
|
|
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 { APIKeysData, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
|
|
export const PublishableAPIKeys = () => {
|
|
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 newApiKeys = useMemo(
|
|
() => apiKeysData.filter(({ type }) => type === 'publishable' || type === 'secret') ?? [],
|
|
[apiKeysData]
|
|
)
|
|
const hasApiKeys = newApiKeys.length > 0
|
|
|
|
const publishableApiKeys = useMemo(
|
|
() =>
|
|
apiKeysData?.filter(
|
|
(key): key is Extract<APIKeysData[number], { type: 'publishable' }> =>
|
|
key.type === 'publishable'
|
|
) ?? [],
|
|
[apiKeysData]
|
|
)
|
|
|
|
const showSelfHostedEmptyState =
|
|
!IS_PLATFORM && publishableApiKeys.length === 0 && !isLoadingApiKeys && !isLoadingPermissions
|
|
|
|
const [deleteId, setDeleteId] = useQueryState('deletePublishableKey', parseAsString)
|
|
const apiKeyToDelete = publishableApiKeys?.find((key) => key.id === deleteId)
|
|
|
|
const {
|
|
mutate: deleteAPIKey,
|
|
isPending: isDeletingAPIKey,
|
|
isSuccess: isDeleteSuccess,
|
|
} = useAPIKeyDeleteMutation({
|
|
onSuccess: () => {
|
|
toast.success('Successfully deleted publishable key')
|
|
setDeleteId(null)
|
|
},
|
|
})
|
|
|
|
const onDeleteAPIKey = (apiKey: Extract<APIKeysData[number], { type: 'publishable' }>) => {
|
|
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 publishable key')
|
|
setDeleteId(null)
|
|
}
|
|
}, [apiKeyToDelete, deleteId, isDeleteSuccess, isSuccessApiKeys, setDeleteId])
|
|
|
|
return (
|
|
<div>
|
|
<FormHeader
|
|
title="Publishable key"
|
|
description="This key is safe to use in a browser if you have enabled Row Level Security (RLS) for your tables and configured policies."
|
|
actions={IS_PLATFORM ? <CreatePublishableAPIKeyDialog /> : null}
|
|
/>
|
|
|
|
{!canReadAPIKeys && !isLoadingPermissions ? (
|
|
<NoPermission resourceText="view API keys" />
|
|
) : isLoadingApiKeys || isLoadingPermissions ? (
|
|
<GenericSkeletonLoader />
|
|
) : isErrorApiKeys ? (
|
|
<AlertError error={error} subject="Failed to load API keys" />
|
|
) : showSelfHostedEmptyState ? (
|
|
<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 publishable API keys found</p>
|
|
<p className="text-sm text-foreground-light">
|
|
This may be a configuration issue. Ensure your API keys are available to Studio.
|
|
</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>
|
|
{hasApiKeys && publishableApiKeys.length === 0 && (
|
|
<TableRow>
|
|
<TableCell colSpan={IS_PLATFORM ? 3 : 2} className="p-0">
|
|
<Admonition showIcon={false} type="default" className="border-0 rounded-none">
|
|
<p className="text-foreground-light">No publishable keys created yet</p>
|
|
</Admonition>
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
{publishableApiKeys.map((apiKey) => (
|
|
<APIKeyRow
|
|
key={apiKey.id}
|
|
apiKey={apiKey}
|
|
isDeleting={apiKeyToDelete?.id === apiKey.id && isDeletingAPIKey}
|
|
isDeleteModalOpen={apiKeyToDelete?.id === apiKey.id}
|
|
onDelete={() => onDeleteAPIKey(apiKey)}
|
|
setKeyToDelete={setDeleteId}
|
|
/>
|
|
))}
|
|
</TableBody>
|
|
|
|
<TableFooter className="border-t">
|
|
<TableRow className="border-b-0">
|
|
<TableCell colSpan={IS_PLATFORM ? 3 : 2} className="py-2">
|
|
<p className="text-xs text-foreground-lighter font-normal">
|
|
Publishable keys can be safely shared publicly
|
|
</p>
|
|
</TableCell>
|
|
</TableRow>
|
|
</TableFooter>
|
|
</Table>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|