Files
supabase/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.utils.test.ts
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

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', signature_prefix: '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',
signature_prefix: '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',
signature_prefix: '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, signature_prefix: 'anonsig' },
]
)
expect(result).toEqual({})
})
})