PublishableAPIKeys.tsx146 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { useParams } from 'common'
3import { parseAsString, useQueryState } from 'nuqs'
4import { useEffect, useMemo } from 'react'
5import { toast } from 'sonner'
6import {
7 Card,
8 Table,
9 TableBody,
10 TableCell,
11 TableFooter,
12 TableHead,
13 TableHeader,
14 TableRow,
15} from 'ui'
16import { Admonition, GenericSkeletonLoader } from 'ui-patterns'
17
18import { APIKeyRow } from './APIKeyRow'
19import { CreatePublishableAPIKeyDialog } from './CreatePublishableAPIKeyDialog'
20import { AlertError } from '@/components/ui/AlertError'
21import { FormHeader } from '@/components/ui/Forms/FormHeader'
22import { NoPermission } from '@/components/ui/NoPermission'
23import { useAPIKeyDeleteMutation } from '@/data/api-keys/api-key-delete-mutation'
24import { APIKeysData, useAPIKeysQuery } from '@/data/api-keys/api-keys-query'
25import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
26
27export const PublishableAPIKeys = () => {
28 const { ref: projectRef } = useParams()
29 const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
30 PermissionAction.SECRETS_READ,
31 '*'
32 )
33
34 const {
35 data: apiKeysData = [],
36 error,
37 isSuccess: isSuccessApiKeys,
38 isPending: isLoadingApiKeys,
39 isError: isErrorApiKeys,
40 } = useAPIKeysQuery({ projectRef, reveal: false }, { enabled: canReadAPIKeys })
41
42 const newApiKeys = useMemo(
43 () => apiKeysData.filter(({ type }) => type === 'publishable' || type === 'secret') ?? [],
44 [apiKeysData]
45 )
46 const hasApiKeys = newApiKeys.length > 0
47
48 const publishableApiKeys = useMemo(
49 () =>
50 apiKeysData?.filter(
51 (key): key is Extract<APIKeysData[number], { type: 'publishable' }> =>
52 key.type === 'publishable'
53 ) ?? [],
54 [apiKeysData]
55 )
56
57 const [deleteId, setDeleteId] = useQueryState('deletePublishableKey', parseAsString)
58 const apiKeyToDelete = publishableApiKeys?.find((key) => key.id === deleteId)
59
60 const {
61 mutate: deleteAPIKey,
62 isPending: isDeletingAPIKey,
63 isSuccess: isDeleteSuccess,
64 } = useAPIKeyDeleteMutation({
65 onSuccess: () => {
66 toast.success('Successfully deleted publishable key')
67 setDeleteId(null)
68 },
69 })
70
71 const onDeleteAPIKey = (apiKey: Extract<APIKeysData[number], { type: 'publishable' }>) => {
72 if (!projectRef) return console.error('Project ref is required')
73 if (!apiKey.id) return console.error('API key ID is required')
74 deleteAPIKey({ projectRef, id: apiKey.id })
75 }
76
77 useEffect(() => {
78 if (isSuccessApiKeys && !!deleteId && !apiKeyToDelete && !isDeleteSuccess) {
79 toast('Unable to find publishable key')
80 setDeleteId(null)
81 }
82 }, [apiKeyToDelete, deleteId, isDeleteSuccess, isSuccessApiKeys, setDeleteId])
83
84 return (
85 <div>
86 <FormHeader
87 title="Publishable key"
88 description="This key is safe to use in a browser if you have enabled Row Level Security (RLS) for your tables and configured policies."
89 actions={<CreatePublishableAPIKeyDialog />}
90 />
91
92 {!canReadAPIKeys && !isLoadingPermissions ? (
93 <NoPermission resourceText="view API keys" />
94 ) : isLoadingApiKeys || isLoadingPermissions ? (
95 <GenericSkeletonLoader />
96 ) : isErrorApiKeys ? (
97 <AlertError error={error} subject="Failed to load API keys" />
98 ) : (
99 <Card className="bg-surface-100">
100 <Table>
101 <TableHeader>
102 <TableRow className="bg-200">
103 <TableHead>Name</TableHead>
104 <TableHead>API Key</TableHead>
105 <TableHead />
106 </TableRow>
107 </TableHeader>
108
109 <TableBody>
110 {hasApiKeys && publishableApiKeys.length === 0 && (
111 <TableRow>
112 <TableCell colSpan={3} className="p-0">
113 <Admonition showIcon={false} type="default" className="border-0 rounded-none">
114 <p className="text-foreground-light">No publishable keys created yet</p>
115 </Admonition>
116 </TableCell>
117 </TableRow>
118 )}
119 {publishableApiKeys.map((apiKey) => (
120 <APIKeyRow
121 showLastSeen={false}
122 key={apiKey.id}
123 apiKey={apiKey}
124 isDeleting={apiKeyToDelete?.id === apiKey.id && isDeletingAPIKey}
125 isDeleteModalOpen={apiKeyToDelete?.id === apiKey.id}
126 onDelete={() => onDeleteAPIKey(apiKey)}
127 setKeyToDelete={setDeleteId}
128 />
129 ))}
130 </TableBody>
131
132 <TableFooter className="border-t">
133 <TableRow className="border-b-0">
134 <TableCell colSpan={3} className="py-2">
135 <p className="text-xs text-foreground-lighter font-normal">
136 Publishable keys can be safely shared publicly
137 </p>
138 </TableCell>
139 </TableRow>
140 </TableFooter>
141 </Table>
142 </Card>
143 )}
144 </div>
145 )
146}