mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
## Problem Studio expected aliased fields from the last-used API-key endpoint, but the live endpoint returns OTEL attribute names. This kept legacy API-key activity unavailable and prevented Studio from showing activity for new JWT signing keys. Tracks FE-2462 and FE-4315. ## Fix Normalize the endpoint response at the data boundary, keep the `showApiKeysLastUsed` feature flag, and show activity from the past 24 hours for new JWT signing keys. Legacy HS256 signing keys remain blank because the analytics response does not provide a stable signing-key record ID for them. The request remains hosted-only, permission-gated, and non-blocking, and the existing last-rotated column remains intact. ## How to test - Make a request with a legacy anon or service-role API key, then open Project Settings > API Keys and verify its last request appears. - Make an Auth request signed by a new JWT signing key, then open JWT Keys and verify the matching key shows a Last used timestamp. - Verify a new key without activity shows No requests in the past 24 hours. - Verify the legacy HS256 signing-key row leaves Last used blank. - Expected result: legacy API keys and new JWT signing keys display activity from the shared endpoint without changing self-hosted Studio. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a **Last used** column for JWT signing keys on supported platforms. * Displays usage timestamps, loading and error states, or when a key has had no requests in the past 24 hours. * Usage tracking now includes both API keys and JWT signing keys. * **Bug Fixes** * Improved handling of usage records for legacy and current keys. * Usage details appear only on supported platforms and for users with the required permissions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
107 lines
3.5 KiB
TypeScript
107 lines
3.5 KiB
TypeScript
import { useQuery } from '@tanstack/react-query'
|
|
import { z } from 'zod'
|
|
|
|
import { analyticsKeys } from './keys'
|
|
import { get, handleError } from '@/data/fetchers'
|
|
import { IS_PLATFORM } from '@/lib/constants'
|
|
import type { UseCustomQueryOptions } from '@/types'
|
|
|
|
export type ApiKeysLastUsedVariables = {
|
|
projectRef?: string
|
|
isoTimestampStart?: string
|
|
isoTimestampEnd?: string
|
|
}
|
|
|
|
const apiKeyLastUsedSchema = z.object({
|
|
timestamp: z.number(),
|
|
role: z.string().optional(),
|
|
signaturePrefix: z.string().optional(),
|
|
keyId: z.string().optional(),
|
|
})
|
|
|
|
export type ApiKeyLastUsed = z.infer<typeof apiKeyLastUsedSchema>
|
|
|
|
const apiKeyLastUsedEndpointRowSchema = z
|
|
.object({
|
|
timestamp: z.number().finite(),
|
|
role: z.string().nullish(),
|
|
signature_prefix: z.string().nullish(),
|
|
key_id: z.string().nullish(),
|
|
request_sb_jwt_authorization_payload_role: z.string().nullish(),
|
|
request_sb_jwt_authorization_payload_signature_prefix: z.string().nullish(),
|
|
request_sb_jwt_authorization_payload_key_id: z.string().nullish(),
|
|
})
|
|
.transform((row): ApiKeyLastUsed => {
|
|
const role = row.role || row.request_sb_jwt_authorization_payload_role
|
|
const signaturePrefix =
|
|
row.signature_prefix || row.request_sb_jwt_authorization_payload_signature_prefix
|
|
const keyId = row.key_id || row.request_sb_jwt_authorization_payload_key_id
|
|
|
|
const apiKeyLastUsed: ApiKeyLastUsed = { timestamp: row.timestamp }
|
|
if (role) apiKeyLastUsed.role = role
|
|
if (signaturePrefix) apiKeyLastUsed.signaturePrefix = signaturePrefix
|
|
if (keyId) apiKeyLastUsed.keyId = keyId
|
|
return apiKeyLastUsed
|
|
})
|
|
.pipe(apiKeyLastUsedSchema)
|
|
|
|
export const apiKeysLastUsedSchema = z.array(apiKeyLastUsedEndpointRowSchema)
|
|
|
|
export const getJWTSigningKeyLastUsedAt = (rows: ApiKeyLastUsed[], keyId: string) =>
|
|
rows.reduce<number | undefined>((latestTimestamp, row) => {
|
|
if (row.keyId !== keyId) return latestTimestamp
|
|
if (latestTimestamp === undefined) return row.timestamp
|
|
return Math.max(latestTimestamp, row.timestamp)
|
|
}, undefined)
|
|
|
|
export async function getApiKeysLastUsed(
|
|
{ projectRef, isoTimestampStart, isoTimestampEnd }: ApiKeysLastUsedVariables,
|
|
signal?: AbortSignal
|
|
) {
|
|
if (!projectRef) {
|
|
throw new Error('projectRef is required')
|
|
}
|
|
|
|
const { data, error } = await get(
|
|
'/platform/projects/{ref}/analytics/endpoints/api_keys.last_used.otel',
|
|
{
|
|
params: {
|
|
path: { ref: projectRef },
|
|
query: {
|
|
iso_timestamp_start: isoTimestampStart,
|
|
iso_timestamp_end: isoTimestampEnd,
|
|
},
|
|
},
|
|
signal,
|
|
}
|
|
)
|
|
|
|
if (error) handleError(error)
|
|
|
|
if (data?.error) {
|
|
throw new Error(
|
|
typeof data.error === 'string' ? data.error : 'Failed to fetch last-used API keys'
|
|
)
|
|
}
|
|
|
|
return apiKeysLastUsedSchema.parse(data?.result ?? [])
|
|
}
|
|
|
|
export type ApiKeysLastUsedData = Awaited<ReturnType<typeof getApiKeysLastUsed>>
|
|
export type ApiKeysLastUsedError = Error
|
|
|
|
export const useApiKeysLastUsedQuery = <TData = ApiKeysLastUsedData>(
|
|
{ projectRef, isoTimestampStart, isoTimestampEnd }: ApiKeysLastUsedVariables,
|
|
{
|
|
enabled = true,
|
|
...options
|
|
}: UseCustomQueryOptions<ApiKeysLastUsedData, ApiKeysLastUsedError, TData> = {}
|
|
) =>
|
|
useQuery<ApiKeysLastUsedData, ApiKeysLastUsedError, TData>({
|
|
queryKey: analyticsKeys.apiKeysLastUsed(projectRef, { isoTimestampStart, isoTimestampEnd }),
|
|
queryFn: ({ signal }) =>
|
|
getApiKeysLastUsed({ projectRef, isoTimestampStart, isoTimestampEnd }, signal),
|
|
enabled: IS_PLATFORM && enabled && typeof projectRef !== 'undefined',
|
|
...options,
|
|
})
|