DisplayApiSettings.tsx236 lines · main
1import { PermissionAction } from '@supabase/shared-types/out/constants'
2import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
3import { useFlag, useParams } from 'common'
4import { AlertCircle, Loader2 } from 'lucide-react'
5import Link from 'next/link'
6import { useMemo } from 'react'
7import { toast } from 'sonner'
8import { Input } from 'ui-patterns/DataInputs/Input'
9import { FormLayout } from 'ui-patterns/form/Layout/FormLayout'
10
11import { getLastUsedAPIKeys, useLastUsedAPIKeysLogQuery } from './DisplayApiSettings.utils'
12import Panel from '@/components/ui/Panel'
13import { useJwtSecretUpdatingStatusQuery } from '@/data/config/jwt-secret-updating-status-query'
14import { useProjectSettingsV2Query } from '@/data/config/project-settings-v2-query'
15import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
16
17export const DisplayApiSettings = ({
18 showTitle = true,
19 showNotice = true,
20 showLegacyText = true,
21}: {
22 showTitle?: boolean
23 showNotice?: boolean
24 showLegacyText?: boolean
25}) => {
26 const { ref: projectRef } = useParams()
27
28 const {
29 data: settings,
30 isError: isProjectSettingsError,
31 isPending: isProjectSettingsLoading,
32 } = useProjectSettingsV2Query({ projectRef })
33 const {
34 data,
35 isError: isJwtSecretUpdateStatusError,
36 isPending: isJwtSecretUpdateStatusLoading,
37 } = useJwtSecretUpdatingStatusQuery({ projectRef })
38 const jwtSecretUpdateStatus = data?.jwtSecretUpdateStatus
39
40 const { isLoading: isLoadingPermissions, can: canReadAPIKeys } = useAsyncCheckPermissions(
41 PermissionAction.READ,
42 'service_api_keys'
43 )
44
45 const isLoading = isProjectSettingsLoading || isLoadingPermissions
46
47 const isNotUpdatingJwtSecret =
48 jwtSecretUpdateStatus === undefined || jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updated
49
50 const apiKeys = useMemo(() => settings?.service_api_keys ?? [], [settings])
51 // api keys should not be empty. However it can be populated with a delay on project creation
52 const isApiKeysEmpty = apiKeys.length === 0
53
54 const showApiKeyLastUsed = useFlag('showApiKeysLastUsed')
55 const { isLoading: isLoadingLastUsed, logData: lastUsedLogData } = useLastUsedAPIKeysLogQuery({
56 projectRef: projectRef ?? '',
57 enabled: showApiKeyLastUsed,
58 })
59
60 const lastUsedAPIKeys = useMemo(() => {
61 if (
62 apiKeys.length < 1 ||
63 !lastUsedLogData ||
64 lastUsedLogData.length < 1 ||
65 !showApiKeyLastUsed
66 ) {
67 return {}
68 }
69
70 try {
71 return getLastUsedAPIKeys(apiKeys, lastUsedLogData)
72 } catch (e: any) {
73 toast.error('Failed to identify when the anon and service_role keys were last used')
74 console.error(e)
75 return {}
76 }
77 }, [lastUsedLogData, apiKeys, showApiKeyLastUsed])
78
79 return (
80 <Panel
81 noMargin
82 title={
83 showTitle && (
84 <div className="space-y-3">
85 <h5 className="text-base">Project API Keys</h5>
86 <p className="text-sm text-foreground-light">
87 Your API is secured behind an API gateway which requires an API Key for every request.
88 <br />
89 You can use the keys below in the Briven client libraries.
90 <br />
91 </p>
92 </div>
93 )
94 }
95 >
96 {isLoading ? (
97 <div className="flex items-center justify-center py-8 space-x-2">
98 <Loader2 className="animate-spin" size={16} strokeWidth={1.5} />
99 <p className="text-sm text-foreground-light">Retrieving API keys</p>
100 </div>
101 ) : !canReadAPIKeys ? (
102 <div className="flex items-center py-8 px-8 space-x-2">
103 <AlertCircle size={16} strokeWidth={1.5} />
104 <p className="text-sm text-foreground-light">
105 You don't have permission to view API keys. These keys restricted to users with higher
106 access levels.
107 </p>
108 </div>
109 ) : isProjectSettingsError || isJwtSecretUpdateStatusError ? (
110 <div className="flex items-center justify-center py-8 space-x-2">
111 <AlertCircle size={16} strokeWidth={1.5} />
112 <p className="text-sm text-foreground-light">
113 {isProjectSettingsError ? 'Failed to retrieve API keys' : 'Failed to update JWT secret'}
114 </p>
115 </div>
116 ) : isApiKeysEmpty || isProjectSettingsLoading || isJwtSecretUpdateStatusLoading ? (
117 <div className="flex items-center justify-center py-8 space-x-2">
118 <Loader2 className="animate-spin" size={16} strokeWidth={1.5} />
119 <p className="text-sm text-foreground-light">
120 {isProjectSettingsLoading || isApiKeysEmpty
121 ? 'Retrieving API keys'
122 : 'JWT secret is being updated'}
123 </p>
124 </div>
125 ) : (
126 apiKeys.map((x, i: number) => (
127 <Panel.Content
128 key={x.api_key}
129 className={
130 i >= 1 &&
131 'border-t border-panel-border-interior-light in-data-[theme*=dark]:border-panel-border-interior-dark'
132 }
133 >
134 <FormLayout
135 layout="horizontal"
136 label={
137 <div className="flex items-center space-x-1">
138 {x.tags?.split(',').map((x, i: number) => (
139 <code key={`${x}${i}`} className="text-code-inline">
140 {x}
141 </code>
142 ))}
143 {x.tags === 'service_role' && (
144 <>
145 <code className="text-code-inline bg-destructive! text-white! border-destructive!">
146 secret
147 </code>
148 </>
149 )}
150 {x.tags === 'anon' && <code className="text-code-inline">public</code>}
151 </div>
152 }
153 description={
154 x.tags === 'service_role' ? (
155 <>
156 This key has the ability to bypass Row Level Security. Never share it publicly.
157 If leaked, generate a new JWT secret immediately.{' '}
158 {showLegacyText && (
159 <span>
160 Prefer using{' '}
161 <Link
162 href={`/project/${projectRef}/settings/api-keys/new`}
163 className="text-link underline"
164 >
165 Secret API keys
166 </Link>{' '}
167 instead.
168 </span>
169 )}
170 </>
171 ) : (
172 <>
173 This key is safe to use in a browser if you have enabled Row Level Security for
174 your tables and configured policies.{' '}
175 {showLegacyText && (
176 <span>
177 Prefer using{' '}
178 <Link
179 href={`/project/${projectRef}/settings/api-keys/new`}
180 className="text-link underline"
181 >
182 Publishable API keys
183 </Link>{' '}
184 instead.
185 </span>
186 )}
187 </>
188 )
189 }
190 >
191 <Input
192 readOnly
193 className="font-mono"
194 copy={canReadAPIKeys && isNotUpdatingJwtSecret}
195 reveal={x.tags !== 'anon' && canReadAPIKeys && isNotUpdatingJwtSecret}
196 value={
197 !canReadAPIKeys
198 ? 'You need additional permissions to view API keys'
199 : jwtSecretUpdateStatus === JwtSecretUpdateStatus.Failed
200 ? 'JWT secret update failed, new API key may have issues'
201 : jwtSecretUpdateStatus === JwtSecretUpdateStatus.Updating
202 ? 'Updating JWT secret...'
203 : (x?.api_key ?? 'You need additional permissions to view API keys')
204 }
205 onChange={() => {}}
206 />
207 </FormLayout>
208
209 {showApiKeyLastUsed && (
210 <div
211 className="pt-2 text-foreground-lighter w-full text-sm data-[invisible=true]:invisible"
212 data-invisible={isLoadingLastUsed}
213 >
214 {lastUsedAPIKeys[x.api_key]
215 ? `Last request was ${lastUsedAPIKeys[x.api_key]} ago.`
216 : 'No requests in the past 24 hours.'}
217 </div>
218 )}
219 </Panel.Content>
220 ))
221 )}
222 {showNotice ? (
223 <Panel.Notice
224 className="border-t"
225 title="API keys have moved"
226 badgeLabel="Changelog"
227 description={`
228 \`anon\` and \`service_role\` API keys can now be replaced with \`publishable\` and \`secret\` API keys.
229 `}
230 href="https://github.com/orgs/briven/discussions/29260"
231 buttonText="Read the announcement"
232 />
233 ) : null}
234 </Panel>
235 )
236}