diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx index 0d2e42ef1c5..0a9d4f65061 100644 --- a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx +++ b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/index.tsx @@ -1,8 +1,8 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' -import { useParams } from 'common' +import { IS_PLATFORM, useFlag, useParams } from 'common' import { AnimatePresence } from 'framer-motion' import { AlertCircle, RotateCw, Timer } from 'lucide-react' -import { useMemo, useState } from 'react' +import { useMemo, useRef, useState } from 'react' import { toast } from 'sonner' import { AlertDialog, @@ -37,6 +37,10 @@ import { KeyDetailsDialog } from './key-details-dialog' import { RotateKeyDialog } from './rotate-key-dialog' import { SigningKeyRow } from './signing-key-row' import { TextConfirmModal } from '@/components/ui/TextConfirmModalWrapper' +import { + getJWTSigningKeyLastUsedAt, + useApiKeysLastUsedQuery, +} from '@/data/analytics/api-keys-last-used-query' import { useLegacyAPIKeysStatusQuery } from '@/data/api-keys/legacy-api-keys-status-query' import { useJWTSigningKeyDeleteMutation } from '@/data/jwt-signing-keys/jwt-signing-key-delete-mutation' import { useJWTSigningKeyUpdateMutation } from '@/data/jwt-signing-keys/jwt-signing-key-update-mutation' @@ -59,11 +63,25 @@ export const JWTSecretKeysTable = () => { const [selectedKey, setSelectedKey] = useState() const [selectedKeyToUpdate, setSelectedKeyToUpdate] = useState() const [shownDialog, setShownDialog] = useState() + const showApiKeysLastUsed = useFlag('showApiKeysLastUsed') const { can: canReadAPIKeys, isLoading: isLoadingCanReadAPIKeys } = useAsyncCheckPermissions( PermissionAction.SECRETS_READ, '*' ) + const now = useRef(new Date()).current + const { + data: lastUsedData, + isError: isLastUsedError, + isLoading: isLoadingLastUsed, + } = useApiKeysLastUsedQuery( + { + projectRef, + isoTimestampStart: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(), + isoTimestampEnd: now.toISOString(), + }, + { enabled: canReadAPIKeys && showApiKeysLastUsed } + ) const { data: signingKeys, isPending: isLoadingSigningKeys } = useJWTSigningKeysQuery( { projectRef, @@ -129,6 +147,7 @@ export const JWTSecretKeysTable = () => { () => sortedKeys.filter((key) => key.status === 'revoked'), [sortedKeys] ) + const getLastUsedAt = (keyId: string) => getJWTSigningKeyLastUsedAt(lastUsedData ?? [], keyId) const resetDialog = () => { setSelectedKey(undefined) @@ -236,7 +255,9 @@ export const JWTSecretKeysTable = () => { Type - + + {IS_PLATFORM && showApiKeysLastUsed && 'Last used'} + Actions @@ -257,6 +278,10 @@ export const JWTSecretKeysTable = () => { setShownDialog={setShownDialog} handleStandbyKey={handleStandbyKey} handlePreviouslyUsedKey={handlePreviouslyUsedKey} + lastUsedAt={getLastUsedAt(standbyKey.id)} + isLoadingLastUsed={isLoadingLastUsed} + isLastUsedError={isLastUsedError} + isLastUsedVisible={IS_PLATFORM && showApiKeysLastUsed} /> )} {inUseKey && ( @@ -269,6 +294,10 @@ export const JWTSecretKeysTable = () => { handlePreviouslyUsedKey={handlePreviouslyUsedKey} legacyKey={legacyKey} standbyKey={standbyKey} + lastUsedAt={getLastUsedAt(inUseKey.id)} + isLoadingLastUsed={isLoadingLastUsed} + isLastUsedError={isLastUsedError} + isLastUsedVisible={IS_PLATFORM && showApiKeysLastUsed} /> )} @@ -302,6 +331,11 @@ export const JWTSecretKeysTable = () => { Type + {IS_PLATFORM && showApiKeysLastUsed && ( + + Last used + + )} Last rotated at @@ -323,6 +357,10 @@ export const JWTSecretKeysTable = () => { setShownDialog={setShownDialog} handleStandbyKey={handleStandbyKey} handlePreviouslyUsedKey={handlePreviouslyUsedKey} + lastUsedAt={getLastUsedAt(key.id)} + isLoadingLastUsed={isLoadingLastUsed} + isLastUsedError={isLastUsedError} + isLastUsedVisible={IS_PLATFORM && showApiKeysLastUsed} /> ))} @@ -367,6 +405,11 @@ export const JWTSecretKeysTable = () => { Type + {IS_PLATFORM && showApiKeysLastUsed && ( + + Last used + + )} Last rotated at @@ -387,6 +430,10 @@ export const JWTSecretKeysTable = () => { handlePreviouslyUsedKey={handlePreviouslyUsedKey} legacyKey={legacyKey} standbyKey={standbyKey} + lastUsedAt={getLastUsedAt(key.id)} + isLoadingLastUsed={isLoadingLastUsed} + isLastUsedError={isLastUsedError} + isLastUsedVisible={IS_PLATFORM && showApiKeysLastUsed} /> ))} diff --git a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx index e61e4851c95..f962d84bfa6 100644 --- a/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx +++ b/apps/studio/components/interfaces/JwtSecrets/jwt-secret-keys-table/signing-key-row.tsx @@ -6,6 +6,7 @@ import { CircleArrowUp, Eye, Key, + Minus, MoreVertical, ShieldOff, Timer, @@ -22,6 +23,7 @@ import { TableCell, TableRow, } from 'ui' +import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader' import { TimestampInfo } from 'ui-patterns/TimestampInfo' import { AlgorithmHoverCard } from '../algorithm-hover-card' @@ -38,10 +40,64 @@ interface SigningKeyRowProps { legacyKey?: JWTSigningKey | null standbyKey?: JWTSigningKey | null isLoading?: boolean + lastUsedAt?: number + isLoadingLastUsed?: boolean + isLastUsedError?: boolean + isLastUsedVisible?: boolean } const MotionTableRow = motion.create(TableRow) +const hasRotationTimestamp = (status: JWTSigningKey['status']) => + status === 'previously_used' || status === 'revoked' + +const LastUsedCell = ({ + lastUsedAt, + isLoading, + isError, + isLastUsedSupported, +}: { + lastUsedAt?: number + isLoading: boolean + isError: boolean + isLastUsedSupported: boolean +}) => { + const className = + 'text-right py-2 text-sm text-foreground-light whitespace-nowrap data-[invisible=true]:invisible' + + if (!isLastUsedSupported) + return ( + + + + ) + + if (isLoading) { + return ( + + + + ) + } + + if (isError) return Unable to load + if (lastUsedAt === undefined) { + return ( + No requests in 24h + ) + } + + return ( + + + + ) +} + export const SigningKeyRow = ({ signingKey, setSelectedKey, @@ -51,6 +107,10 @@ export const SigningKeyRow = ({ legacyKey, standbyKey, isLoading = false, + lastUsedAt, + isLoadingLastUsed = false, + isLastUsedError = false, + isLastUsedVisible = false, }: SigningKeyRowProps) => ( - {signingKey.status === 'previously_used' || signingKey.status === 'revoked' ? ( + {isLastUsedVisible && ( + + )} + {!isLastUsedVisible && !hasRotationTimestamp(signingKey.status) && } + {hasRotationTimestamp(signingKey.status) && ( - ) : ( - )} {(signingKey.status !== 'in_use' || signingKey.algorithm !== 'HS256') && (