mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +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>
74 lines
2.2 KiB
TypeScript
74 lines
2.2 KiB
TypeScript
import dayjs from 'dayjs'
|
|
import duration from 'dayjs/plugin/duration'
|
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
import { getLastUsedAPIKeys } from './DisplayApiSettings.utils'
|
|
|
|
// getLastUsedAPIKeys humanizes a duration, which needs both plugins.
|
|
dayjs.extend(duration)
|
|
dayjs.extend(relativeTime)
|
|
|
|
// JWT-shaped keys: `header.payload.signature`. Matching is done on the signature
|
|
// segment ([2]) via startsWith(signature_prefix).
|
|
const anonKey = { tags: 'anon', api_key: 'header.payload.anonsignature123' }
|
|
const serviceRoleKey = { tags: 'service_role', api_key: 'header.payload.servicesignature456' }
|
|
|
|
describe('getLastUsedAPIKeys', () => {
|
|
it('returns an empty object when there are no api keys', () => {
|
|
expect(
|
|
getLastUsedAPIKeys([], [{ timestamp: 1, role: 'anon', signaturePrefix: 'anon' }])
|
|
).toEqual({})
|
|
})
|
|
|
|
it('returns an empty object when log data is null or empty', () => {
|
|
expect(getLastUsedAPIKeys([anonKey], null)).toEqual({})
|
|
expect(getLastUsedAPIKeys([anonKey], undefined)).toEqual({})
|
|
expect(getLastUsedAPIKeys([anonKey], [])).toEqual({})
|
|
})
|
|
|
|
it('maps a matching key to a humanized last-used duration', () => {
|
|
const result = getLastUsedAPIKeys(
|
|
[anonKey],
|
|
[
|
|
{
|
|
timestamp: dayjs().subtract(2, 'hour').valueOf(),
|
|
role: 'anon',
|
|
signaturePrefix: 'anonsig',
|
|
},
|
|
]
|
|
)
|
|
|
|
expect(Object.keys(result)).toEqual([anonKey.api_key])
|
|
expect(result[anonKey.api_key]).toContain('hours')
|
|
})
|
|
|
|
it('only matches the key whose role and signature prefix line up', () => {
|
|
const result = getLastUsedAPIKeys(
|
|
[anonKey, serviceRoleKey],
|
|
[
|
|
{
|
|
timestamp: dayjs().subtract(1, 'day').valueOf(),
|
|
role: 'service_role',
|
|
signaturePrefix: 'servicesig',
|
|
},
|
|
]
|
|
)
|
|
|
|
expect(Object.keys(result)).toEqual([serviceRoleKey.api_key])
|
|
expect(result[anonKey.api_key]).toBeUndefined()
|
|
})
|
|
|
|
it('ignores log rows missing a role or signature prefix', () => {
|
|
const result = getLastUsedAPIKeys(
|
|
[anonKey],
|
|
[
|
|
{ timestamp: 1, role: 'anon' },
|
|
{ timestamp: 2, signaturePrefix: 'anonsig' },
|
|
]
|
|
)
|
|
|
|
expect(result).toEqual({})
|
|
})
|
|
})
|