mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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:
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>
|
||||
)
|
||||
}
|
||||
+25
-10
@@ -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
|
||||
+31
-8
@@ -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
|
||||
@@ -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}`,
|
||||
|
||||
|
||||
Reference in new issue
Block a user