mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 01:15:03 +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>
244 lines
9.2 KiB
TypeScript
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>
|
|
)
|
|
}
|