Feat/api keys rollout prep (#35559)

* first pass

* init

* updated types

* fix up key reveal

* Update QuickKeyCopy.tsx

* remove quick key copy

* api key pill now only allows reveal and copy if you have perm

* Update LegacyAPIKeys.tsx

* fix up layouts

* fix copy

* Fix action menu dropdown position, few small nudges

* Remove unused files.

* Remove the hardcoded and rename the feature flag for basic API keys.

* add support for name and description, some smaller improvements

* Fix the trims for the description.

---------

Co-authored-by: Terry Sutton <saltcod@gmail.com>
Co-authored-by: Ivan Vasilov <vasilov.ivan@gmail.com>
Co-authored-by: Stojan Dimitrovski <sdimitrovski@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-05-27 15:50:42 +02:00
1 parent 4f2b09b61f
commit 9318404e61
27 files changed
+996 -517

No files matched your search

@@ -1,28 +1,16 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useQueryClient } from '@tanstack/react-query'
import { InputVariants } from '@ui/components/shadcn/ui/input'
import { Eye, MoreVertical } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { useParams } from 'common'
import CopyButton from 'components/ui/CopyButton'
import { useAPIKeyIdQuery } from 'data/api-keys/[id]/api-key-id-query'
import { APIKeysData } from 'data/api-keys/api-keys-query'
import { apiKeysKeys } from 'data/api-keys/keys'
import { AnimatePresence, motion } from 'framer-motion'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { MoreVertical } from 'lucide-react'
import { motion } from 'framer-motion'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuSeparator,
DropdownMenuTrigger,
TableCell,
TableRow,
cn,
} from 'ui'
import { APIKeyDeleteDialog } from './APIKeyDeleteDialog'
import { ApiKeyPill } from './ApiKeyPill'
import { APIKeysData } from 'data/api-keys/api-keys-query'
export const APIKeyRow = ({
apiKey,
@@ -44,12 +32,13 @@ export const APIKeyRow = ({
mass: 1,
}}
>
<TableCell className="py-2">{apiKey.description || '/'}</TableCell>
<TableCell className="py-2">{apiKey.name}</TableCell>
<TableCell className="py-2">
<div className="flex flex-row gap-2">
<Input apiKey={apiKey} />
<ApiKeyPill apiKey={apiKey} />
</div>
</TableCell>
<TableCell className="py-2">{apiKey.description || '/'}</TableCell>
<TableCell className="flex justify-end">
<DropdownMenu>
@@ -70,124 +59,3 @@ export const APIKeyRow = ({
</MotionTableRow>
)
}
function Input({
apiKey,
}: {
apiKey: Extract<APIKeysData[number], { type: 'secret' | 'publishable' }>
}) {
const queryClient = useQueryClient()
const { ref: projectRef } = useParams()
const [show, setShowState] = useState(false)
// to do
// const canReadAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*')
const isSecret = apiKey.type === 'secret'
const {
data,
isLoading: isLoadingApiKey,
error,
refetch: refetchApiKey,
} = useAPIKeyIdQuery(
{
projectRef,
id: apiKey.id as string,
reveal: true,
},
{
enabled: show,
staleTime: 0, // Data is considered stale immediately
cacheTime: 0, // Cache is cleared immediately after query becomes stale
}
)
async function onSubmitShow() {
setShowState(true)
// Set a timeout to invalidate the cache after a certain amount of time
setTimeout(() => {
setShowState(false)
queryClient.removeQueries({
queryKey: apiKeysKeys.single(projectRef, apiKey.id as string),
exact: true,
})
}, 10000) // Destroy query after 10 seconds
}
async function onCopy() {
// if ID already exists from a reveal action, return that
if (data?.api_key) return data?.api_key
try {
// fetch ID and then destroy query immediately
const result = await refetchApiKey()
queryClient.removeQueries({
queryKey: apiKeysKeys.single(projectRef, apiKey.id as string),
exact: true,
})
if (result.isSuccess) return result.data.api_key
if (error) {
toast.error('Failed to copy secret API key')
}
} catch (error) {
console.error('Failed to fetch API key:', error)
}
// Fallback to the masked version
return apiKey.api_key
}
return (
<>
<div
className={cn(
InputVariants({ size: 'tiny' }),
'flex-1 grow gap-0 font-mono rounded-full max-w-60 overflow-hidden',
show ? 'ring-1 ring-foreground-lighter ring-opacity-50' : 'ring-0 ring-opacity-0',
'transition-all'
)}
>
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={show ? 'shown' : 'hidden'}
initial={{ opacity: 0, y: show ? 16 : -16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: show ? 16 : -16 }}
transition={{
duration: 0.12,
y: { type: 'spring', stiffness: 2450, damping: 55 },
}}
className="truncate"
>
{show ? data?.api_key : apiKey?.api_key}
</motion.span>
</AnimatePresence>
</div>
{isSecret && (
<AnimatePresence>
{!show && (
<motion.div
initial={{ opacity: 0, scale: 1, width: 0 }}
animate={{ opacity: 1, scale: 1, width: 'auto' }}
exit={{ opacity: 0, scale: 1, width: 0 }}
transition={{ duration: 0.12 }}
style={{ overflow: 'hidden' }}
>
<Button
type="outline"
className="rounded-full px-2"
icon={<Eye strokeWidth={2} />}
onClick={onSubmitShow}
/>
</motion.div>
)}
</AnimatePresence>
)}
<CopyButton type="default" asyncText={onCopy} iconOnly className="rounded-full px-2" />
</>
)
}
@@ -0,0 +1,201 @@
import { useQueryClient } from '@tanstack/react-query'
import { AnimatePresence, motion } from 'framer-motion'
import { Eye } from 'lucide-react'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { InputVariants } from '@ui/components/shadcn/ui/input'
import { useParams } from 'common'
import CopyButton from 'components/ui/CopyButton'
import { useAPIKeyIdQuery } from 'data/api-keys/[id]/api-key-id-query'
import { APIKeysData } from 'data/api-keys/api-keys-query'
import { apiKeysKeys } from 'data/api-keys/keys'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { Button, cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
export function ApiKeyPill({
apiKey,
}: {
apiKey: Extract<APIKeysData[number], { type: 'secret' | 'publishable' }>
}) {
const queryClient = useQueryClient()
const { ref: projectRef } = useParams()
// State that controls whether to show the full API key
const [show, setShowState] = useState(false)
const isSecret = apiKey.type === 'secret'
// Permission check for revealing/copying secret API keys
const { can: canManageSecretKeys, isLoading: isLoadingPermission } =
useAsyncCheckProjectPermissions(PermissionAction.READ, 'service_api_keys')
// This query only runs when show=true (enabled: show)
// It fetches the fully revealed API key when needed
const {
data,
isLoading: isLoadingApiKey,
error,
refetch: refetchApiKey,
} = useAPIKeyIdQuery(
{
projectRef,
id: apiKey.id as string,
reveal: true, // Request the unmasked key
},
{
enabled: show, // Only run query when show is true
staleTime: 0, // Always consider data stale
cacheTime: 0, // Don't cache the key data
}
)
// Auto-hide timer for the API key (security feature)
useEffect(() => {
if (show && data?.api_key) {
// Auto-hide the key after 10 seconds
const timer = setTimeout(() => {
setShowState(false)
// Clear the cached key from memory
queryClient.removeQueries({
queryKey: apiKeysKeys.single(projectRef, apiKey.id as string),
exact: true,
})
}, 10000) // Hide after 10 seconds
return () => clearTimeout(timer)
}
}, [show, data?.api_key, projectRef, queryClient, apiKey.id])
async function onSubmitShow() {
// Don't reveal key if not allowed or loading
if (isSecret && !canManageSecretKeys) return
if (isLoadingPermission) return
// This will enable the API key query to fetch and reveal the key
setShowState(true)
}
async function onCopy() {
// If key is already revealed, use that value
if (data?.api_key) return data?.api_key
try {
// Fetch full key and immediately clear from cache after copying
const result = await refetchApiKey()
queryClient.removeQueries({
queryKey: apiKeysKeys.single(projectRef, apiKey.id as string),
exact: true,
})
if (result.isSuccess) return result.data.api_key
if (error) {
toast.error('Failed to copy secret API key')
}
} catch (error) {
console.error('Failed to fetch API key:', error)
}
// Fallback to the masked version if fetch fails
return apiKey.api_key
}
// States for disabling buttons/showing tooltips
const isRestricted = isSecret && !canManageSecretKeys
const isLoading = isLoadingPermission
return (
<>
<AnimatePresence mode="wait" initial={false}>
<div
className={cn(
InputVariants({ size: 'tiny' }),
'flex-1 grow gap-0 font-mono rounded-full',
isSecret ? 'overflow-hidden' : '',
show ? 'ring-1 ring-foreground-lighter ring-opacity-50' : 'ring-0 ring-opacity-0',
'transition-all',
'max-w-[380px]',
'cursor-text',
'relative'
)}
style={{ userSelect: 'all' }}
>
{isSecret ? (
<>
<span>{apiKey?.api_key.slice(0, 15)}</span>
<motion.span
key={show ? 'shown' : 'hidden'}
initial={{ opacity: 0, y: show ? 16 : -16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: show ? 16 : -16 }}
transition={{
duration: 0.1,
y: { type: 'spring', stiffness: 1000, damping: 55 },
}}
>
{show && data?.api_key ? data?.api_key.slice(15) : '••••••••••••••••'}
</motion.span>
</>
) : (
<span>{apiKey?.api_key}</span>
)}
</div>
</AnimatePresence>
{/* Reveal button - only shown for secret keys and when not already revealed */}
{isSecret && (
<AnimatePresence initial={false}>
{!show && (
<motion.div
initial={{ opacity: 0, scale: 1, width: 0 }}
animate={{ opacity: 1, scale: 1, width: 'auto' }}
exit={{ opacity: 0, scale: 1, width: 0 }}
transition={{ duration: 0.12 }}
style={{ overflow: 'hidden' }}
>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="outline"
className="rounded-full px-2 pointer-events-auto cursor-default"
icon={<Eye strokeWidth={2} />}
onClick={onSubmitShow}
disabled={isRestricted}
/>
</TooltipTrigger>
<TooltipContent side="bottom">
{isRestricted
? 'You need additional permissions to reveal secret API keys'
: isLoadingPermission
? 'Loading permissions...'
: 'Reveal API key'}
</TooltipContent>
</Tooltip>
</motion.div>
)}
</AnimatePresence>
)}
<Tooltip>
<TooltipTrigger asChild>
<CopyButton
type="default"
asyncText={onCopy}
iconOnly
className="rounded-full px-2 pointer-events-auto cursor-default"
disabled={isRestricted || isLoadingPermission}
/>
</TooltipTrigger>
<TooltipContent side="bottom">
{isRestricted
? 'You need additional permissions to copy secret API keys'
: isLoadingPermission
? 'Loading permissions...'
: 'Copy API key'}
</TooltipContent>
</Tooltip>
</>
)
}
@@ -0,0 +1,200 @@
import { ExternalLink, Github, Mail } from 'lucide-react'
import { LOCAL_STORAGE_KEYS } from 'common'
import { FeatureBanner } from 'components/ui/FeatureBanner'
import { APIKeysData } from 'data/api-keys/api-keys-query'
import {
Button,
Card,
CardContent,
Separator,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from 'ui'
import { ApiKeyPill } from './ApiKeyPill'
import { CreateNewAPIKeysButton } from './CreateNewAPIKeysButton'
import { useApiKeysVisibility } from './hooks/useApiKeysVisibility'
// Mock API Keys for demo
const mockApiKeys = [
{
id: 'mock-id-2',
type: 'publishable',
api_key: 'sb_publishable_ltaNA7nnVozoSCOcZIjg',
name: 'web',
},
{
id: 'mock-id-3',
type: 'publishable',
api_key: 'sb_publishable_YpotEpinEWsC2dI7FIKI',
name: 'mobile',
},
{
id: 'mock-id-1',
type: 'secret',
api_key: 'sb_secret_8I4Se•••••••••••••',
name: 'backend_api',
},
] as Extract<APIKeysData[number], { type: 'secret' | 'publishable' }>[]
/**
* Reusable table illustration component
*/
export const ApiKeysTableIllustration = () => {
return (
<Card className="w-full overflow-hidden opacity-60 pointer-events-none bg-surface-100">
<CardContent className="p-0">
<Table className="p-5">
<TableHeader>
<TableRow className="bg-200">
<TableHead
key=""
className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2 overflow-hidden w-[180px]"
>
Name
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2 pr-0">
API Key
</TableHead>
<TableHead
className="text-right font-mono uppercase text-xs text-foreground-lighter h-auto py-2"
key="actions"
/>
</TableRow>
</TableHeader>
<TableBody>
{mockApiKeys.map((apiKey) => (
<TableRow key={apiKey.id}>
<TableCell className="py-2 w-[180px]">{apiKey.name}</TableCell>
<TableCell className="py-2">
<div className="flex flex-row gap-2">
<ApiKeyPill apiKey={apiKey} />
</div>
</TableCell>
<TableCell />
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
)
}
/**
* Reusable illustration with gradient overlay component
*/
export const ApiKeysIllustrationWithOverlay = () => {
return (
<>
{/* Gradient overlay - horizontal on desktop, vertical on mobile */}
<div
className="absolute inset-x-0 bottom-0 h-1/2 xl:h-full xl:inset-x-auto xl:-right-16 xl:top-3 w-full xl:w-2/3
bg-gradient-to-t xl:bg-gradient-to-l
from-background-alternative via-background-alternative/90 via-[5%] to-transparent
z-[3] pointer-events-none xl:max-w-[500px]"
/>
<div className="absolute scale-100 left-10 -bottom-14 w-[720px] xl:w-[500px] xl:left-auto xl:-right-[200px] 2xl:-right-16 xl:top-[21px] xl:scale-75">
<ApiKeysTableIllustration />
</div>
</>
)
}
/**
* "Coming Soon" banner for users who don't have the feature flag enabled
*/
export const ApiKeysComingSoonBanner = () => {
return (
<FeatureBanner illustration={<ApiKeysIllustrationWithOverlay />} bgAlt>
<div className="flex flex-col gap-0 z-[2]">
<p className="text-sm text-foreground">New API keys are coming soon</p>
<p className="text-sm text-foreground-lighter lg:max-w-sm 2xl:max-w-none">
We're rolling out new API keys to better support your application needs.
</p>
<div className="mt-4">
<Button type="default" icon={<Github />}>
Learn more
</Button>
</div>
</div>
</FeatureBanner>
)
}
/**
* Create API Keys callout for users who have the feature flag enabled but no keys yet
*/
export const ApiKeysCreateCallout = () => {
const { canInitApiKeys } = useApiKeysVisibility()
if (!canInitApiKeys) return null
return (
<FeatureBanner illustration={<ApiKeysIllustrationWithOverlay />} bgAlt>
<div className="flex flex-col gap-0 z-[2]">
<p className="text-sm text-foreground">Create API keys</p>
<p className="text-sm text-foreground-lighter lg:max-w-sm 2xl:max-w-none">
Use keys to authenticate requests to your app
</p>
<div className="mt-4">
<CreateNewAPIKeysButton />
</div>
</div>
</FeatureBanner>
)
}
/**
* Feedback banner for users who have API keys and the feature is rolled out to them
*/
export const ApiKeysFeedbackBanner = () => {
const { hasApiKeys, isInRollout } = useApiKeysVisibility()
// Don't show anything if not in rollout or if keys don't exist
if (!isInRollout || !hasApiKeys) {
return null
}
return (
<FeatureBanner
storageKey={LOCAL_STORAGE_KEYS.API_KEYS_FEEDBACK_DISMISSED}
className="!p-0 flex flex-col gap-0"
dismissable
>
<div className="p-5">
<p className="text-sm text-foreground">Your new API keys are here</p>
<p className="text-sm text-foreground-lighter">
We've updated our API keys to better support your application needs.{' '}
<a
href="https://github.com/orgs/supabase/discussions/29260"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1 text-foreground"
>
Join the discussion on GitHub <ExternalLink size={14} strokeWidth={1.5} />
</a>
</p>
</div>
<Separator className="w-full" />
<div className="px-5 py-2 bg-surface-200/30">
<p className="text-sm text-foreground-lighter">
Having trouble with the new API keys?{' '}
<a
href="/support/new?category=Problem&subject=Help%20with%20API%20keys&message=I'm%20experiencing%20problems%20with%20the%20new%20API%20keys%20feature.%20Please%20describe%20your%20specific%20issue%20here."
className="inline-flex items-center gap-1 text-foreground"
>
Contact support
</a>
</p>
</div>
</FeatureBanner>
)
}
@@ -0,0 +1,72 @@
import { useState } from 'react'
import { useParams } from 'common'
import { useAPIKeyCreateMutation } from 'data/api-keys/api-key-create-mutation'
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
Button,
} from 'ui'
export const CreateNewAPIKeysButton = () => {
const { ref: projectRef } = useParams()
const [createKeysDialogOpen, setCreateKeysDialogOpen] = useState(false)
const [isCreatingKeys, setIsCreatingKeys] = useState(false)
const { mutate: createAPIKey } = useAPIKeyCreateMutation()
const handleCreateNewApiKeys = async () => {
if (!projectRef) return
setIsCreatingKeys(true)
try {
// Create publishable key
await createAPIKey({
projectRef,
type: 'publishable',
name: 'default',
})
// Create secret key
await createAPIKey({
projectRef,
type: 'secret',
name: 'default',
})
setCreateKeysDialogOpen(false)
} catch (error) {
console.error('Failed to create API keys:', error)
} finally {
setIsCreatingKeys(false)
}
}
return (
<AlertDialog open={createKeysDialogOpen} onOpenChange={setCreateKeysDialogOpen}>
<Button onClick={() => setCreateKeysDialogOpen(true)}>Create new API keys</Button>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Create new API keys</AlertDialogTitle>
<AlertDialogDescription>
This will create a default publishable key and a default secret key named{' '}
<code>default</code>. These keys are required to connect your application to your
Supabase project.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={handleCreateNewApiKeys} disabled={isCreatingKeys}>
{isCreatingKeys ? 'Creating...' : 'Create keys'}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)
}
@@ -1,6 +1,5 @@
import { useState } from 'react'
import * as z from 'zod'
import { zodResolver } from '@hookform/resolvers/zod'
import { useState } from 'react'
import { SubmitHandler, useForm } from 'react-hook-form'
import {
Button,
@@ -17,25 +16,24 @@ import {
FormField_Shadcn_,
Form_Shadcn_,
Input_Shadcn_,
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
Alert_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import * as z from 'zod'
import { useAPIKeyCreateMutation } from 'data/api-keys/api-key-create-mutation'
import { useParams } from 'next/navigation'
const FORM_ID = 'create-publishable-api-key'
const SCHEMA = z.object({
description: z.string(),
name: z.string(),
description: z.string().trim(),
})
export interface CreatePublishableAPIKeyModalProps {
export interface CreatePublishableAPIKeyDialogProps {
projectRef: string
}
function CreatePublishableAPIKeyModal() {
function CreatePublishableAPIKeyDialog() {
const params = useParams()
const projectRef = params?.ref as string
@@ -48,6 +46,7 @@ function CreatePublishableAPIKeyModal() {
const form = useForm<z.infer<typeof SCHEMA>>({
resolver: zodResolver(SCHEMA),
defaultValues: {
name: '',
description: '',
},
})
@@ -59,7 +58,8 @@ function CreatePublishableAPIKeyModal() {
{
projectRef,
type: 'publishable',
description: values.description.trim(),
name: values.name,
description: values.description,
},
{
onSuccess: () => {
@@ -93,6 +93,21 @@ function CreatePublishableAPIKeyModal() {
id={FORM_ID}
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField_Shadcn_
key="name"
name="name"
control={form.control}
render={({ field }) => (
<FormItemLayout
label="Name"
description="A short name of lowercase alphanumeric characters and underscore, must start with letter or underscore."
>
<FormControl_Shadcn_>
<Input_Shadcn_ {...field} />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
key="description"
name="description"
@@ -121,4 +136,4 @@ function CreatePublishableAPIKeyModal() {
)
}
export default CreatePublishableAPIKeyModal
export default CreatePublishableAPIKeyDialog
@@ -26,10 +26,11 @@ import { Plus } from 'lucide-react'
const FORM_ID = 'create-secret-api-key'
const SCHEMA = z.object({
description: z.string(),
name: z.string(),
description: z.string().trim(),
})
const CreateSecretAPIKeyModal = () => {
const CreateSecretAPIKeyDialog = () => {
const [visible, setVisible] = useState(false)
const { ref: projectRef } = useParams()
@@ -40,6 +41,7 @@ const CreateSecretAPIKeyModal = () => {
const form = useForm<z.infer<typeof SCHEMA>>({
resolver: zodResolver(SCHEMA),
defaultValues: {
name: '',
description: '',
},
})
@@ -51,7 +53,8 @@ const CreateSecretAPIKeyModal = () => {
{
projectRef,
type: 'secret',
description: values.description.trim(),
name: values.name,
description: values.description,
},
{
onSuccess: () => {
@@ -71,10 +74,15 @@ const CreateSecretAPIKeyModal = () => {
<DialogContent>
<DialogHeader>
<DialogTitle>Create new secret API key</DialogTitle>
<DialogDescription>
Secret API keys are used to authorize requests to your project from servers, functions,
workers or other backend components of your application. Keep them secret, don't publish
them online and don't check them in source code.
<DialogDescription className="grid gap-y-2">
<p>
Secret API keys are used to authorize requests to your project from servers,
functions, workers or other backend components of your application.{' '}
</p>
<p>
Keep these keys private. Don't publish them online or commit them to source control.
</p>
</DialogDescription>
</DialogHeader>
<DialogSectionSeparator />
@@ -85,6 +93,21 @@ const CreateSecretAPIKeyModal = () => {
id={FORM_ID}
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField_Shadcn_
key="name"
name="name"
control={form.control}
render={({ field }) => (
<FormItemLayout
label="Name"
description="A short name of lowercase alphanumeric characters and underscore, must start with letter or underscore."
>
<FormControl_Shadcn_>
<Input_Shadcn_ {...field} />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
key="description"
name="description"
@@ -113,4 +136,4 @@ const CreateSecretAPIKeyModal = () => {
)
}
export default CreateSecretAPIKeyModal
export default CreateSecretAPIKeyDialog
@@ -1,17 +1,10 @@
import { FormHeader } from 'components/ui/Forms/FormHeader'
import DisplayApiSettings from 'components/ui/ProjectSettings/DisplayApiSettings'
const LegacyAPIKeys = () => {
export function LegacyAPIKeys() {
return (
<div>
<FormHeader
title="Legacy API keys"
description="Legacy, JWT-based API keys. Support for these is ending in October 2025. Prefer Publishable and Secret API keys instead."
/>
<DisplayApiSettings legacy showNotice={false} />
</div>
)
return
}
export default LegacyAPIKeys
@@ -8,9 +8,8 @@ import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPe
import { Link } from 'lucide-react'
import { useMemo } from 'react'
import { cn, EyeOffIcon, Input_Shadcn_, Skeleton, WarningIcon } from 'ui'
import QuickKeyCopyWrapper from './QuickKeyCopy'
// to add in later with follow up PR
// import CreatePublishableAPIKeyModal from './CreatePublishableAPIKeyModal'
// import CreatePublishableAPIKeyDialog from './CreatePublishableAPIKeyDialog'
// to add in later with follow up PR
// import ShowPublicJWTsDialogComposer from '../JwtSecrets/ShowPublicJWTsDialogComposer'
@@ -37,12 +36,12 @@ export const PublishableAPIKeys = () => {
<div>
<FormHeader
title="Publishable key"
description="Use these API keys on the web, in mobile or desktop apps, CLIs or other public components of your app. It's safe to publish these."
description="This key is safe to use in a browser if you have enabled Row Level Security (RLS) for your tables and configured policies."
/>
<div className="flex flex-col gap-8">
<div className="-space-y-px w-content w-fit">
<div className="bg-surface-100 px-5 py-2 flex items-center gap-5 first:rounded-t-md border">
<span className="text-sm">Default publishable key</span>
<span className="text-sm">Publishable key</span>
<div className="flex items-center gap-2">
<ApiKeyInput />
<CopyButton
@@ -66,17 +65,12 @@ export const PublishableAPIKeys = () => {
)}
</div>
<div className="flex flex-col gap-2 max-w-64">
<div className="flex items-center gap-2 text-xs text-foreground-light hover:text-foreground cursor-pointer">
<Link size={14} className="text-foreground-light" /> Show Supabase Url
</div>
{/* <Separator /> */}
{/* @mildtomato - To add in later with follow up PR */}
{/* <ShowPublicJWTsDialogComposer /> */}
</div>
{/* <CreatePublishableAPIKeyModal /> */}
<QuickKeyCopyWrapper />
</div>
</div>
)
@@ -98,7 +92,7 @@ function ApiKeyInput() {
// The default publisahble key will always be the first one
const apiKey = publishableApiKeys[0]
const baseClasses = 'flex-1 grow gap-1 rounded-full min-w-[300px] truncate'
const baseClasses = 'flex-1 grow gap-1 rounded-full min-w-[32em]'
const size = 'tiny'
if (isApiKeysLoading || isPermissionsLoading) {
@@ -1,136 +0,0 @@
import React, { useState } from 'react'
import { Button, EyeIcon, EyeOffIcon, Skeleton, WarningIcon } from 'ui'
import { ChevronsUpDownIcon } from 'lucide-react'
import { useParams } from 'common'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { SimpleCodeBlock } from 'ui/src/components/SimpleCodeBlock'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
const frameworkOptions = ['React', 'Vue', 'Angular', 'Svelte'] // Add more as needed
const QuickKeyCopyWrapper = () => {
const [selectedFramework, setSelectedFramework] = useState('React')
return (
<div className="flex flex-col gap-0 bg-alternative/50 border rounded-lg overflow-hidden">
<div className="flex items-center gap-3 text-xs justify-between px-5 py-4">
<div className="flex flex-col gap-0">
<h4 className="text-sm">Quick key copy</h4>
<p className="text-foreground-lighter">
Choose your framework and paste the code into your environment file.
</p>
</div>
<Button
type="default"
iconRight={<ChevronsUpDownIcon />}
onClick={() => {
const currentIndex = frameworkOptions.indexOf(selectedFramework)
const nextIndex = (currentIndex + 1) % frameworkOptions.length
setSelectedFramework(frameworkOptions[nextIndex])
}}
>
{selectedFramework}
</Button>
</div>
<QuickKeyCopyContent selectedFramework={selectedFramework} />
</div>
)
}
const QuickKeyCopyContent = ({ selectedFramework }: { selectedFramework: string }) => {
const { ref: projectRef } = useParams()
const {
data: projectAPI,
isLoading: isProjectApiLoading,
error: projectApiError,
} = useProjectSettingsV2Query({
projectRef: projectRef as string,
})
const {
data: apiKeysData,
isLoading: isApiKeysLoading,
error: apiKeysError,
} = useAPIKeysQuery({
projectRef: projectRef as string,
reveal: false,
})
const isPermissionsLoading = !usePermissionsLoaded()
const canReadAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*')
const publishableApiKey = apiKeysData?.find(({ type }) => type === 'publishable')?.api_key
const dataErrors = projectApiError || apiKeysError
const permissionErrors = !canReadAPIKeys && !isPermissionsLoading
if (isProjectApiLoading || isApiKeysLoading || isPermissionsLoading) {
return (
<div className="bg-alternative px-5 py-3 border-t overflow-hidden flex flex-col gap-0 h-20">
<div className="flex items-center gap-2 mb-3">
<Skeleton className="h-4 w-4 rounded" />
<Skeleton className="h-4 w-[48px] rounded" />
</div>
<Skeleton className="h-[40px] w-3/4 rounded" />
</div>
)
}
const EmptyContainer = ({ children }: { children: React.ReactNode }) => {
return (
<div className="bg-alternative justify-center px-5 py-3 border-t overflow-hidden flex flex-col gap-1 h-20">
{children}
</div>
)
}
// if (!canReadAPIKeys) {
// return (
// <EmptyContainer>
// <div className="flex items-center gap-2">
// <EyeOffIcon />
// <p className="text-sm text-foreground">You do not have permission to read API Keys</p>
// </div>
// <p className="text-foreground-light text-xs">
// Please contact your project admin/owner to request access.
// </p>
// </EmptyContainer>
// )
// }
// TO DO : this needs to be changed to just if(error)
// currently it's not working as API returns an error.
if (dataErrors) {
return (
<EmptyContainer>
<div className="flex items-center gap-2">
<WarningIcon />
<p className="text-sm text-warning-600">Error loading Secret API Keys</p>
</div>
<p className="text-warning/75 text-xs">
{projectApiError?.message ?? apiKeysError?.message ?? 'Error: Failed to load API keys'}
</p>
</EmptyContainer>
)
}
const getEnvContent = () => {
return `
NEXT_PUBLIC_SUPABASE_URL=${projectAPI?.app_config?.endpoint || ''}
NEXT_PUBLIC_SUPABASE_PUBLISHABLE_API_KEY=${!canReadAPIKeys ? 'You do not have permission to read the Publishable API key' : publishableApiKey || ''}
`
}
return (
<div className="bg-alternative px-5 py-3 border-t overflow-hidden">
<div className="flex items-center gap-2 mb-3">
<div className="bg-foreground w-4 h-4 flex items-end justify-center rounded-sm">
<span className="font-mono text-[8px] text-background font-bold">env</span>
</div>
<span className="font-mono text-xs">.env.local</span>
</div>
<SimpleCodeBlock parentClassName="!p-0">{getEnvContent()}</SimpleCodeBlock>
</div>
)
}
export default QuickKeyCopyWrapper
@@ -15,7 +15,7 @@ import {
TableRow,
} from 'ui/src/components/shadcn/ui/table'
import { APIKeyRow } from './APIKeyRow'
import CreateSecretAPIKeyModal from './CreateSecretAPIKeyModal'
import CreateSecretAPIKeyDialog from './CreateSecretAPIKeyDialog'
export const SecretAPIKeys = () => {
const { ref: projectRef } = useParams()
@@ -56,23 +56,29 @@ export const SecretAPIKeys = () => {
<div>
<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. Keep secret and never publish."
actions={<CreateSecretAPIKeyModal />}
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 />}
/>
<Card className={cn('w-full overflow-hidden', !empty && 'bg-surface-100')}>
<CardContent className="p-0">
<Table className="p-5">
<Table className="p-5 table-auto">
<TableHeader>
<TableRow className={cn('bg-200', empty && 'hidden')}>
<TableHead
key=""
className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2 overflow-hidden"
className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2"
>
Name
</TableHead>
<TableHead className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2 pr-0">
API Key
</TableHead>
<TableHead
key=""
className="text-left font-mono uppercase text-xs text-foreground-lighter h-auto py-2"
>
Description
</TableHead>
<TableHead
className="text-right font-mono uppercase text-xs text-foreground-lighter h-auto py-2"
key="actions"
@@ -127,9 +133,9 @@ export const SecretAPIKeys = () => {
return (
<TableContainer>
<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 exist</p>
<p className="text-sm text-foreground">No secret API keys found</p>
<p className="text-sm text-foreground-light">
Your project can't be accessed using secret API keys.
Your project is not accessible via secret keys—there are no active secret keys created.
</p>
</div>
</TableContainer>
@@ -0,0 +1,54 @@
import { useMemo } from 'react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useFlag } from 'hooks/ui/useFlag'
interface ApiKeysVisibilityState {
isInRollout: boolean
hasApiKeys: boolean
isLoading: boolean
canReadAPIKeys: boolean
canInitApiKeys: boolean
shouldDisableUI: boolean
}
/**
* A hook that provides visibility states for API keys UI components
* Consolidates logic for determining access to API keys functionality
*/
export function useApiKeysVisibility(): ApiKeysVisibilityState {
const { ref: projectRef } = useParams()
const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'api_keys')
const isInRollout = useFlag('basicApiKeys')
const { data: apiKeysData, isLoading } = useAPIKeysQuery({
projectRef,
reveal: false,
})
const publishableApiKeys = useMemo(
() => apiKeysData?.filter(({ type }) => type === 'publishable') ?? [],
[apiKeysData]
)
// Check if there are any publishable API keys
// we don't check for secret keys because they can be optionally deleted
const hasApiKeys = publishableApiKeys.length > 0
// Can initialize API keys when in rollout, has permissions, not loading, and no API keys yet
const canInitApiKeys = hasApiKeys ? false : isInRollout && canReadAPIKeys && !isLoading
const shouldDisableUI = hasApiKeys ? false : !isInRollout
return {
isInRollout,
hasApiKeys,
isLoading,
canReadAPIKeys,
canInitApiKeys,
shouldDisableUI,
}
}
@@ -0,0 +1,64 @@
import { useParams } from 'common'
import Link from 'next/link'
import { Button, Badge, Menu } from 'ui'
import { ArrowUpRight } from 'lucide-react'
import { ProductMenu } from 'components/ui/ProductMenu'
import type { ProductMenuGroup } from 'components/ui/ProductMenu/ProductMenu.types'
import { Sidebar } from 'components/interfaces/Sidebar'
export const ApiKeysMoved = () => {
const { ref: projectRef } = useParams()
// Create menu structure matching the ProductMenu expected format
const mockMenu: ProductMenuGroup[] = [
{
title: 'Project Settings',
items: [
{ name: 'General', key: 'general', url: '#' },
{ name: 'Compute and Disk', key: 'compute-and-disk', url: '#' },
{ name: 'Infrastructure', key: 'infrastructure', url: '#' },
{ name: 'Integrations', key: 'integrations', url: '#' },
{ name: 'API Keys', key: 'api-keys', url: '#', label: 'NEW' },
{ name: 'Add Ons', key: 'addons', url: '#' },
{
name: 'Vault',
key: 'vault',
url: '#',
label: 'Alpha',
rightIcon: <ArrowUpRight strokeWidth={1} className="h-4 w-4" />,
},
],
},
{
title: 'Configuration',
items: [{ name: 'Database', key: 'database', url: '#' }],
},
]
return (
<div className="pb-36 pt-10 relative w-full flex flex-col xl:flex-row border xl:py-10 px-10 rounded-md overflow-hidden">
<div className="flex flex-col gap-0 z-[2]">
<p className="text-sm text-foreground">API keys have moved</p>
<p className="text-sm text-foreground-lighter mb-4">
They can now be found in the new API Keys page
</p>
<Link href={`/project/${projectRef}/settings/api-keys`}>
<Button type="default">Go to API Keys</Button>
</Link>
</div>
{/* Menu illustration using the actual ProductMenu component */}
<div className="absolute right-20 -top-[80px] w-fit scale-80 origin-top-right">
<div className="absolute top-0 right-0 bottom-0 left-0 bg-gradient-to-r from-transparent to-background-200/75 z-10" />
<div className="bg-studio-sidebar border rounded-md overflow-hidden shadow-sm opacity-75 flex flex-row w-[280px]">
<div className="opacity-50">
<Sidebar />
</div>
<ProductMenu page="api-keys" menu={mockMenu} />
</div>
</div>
</div>
)
}
@@ -21,6 +21,7 @@ import { Badge, Input } from 'ui'
import { JWT_SECRET_UPDATE_ERROR_MESSAGES } from './API.constants'
import JWTSettings from './JWTSettings'
import { PostgrestConfig } from './PostgrestConfig'
import { ApiKeysMoved } from './ApiKeysMoved'
const ServiceList = () => {
const client = useQueryClient()
@@ -143,8 +144,8 @@ const ServiceList = () => {
</Panel>
</section>
<section>
<DisplayApiSettings key="DisplayAPISettings" />
<section className="mb-8">
<ApiKeysMoved />
</section>
<section id="jwt-settings">
@@ -0,0 +1,36 @@
import { PageLayout } from 'components/layouts/PageLayout/PageLayout'
import { ScaffoldContainer } from 'components/layouts/Scaffold'
import { PropsWithChildren } from 'react'
import { useParams } from 'common'
const ApiKeysLayout = ({ children }: PropsWithChildren) => {
const { ref: projectRef } = useParams()
const navigationItems = [
{
label: 'API Keys',
href: `/project/${projectRef}/settings/api-keys`,
id: 'legacy-keys',
},
{
label: 'New API Keys (Coming Soon)',
href: `/project/${projectRef}/settings/api-keys/new`,
id: 'new-keys',
},
]
return (
<PageLayout
title="API Keys"
subtitle="Configure API keys to securely control access to your project"
navigationItems={navigationItems}
>
<ScaffoldContainer className="flex flex-col py-8 gap-8" bottomPadding>
{children}
</ScaffoldContainer>
</PageLayout>
)
}
export default ApiKeysLayout
@@ -3,7 +3,7 @@ import { useRouter } from 'next/router'
import { ReactNode } from 'react'
import { useParams } from 'common'
import { Button, cn, NavMenu, NavMenuItem } from 'ui'
import { Badge, Button, cn, NavMenu, NavMenuItem } from 'ui'
import { ScaffoldContainer } from '../Scaffold'
import { PageHeader } from './PageHeader'
@@ -13,6 +13,7 @@ export interface NavigationItem {
href?: string
icon?: ReactNode
onClick?: () => void
badge?: string
}
interface PageLayoutProps {
@@ -118,6 +119,7 @@ export const PageLayout = ({
>
{item.icon && <span>{item.icon}</span>}
{item.label}
{item.badge && <Badge variant="default">{item.badge}</Badge>}
</Link>
) : (
<Button
@@ -127,6 +129,7 @@ export const PageLayout = ({
>
{item.icon && <span className="mr-2">{item.icon}</span>}
{item.label}
{item.badge && <Badge variant="default">{item.badge}</Badge>}
</Button>
)}
</NavMenuItem>
@@ -58,17 +58,13 @@ export const generateSettingsMenu = (
url: `/project/${ref}/settings/integrations`,
items: [],
},
...(features?.newApiKeys
? [
{
name: 'API Keys',
key: 'api-keys',
url: `/project/${ref}/settings/api-keys`,
items: [],
label: 'NEW',
},
]
: []),
{
name: 'API Keys',
key: 'api-keys',
url: `/project/${ref}/settings/api-keys`,
items: [],
label: 'NEW',
},
]
: []),
{
@@ -1,90 +0,0 @@
import {
ScaffoldContainer,
ScaffoldDescription,
ScaffoldHeader,
ScaffoldTitle,
} from 'components/layouts/Scaffold'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { Sparkles } from 'lucide-react'
import { PropsWithChildren } from 'react'
import {
buttonVariants,
cn,
Separator,
Tabs_Shadcn_,
TabsList_Shadcn_,
TabsTrigger_Shadcn_,
} from 'ui'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
const ApiKeysLayout = ({ children }: PropsWithChildren) => {
const { ref: projectRef } = useParams()
const [apiKeysView, setApiKeysViewState] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.API_KEYS_VIEW(projectRef ?? ''),
'new-keys'
)
return (
<>
<ScaffoldContainer>
<ScaffoldHeader>
<ScaffoldTitle>API Keys</ScaffoldTitle>
<ScaffoldDescription>
Configure API keys that help secure your project
</ScaffoldDescription>
</ScaffoldHeader>
</ScaffoldContainer>
<ScaffoldContainer className="flex flex-col gap-10">
<Tabs_Shadcn_
defaultValue={apiKeysView}
className="relative"
value={apiKeysView}
onValueChange={setApiKeysViewState}
>
<div className="flex-shrink-0">
<TabsList_Shadcn_ className="inline-flex gap-1 p-1 bg-surface-75 bg-opacity-100 border rounded-lg">
<TabsTrigger_Shadcn_
value="new-keys"
className={cn(
buttonVariants({
type: apiKeysView === 'new-keys' ? 'default' : 'text',
size: 'tiny',
}),
apiKeysView !== 'new-keys'
? 'text-foreground-lighter'
: '!border-foreground-muted',
'flex gap-2'
)}
>
<Sparkles size={13} strokeWidth={1.2} className="text-foreground-light" />
New Keys
</TabsTrigger_Shadcn_>
<TabsTrigger_Shadcn_
value="legacy-keys"
className={cn(
buttonVariants({
type: apiKeysView === 'legacy-keys' ? 'default' : 'text',
size: 'tiny',
}),
apiKeysView !== 'legacy-keys'
? 'text-foreground-lighter'
: '!border-foreground-muted'
)}
>
Legacy Keys
</TabsTrigger_Shadcn_>
</TabsList_Shadcn_>
</div>
</Tabs_Shadcn_>
</ScaffoldContainer>
<ScaffoldContainer className="flex flex-col gap-10 py-8" bottomPadding>
<Separator />
{children}
</ScaffoldContainer>
</>
)
}
export default ApiKeysLayout
+56 -42
View File
@@ -1,8 +1,8 @@
import { Check, Clipboard } from 'lucide-react'
import { useEffect, useState } from 'react'
import React, { forwardRef, useEffect, useState } from 'react'
import { copyToClipboard } from 'lib/helpers'
import { Button, ButtonProps, cn } from 'ui'
import { Button, cn } from 'ui'
type CopyButtonBaseProps = {
iconOnly?: boolean
@@ -21,48 +21,62 @@ type CopyButtonWithAsyncText = CopyButtonBaseProps & {
asyncText: () => Promise<string> | string
}
export type CopyButtonProps = (CopyButtonWithText | CopyButtonWithAsyncText) & ButtonProps
export type CopyButtonProps = (CopyButtonWithText | CopyButtonWithAsyncText) &
React.ComponentProps<typeof Button>
const CopyButton = ({
text,
asyncText,
iconOnly = false,
children,
onClick,
copyLabel = 'Copy',
copiedLabel = 'Copied',
...props
}: CopyButtonProps) => {
const [showCopied, setShowCopied] = useState(false)
const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
(
{
text,
asyncText,
iconOnly = false,
children,
onClick,
copyLabel = 'Copy',
copiedLabel = 'Copied',
...props
},
ref
) => {
const [showCopied, setShowCopied] = useState(false)
useEffect(() => {
if (!showCopied) return
const timer = setTimeout(() => setShowCopied(false), 2000)
return () => clearTimeout(timer)
}, [showCopied])
useEffect(() => {
if (!showCopied) return
const timer = setTimeout(() => setShowCopied(false), 2000)
return () => clearTimeout(timer)
}, [showCopied])
return (
<Button
onClick={async (e) => {
const textToCopy = asyncText ? await asyncText() : text
setShowCopied(true)
await copyToClipboard(textToCopy!)
onClick?.(e)
}}
{...props}
className={cn(
{
'px-1': iconOnly,
},
props.className
)}
icon={
showCopied ? <Check strokeWidth={2} className="text-brand" /> : props.icon ?? <Clipboard />
}
>
{!iconOnly && <>{children ?? (showCopied ? copiedLabel : copyLabel)}</>}
</Button>
)
}
return (
<Button
ref={ref}
onClick={async (e) => {
const textToCopy = asyncText ? await asyncText() : text
setShowCopied(true)
await copyToClipboard(textToCopy!)
onClick?.(e)
}}
{...props}
className={cn(
{
'px-1': iconOnly,
// '!pointer-events-auto': props.disabled,
},
props.className
)}
icon={
showCopied ? (
<Check strokeWidth={2} className="text-brand" />
) : (
props.icon ?? <Clipboard />
)
}
>
{!iconOnly && <>{children ?? (showCopied ? copiedLabel : copyLabel)}</>}
</Button>
)
}
)
CopyButton.displayName = 'CopyButton'
export default CopyButton
@@ -0,0 +1,38 @@
import React, { forwardRef } from 'react'
import { cn } from 'ui'
interface DisableInteractionProps extends React.HTMLAttributes<HTMLDivElement> {
disabled?: boolean
}
/**
* DisableInteraction component
*
* A utility component that wraps content and prevents all user interactions when disabled
* including clicking, hovering, and text selection.
*
* @example
* <DisableInteraction disabled={isDisabled}>
* <YourContent />
* </DisableInteraction>
*/
export const DisableInteraction = forwardRef<HTMLDivElement, DisableInteractionProps>(
({ disabled, style, className, ...props }, ref) => (
<div
ref={ref}
{...props}
className={cn(disabled && 'opacity-50 pointer-events-none', className)}
style={{
...(disabled && {
userSelect: 'none',
WebkitUserSelect: 'none',
MozUserSelect: 'none',
msUserSelect: 'none',
}),
...style,
}}
/>
)
)
DisableInteraction.displayName = 'DisableInteraction'
@@ -0,0 +1,88 @@
import { X } from 'lucide-react'
import { ReactNode } from 'react'
import { motion, HTMLMotionProps } from 'framer-motion'
import { useParams } from 'common/hooks'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { Button, cn } from 'ui'
// Base props common to all feature banners
interface BaseFeatureBannerProps extends HTMLMotionProps<'div'> {
children: ReactNode
className?: string
dismissClassName?: string
defaultDismissed?: boolean
illustration?: ReactNode
bgAlt?: boolean
}
// Type for non-dismissable banners (no storageKey needed)
interface NonDismissableFeatureBannerProps extends BaseFeatureBannerProps {
dismissable?: false
storageKey?: never
}
// Type for dismissable banners (requires storageKey)
interface DismissableFeatureBannerProps extends BaseFeatureBannerProps {
dismissable: true
storageKey: string | ((ref: string) => string)
}
// Union type that enforces the constraint
export type FeatureBannerProps = NonDismissableFeatureBannerProps | DismissableFeatureBannerProps
export const FeatureBanner = ({
storageKey,
children,
className,
dismissClassName,
defaultDismissed = false,
illustration,
dismissable = false,
bgAlt = false,
...props
}: FeatureBannerProps) => {
const { ref } = useParams()
const key = storageKey && typeof storageKey === 'function' ? storageKey(ref ?? '') : storageKey
const [isDismissed, setIsDismissed] = useLocalStorageQuery(
key || 'feature-banner-dismissed',
defaultDismissed
)
if (dismissable && storageKey && isDismissed) return null
return (
<motion.div
initial={{ opacity: 0, y: 6 }}
animate={{ opacity: 1, y: 0 }}
transition={{
type: 'spring',
stiffness: 500,
damping: 30,
mass: 1,
}}
{...props}
className={cn(
'pb-36 pt-10 relative w-full border xl:py-10 px-10 rounded-md overflow-hidden',
bgAlt && 'bg-background-alternative',
className
)}
>
{children}
{illustration}
{dismissable && storageKey && (
<div className={cn('absolute top-3 right-3', dismissClassName)}>
<Button
type="text"
size="tiny"
icon={<X size={16} strokeWidth={1.5} />}
onClick={() => setIsDismissed(true)}
className="opacity-75 px-1"
aria-label="Dismiss notification"
/>
</div>
)}
</motion.div>
)
}
@@ -23,7 +23,7 @@ const FormHeader = ({
>
<div className="space-y-1">
<h3 className="text-foreground text-xl prose">{title}</h3>
{description && <div className="prose text-sm max-w-full">{description}</div>}
{description && <div className="prose text-sm max-w-2xl">{description}</div>}
</div>
<div className="flex flex-col sm:flex-row md:items-center gap-x-2">
{docsUrl !== undefined && <DocsButton href={docsUrl} />}
@@ -7,7 +7,8 @@ import { apiKeysKeys } from './keys'
export type APIKeyCreateVariables = {
projectRef?: string
description: string
name: string
description?: string
} & (
| {
type: 'publishable'
@@ -30,20 +31,20 @@ export async function createAPIKey(payload: APIKeyCreateVariables) {
reveal: false,
},
},
body:
payload.type === 'secret'
body: {
...(payload.type === 'secret'
? {
type: payload.type,
description: payload.description || null,
// secret_jwt_template: payload?.secret_jwt_template || null,
secret_jwt_template: {
role: 'service_role', // @mildtomato (Jonny) this should be default in API for type secret
},
}
: {
type: payload.type,
description: payload.description || null,
},
: name),
type: payload.type,
name: payload.name,
description: payload.description || null,
},
})
if (error) handleError(error)
@@ -1,49 +1,25 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { LOCAL_STORAGE_KEYS, useParams } from 'common'
import LegacyAPIKeys from 'components/interfaces/APIKeys/LegacyAPIKeys'
import { PublishableAPIKeys } from 'components/interfaces/APIKeys/PublishableAPIKeys'
import { SecretAPIKeys } from 'components/interfaces/APIKeys/SecretAPIKeys'
import DefaultLayout from 'components/layouts/DefaultLayout'
import ApiKeysLayout from 'components/layouts/project/[ref]/settings/APIKeysLayout'
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import type { NextPageWithLayout } from 'types'
import { Separator } from 'ui'
const ApiKeysSettings: NextPageWithLayout = () => {
const { ref: projectRef } = useParams()
const [apiKeysView] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.API_KEYS_VIEW(projectRef ?? ''),
undefined
)
export default function ApiKeysLegacyPage() {
const router = useRouter()
const { ref } = useParams()
// const isPermissionsLoaded = usePermissionsLoaded()
// TODO: check if these permissions cover third party auth as well
const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'api_keys')
// For backward compatibility, check if there's a view query parameter
const view = router.query.view as string
return (
<>
{apiKeysView !== 'legacy-keys' ? (
<>
<PublishableAPIKeys />
<Separator />
<SecretAPIKeys />
</>
) : (
<LegacyAPIKeys />
)}
</>
)
// Get the view from local storage if no query parameter
const [storedView] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.API_KEYS_VIEW(ref ?? ''), 'new-keys')
// Determine which page to redirect to
const targetView = view || storedView || 'new-keys'
useEffect(() => {
// Redirect to the appropriate page
router.replace(`/project/${ref}/settings/api-keys/${targetView}`)
}, [])
return null
}
ApiKeysSettings.getLayout = (page) => (
<DefaultLayout>
<SettingsLayout>
<ApiKeysLayout>{page}</ApiKeysLayout>
</SettingsLayout>
</DefaultLayout>
)
export default ApiKeysSettings
@@ -0,0 +1,19 @@
import { LegacyAPIKeys } from 'components/interfaces/APIKeys/LegacyAPIKeys'
import ApiKeysLayout from 'components/layouts/APIKeys/APIKeysLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
import type { NextPageWithLayout } from 'types'
const ApiKeysLegacyPage: NextPageWithLayout = () => {
return <LegacyAPIKeys />
}
ApiKeysLegacyPage.getLayout = (page) => (
<DefaultLayout>
<SettingsLayout>
<ApiKeysLayout>{page}</ApiKeysLayout>
</SettingsLayout>
</DefaultLayout>
)
export default ApiKeysLegacyPage
@@ -0,0 +1,41 @@
import {
ApiKeysComingSoonBanner,
ApiKeysCreateCallout,
ApiKeysFeedbackBanner,
} from 'components/interfaces/APIKeys/ApiKeysIllustrations'
import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { PublishableAPIKeys } from 'components/interfaces/APIKeys/PublishableAPIKeys'
import { SecretAPIKeys } from 'components/interfaces/APIKeys/SecretAPIKeys'
import ApiKeysLayout from 'components/layouts/APIKeys/APIKeysLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
import { DisableInteraction } from 'components/ui/DisableInteraction'
import type { NextPageWithLayout } from 'types'
import { Separator } from 'ui'
const ApiKeysNewPage: NextPageWithLayout = () => {
const { isInRollout, shouldDisableUI, canInitApiKeys } = useApiKeysVisibility()
return (
<>
{!isInRollout && <ApiKeysComingSoonBanner />}
{canInitApiKeys && <ApiKeysCreateCallout />}
<ApiKeysFeedbackBanner />
<DisableInteraction disabled={shouldDisableUI} className="flex flex-col gap-8">
<PublishableAPIKeys />
<Separator />
<SecretAPIKeys />
</DisableInteraction>
</>
)
}
ApiKeysNewPage.getLayout = (page) => (
<DefaultLayout>
<SettingsLayout>
<ApiKeysLayout>{page}</ApiKeysLayout>
</SettingsLayout>
</DefaultLayout>
)
export default ApiKeysNewPage
@@ -1,3 +1,4 @@
import { ApiKeysMoved } from 'components/interfaces/Settings/API/ApiKeysMoved'
import ServiceList from 'components/interfaces/Settings/API/ServiceList'
import DefaultLayout from 'components/layouts/DefaultLayout'
import SettingsLayout from 'components/layouts/ProjectSettingsLayout/SettingsLayout'
@@ -52,6 +52,7 @@ export const LOCAL_STORAGE_KEYS = {
GITHUB_AUTHORIZATION_STATE: 'supabase-github-authorization-state',
// Notice banner keys
FLY_POSTGRES_DEPRECATION_WARNING: 'fly-postgres-deprecation-warning-dismissed',
API_KEYS_FEEDBACK_DISMISSED: (ref: string) => `supabase-api-keys-feedback-dismissed-${ref}`,
MIDDLEWARE_OUTAGE_BANNER: 'middleware-outage-banner-2025-05-16',
AUTH_USERS_COLUMNS_CONFIGURATION: (ref: string) => `supabase-auth-users-columns-${ref}`,