DisplayApiSettings.tsx 8.9 KB

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