Files
supabase/apps/studio/components/ui/ProjectSettings/DisplayApiSettings.tsx
T
Jordi EnricandJoshen Lim 3063679f1b feat(auth): restore key last-used timestamps FE-2462 FE-4315 (#50732)
## 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>
2026-09-23 13:46:00 +02:00

244 lines
9.2 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
import { IS_PLATFORM, useFlag, useParams } from 'common'
import { AlertCircle, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { useMemo, useRef } from 'react'
import { toast } from 'sonner'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
import { getLastUsedAPIKeys } from './DisplayApiSettings.utils'
import Panel from '@/components/ui/Panel'
import { useApiKeysLastUsedQuery } from '@/data/analytics/api-keys-last-used-query'
import { useJwtSecretUpdatingStatusQuery } from '@/data/config/jwt-secret-updating-status-query'
import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
export const DisplayApiSettings = ({
showTitle = true,
showNotice = true,
showLegacyText = true,
}: {
showTitle?: boolean
showNotice?: boolean
showLegacyText?: boolean
}) => {
const { ref: projectRef } = useParams()
const {
data: settings,
isError: isProjectSettingsError,
isPending: isProjectSettingsLoading,
} = useProjectSettingsV2Query({ projectRef })
const {
data,
isError: isJwtSecretUpdateStatusError,
isPending: isJwtSecretUpdateStatusLoading,
} = useJwtSecretUpdatingStatusQuery({ projectRef })
const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus
const { isLoading: isLoadingPermissions, can: canReadAPIKeys } = useAsyncCheckPermissions(
PermissionAction.READ,
'service_api_keys'
)
const isLoading = isProjectSettingsLoading || isLoadingPermissions
const isNotUpdatingJwtSecret =
jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
const apiKeys = useMemo(() => settings?.service_api_keys ?? [], [settings])
// api keys should not be empty. However it can be populated with a delay on project creation
const isApiKeysEmpty = apiKeys.length === 0
const now = useRef(new Date()).current
const showApiKeysLastUsed = useFlag('showApiKeysLastUsed')
const {
isLoading: isLoadingLastUsed,
isError: isLastUsedError,
data: lastUsedLogData,
} = useApiKeysLastUsedQuery(
{
projectRef,
isoTimestampStart: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(),
isoTimestampEnd: now.toISOString(),
},
{ enabled: canReadAPIKeys && showApiKeysLastUsed }
)
const lastUsedAPIKeys = useMemo(() => {
if (apiKeys.length < 1 || !lastUsedLogData || lastUsedLogData.length < 1) {
return {}
}
try {
return getLastUsedAPIKeys(apiKeys, lastUsedLogData)
} catch (e: any) {
toast.error('Failed to identify when the anon and service_role keys were last used')
console.error(e)
return {}
}
}, [lastUsedLogData, apiKeys])
return (
<Panel
noMargin
title={
showTitle && (
<div className="space-y-3">
<h5 className="text-base">Project API Keys</h5>
<p className="text-sm text-foreground-light">
Your API is secured behind an API gateway which requires an API Key for every request.
<br />
You can use the keys below in the Supabase client libraries.
<br />
</p>
</div>
)
}
>
{isLoading ? (
<div className="flex items-center justify-center py-8 space-x-2">
<Loader2 className="animate-spin" size={16} strokeWidth={1.5} />
<p className="text-sm text-foreground-light">Retrieving API keys</p>
</div>
) : !canReadAPIKeys ? (
<div className="flex items-center py-8 px-8 space-x-2">
<AlertCircle size={16} strokeWidth={1.5} />
<p className="text-sm text-foreground-light">
You don't have permission to view API keys. These keys restricted to users with higher
access levels.
</p>
</div>
) : isProjectSettingsError || isJwtSecretUpdateStatusError ? (
<div className="flex items-center justify-center py-8 space-x-2">
<AlertCircle size={16} strokeWidth={1.5} />
<p className="text-sm text-foreground-light">
{isProjectSettingsError ? 'Failed to retrieve API keys' : 'Failed to update JWT secret'}
</p>
</div>
) : isApiKeysEmpty || isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? (
<div className="flex items-center justify-center py-8 space-x-2">
<Loader2 className="animate-spin" size={16} strokeWidth={1.5} />
<p className="text-sm text-foreground-light">
{isProjectSettingsLoading || isApiKeysEmpty
? 'Retrieving API keys'
: 'JWT secret is being updated'}
</p>
</div>
) : (
apiKeys.map((x, i: number) => (
<Panel.Content
key={x.api_key}
className={
i >= 1 &&
'border-t border-panel-border-interior-light in-data-[theme*=dark]:border-panel-border-interior-dark'
}
>
<FormLayout
layout="horizontal"
label={
<div className="flex items-center space-x-1">
{x.tags?.split(',').map((x, i: number) => (
<code key={`${x}${i}`} className="text-code-inline">
{x}
</code>
))}
{x.tags === 'service_role' && (
<>
<code className="text-code-inline bg-destructive! text-destructive-foreground! border-destructive!">
secret
</code>
</>
)}
{x.tags === 'anon' && <code className="text-code-inline">public</code>}
</div>
}
description={
x.tags === 'service_role' ? (
<>
This key has the ability to bypass Row Level Security. Never share it publicly.
If leaked, generate a new JWT secret immediately.{' '}
{showLegacyText && (
<span>
Prefer using{' '}
<Link
href={`/project/${projectRef}/settings/api-keys/new`}
className="text-link underline"
>
Secret API keys
</Link>{' '}
instead.
</span>
)}
</>
) : (
<>
This key is safe to use in a browser if you have enabled Row Level Security for
your tables and configured policies.{' '}
{showLegacyText && (
<span>
Prefer using{' '}
<Link
href={`/project/${projectRef}/settings/api-keys/new`}
className="text-link underline"
>
Publishable API keys
</Link>{' '}
instead.
</span>
)}
</>
)
}
>
<Input
readOnly
className="font-mono"
copy={canReadAPIKeys && isNotUpdatingJwtSecret}
reveal={x.tags !== 'anon' && canReadAPIKeys && isNotUpdatingJwtSecret}
value={
!canReadAPIKeys
? 'You need additional permissions to view API keys'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
? 'JWT secret update failed, new API key may have issues'
: jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
? 'Updating JWT secret...'
: (x?.api_key ?? 'You need additional permissions to view API keys')
}
onChange={() => {}}
/>
</FormLayout>
{IS_PLATFORM && showApiKeysLastUsed && (
<div
className="pt-2 text-foreground-lighter w-full text-sm data-[invisible=true]:invisible"
data-invisible={isLoadingLastUsed}
>
{isLastUsedError
? 'Unable to load requests from the past 24 hours.'
: lastUsedAPIKeys[x.api_key]
? `Last request was ${lastUsedAPIKeys[x.api_key]} ago.`
: 'No requests in the past 24 hours.'}
</div>
)}
</Panel.Content>
))
)}
{showNotice ? (
<Panel.Notice
className="border-t"
title="API keys have moved"
badgeLabel="Changelog"
description={`
\`anon\` and \`service_role\` API keys can now be replaced with \`publishable\` and \`secret\` API keys.
`}
href="https://github.com/orgs/supabase/discussions/29260"
buttonText="Read the announcement"
/>
) : null}
</Panel>
)
}