mirror of
https://github.com/supabase/supabase.git
synced 2026-05-06 22:18:00 +08:00
This PR migrates the whole monorepo to use Tailwind v4: - Removed `@tailwindcss/container-queries` plugin since it's included by default in v4, - Bump all instances of Tailwind to v4. Made minimal changes to the shared config to remove non-supported features (`alpha` mentions), - Migrate all apps to be compatible with v4 configs, - Fix the `typography.css` import in 3 apps, - Add missing rules which were included by default in v3, - Run `pnpm dlx @tailwindcss/upgrade` on all apps, which renames a lot of classes - Rename all misnamed classes according to https://tailwindcss.com/docs/upgrade-guide#renamed-utilities in all apps. --------- Co-authored-by: Jordi Enric <jordi.err@gmail.com>
185 lines
6.5 KiB
TypeScript
185 lines
6.5 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useFlag, useParams } from 'common'
|
|
import dayjs from 'dayjs'
|
|
import { parseAsString, useQueryState } from 'nuqs'
|
|
import { useEffect, useMemo, useRef } from 'react'
|
|
import { toast } from 'sonner'
|
|
import { Card } from 'ui'
|
|
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from 'ui/src/components/shadcn/ui/table'
|
|
|
|
import { APIKeyRow } from './APIKeyRow'
|
|
import { CreateSecretAPIKeyDialog } from './CreateSecretAPIKeyDialog'
|
|
import { AlertError } from '@/components/ui/AlertError'
|
|
import { FormHeader } from '@/components/ui/Forms/FormHeader'
|
|
import { NoPermission } from '@/components/ui/NoPermission'
|
|
import { useAPIKeyDeleteMutation } from '@/data/api-keys/api-key-delete-mutation'
|
|
import type { APIKeysData } from '@/data/api-keys/api-keys-query'
|
|
import { useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
|
|
import { useLogsQuery } from '@/hooks/analytics/useLogsQuery'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
|
|
interface LastSeenData {
|
|
[hash: string]: { timestamp: number; relative: string }
|
|
}
|
|
|
|
function useLastSeen({ projectRef, enabled }: { projectRef: string; enabled?: boolean }): {
|
|
data?: LastSeenData
|
|
isLoading: boolean
|
|
} {
|
|
const now = useRef(new Date()).current
|
|
|
|
const query = useLogsQuery(
|
|
projectRef,
|
|
{
|
|
iso_timestamp_start: new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(),
|
|
iso_timestamp_end: now.toISOString(),
|
|
sql: "-- last-used-secret-api-keys\nSELECT unix_millis(max(timestamp)) as timestamp, apikey.`hash` FROM edge_logs cross join unnest(metadata) as m cross join unnest(m.request) as request cross join unnest(request.sb) as sb cross join unnest(sb.apikey) as sbapikey cross join unnest(sbapikey.apikey) as apikey WHERE apikey.error is null and apikey.`hash` is not null and apikey.prefix like 'sb_secret_%' GROUP BY apikey.`hash`",
|
|
},
|
|
enabled
|
|
)
|
|
|
|
return useMemo(() => {
|
|
if (query.isLoading || !query.logData) {
|
|
return { data: undefined, isLoading: query.isLoading }
|
|
}
|
|
|
|
const now = dayjs()
|
|
|
|
const lastSeen = (query.logData as unknown as { timestamp: number; hash: string }[]).reduce(
|
|
(a, i) => {
|
|
a[i.hash] = {
|
|
timestamp: i.timestamp,
|
|
relative: `${dayjs.duration(now.diff(dayjs(i.timestamp))).humanize(false)} ago`,
|
|
}
|
|
return a
|
|
},
|
|
{} as LastSeenData
|
|
)
|
|
|
|
return { data: lastSeen, isLoading: query.isLoading }
|
|
}, [query])
|
|
}
|
|
|
|
export const SecretAPIKeys = () => {
|
|
const { ref: projectRef } = useParams()
|
|
const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
|
|
PermissionAction.SECRETS_READ,
|
|
'*'
|
|
)
|
|
|
|
const {
|
|
data: apiKeysData,
|
|
error,
|
|
isSuccess: isSuccessApiKeys,
|
|
isPending: isLoadingApiKeys,
|
|
isError: isErrorApiKeys,
|
|
} = useAPIKeysQuery({ projectRef, reveal: false }, { enabled: canReadAPIKeys })
|
|
|
|
const showApiKeysLastUsed = useFlag('showApiKeysLastUsed')
|
|
const { data: lastSeen, isLoading: isLoadingLastSeen } = useLastSeen({
|
|
projectRef: projectRef ?? '',
|
|
enabled: showApiKeysLastUsed,
|
|
})
|
|
|
|
const secretApiKeys = useMemo(
|
|
() =>
|
|
apiKeysData?.filter(
|
|
(key): key is Extract<APIKeysData[number], { type: 'secret' }> => key.type === 'secret'
|
|
) ?? [],
|
|
[apiKeysData]
|
|
)
|
|
|
|
const empty = secretApiKeys?.length === 0 && !isLoadingApiKeys && !isLoadingPermissions
|
|
|
|
const [deleteId, setDeleteId] = useQueryState('deleteSecretKey', parseAsString)
|
|
const apiKeyToDelete = secretApiKeys?.find((key) => key.id === deleteId)
|
|
|
|
const {
|
|
mutate: deleteAPIKey,
|
|
isPending: isDeletingAPIKey,
|
|
isSuccess: isDeleteSuccess,
|
|
} = useAPIKeyDeleteMutation({
|
|
onSuccess: () => {
|
|
toast.success('Successfully deleted secret key')
|
|
setDeleteId(null)
|
|
},
|
|
})
|
|
|
|
const onDeleteAPIKey = (apiKey: Extract<APIKeysData[number], { type: 'secret' }>) => {
|
|
if (!projectRef) return console.error('Project ref is required')
|
|
if (!apiKey.id) return console.error('API key ID is required')
|
|
deleteAPIKey({ projectRef, id: apiKey.id })
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (isSuccessApiKeys && !!deleteId && !apiKeyToDelete && !isDeleteSuccess) {
|
|
toast('Unable to find secret key')
|
|
setDeleteId(null)
|
|
}
|
|
}, [apiKeyToDelete, deleteId, isDeleteSuccess, isSuccessApiKeys, setDeleteId])
|
|
|
|
return (
|
|
<div className="pb-30">
|
|
<FormHeader
|
|
title="Secret keys"
|
|
description="These API keys allow privileged access to your project's APIs. Use in servers, functions, workers or other backend components of your application."
|
|
actions={<CreateSecretAPIKeyDialog />}
|
|
/>
|
|
|
|
{!canReadAPIKeys && !isLoadingPermissions ? (
|
|
<NoPermission resourceText="view API keys" />
|
|
) : isLoadingApiKeys || isLoadingPermissions ? (
|
|
<GenericSkeletonLoader />
|
|
) : isErrorApiKeys ? (
|
|
<AlertError error={error} subject="Failed to load secret API keys" />
|
|
) : empty ? (
|
|
<Card>
|
|
<div className="rounded-b-md! overflow-hidden py-12 flex flex-col gap-1 items-center justify-center">
|
|
<p className="text-sm text-foreground">No secret API keys found</p>
|
|
<p className="text-sm text-foreground-light">
|
|
Your project is not accessible via secret keys—there are no active secret keys
|
|
created.
|
|
</p>
|
|
</div>
|
|
</Card>
|
|
) : (
|
|
<Card className="bg-surface-100">
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow className="bg-200">
|
|
<TableHead>Name</TableHead>
|
|
<TableHead>API Key</TableHead>
|
|
{showApiKeysLastUsed && (
|
|
<TableHead className="hidden lg:table-cell">Last Used</TableHead>
|
|
)}
|
|
<TableHead />
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{secretApiKeys.map((apiKey) => (
|
|
<APIKeyRow
|
|
key={apiKey.id}
|
|
apiKey={apiKey}
|
|
lastSeen={lastSeen?.[apiKey.hash]}
|
|
isLoadingLastSeen={isLoadingLastSeen}
|
|
isDeleting={apiKeyToDelete?.id === apiKey.id && isDeletingAPIKey}
|
|
onDelete={() => onDeleteAPIKey(apiKey)}
|
|
setKeyToDelete={setDeleteId}
|
|
isDeleteModalOpen={apiKeyToDelete?.id === apiKey.id}
|
|
/>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</Card>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|