@@ -134,6 +118,14 @@ const ApiKeyInput = () => {
)
}
+ if (isApiKeysLoading || isPermissionsLoading) {
+ return (
+
diff --git a/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx b/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx
index 9bd053a40ce..9497705b5e3 100644
--- a/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx
+++ b/apps/studio/components/interfaces/APIKeys/SecretAPIKeys.tsx
@@ -1,4 +1,3 @@
-import { PermissionAction } from '@supabase/shared-types/out/constants'
import dayjs from 'dayjs'
import { useMemo, useRef } from 'react'
@@ -7,7 +6,6 @@ import AlertError from 'components/ui/AlertError'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { APIKeysData, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import useLogsQuery from 'hooks/analytics/useLogsQuery'
-import { useAsyncCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Card, EyeOffIcon } from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader'
import {
@@ -19,6 +17,7 @@ import {
} from 'ui/src/components/shadcn/ui/table'
import { APIKeyRow } from './APIKeyRow'
import CreateSecretAPIKeyDialog from './CreateSecretAPIKeyDialog'
+import { useApiKeysVisibility } from './hooks/useApiKeysVisibility'
interface LastSeenData {
[hash: string]: { timestamp: string }
@@ -51,17 +50,14 @@ function useLastSeen(projectRef: string): LastSeenData {
export const SecretAPIKeys = () => {
const { ref: projectRef } = useParams()
+
+ const { canReadAPIKeys, isLoading: isLoadingPermissions } = useApiKeysVisibility()
const {
data: apiKeysData,
error,
isLoading: isLoadingApiKeys,
isError: isErrorApiKeys,
- } = useAPIKeysQuery({ projectRef, reveal: false })
-
- const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
- PermissionAction.TENANT_SQL_ADMIN_WRITE,
- '*'
- )
+ } = useAPIKeysQuery({ projectRef, reveal: false }, { enabled: canReadAPIKeys })
const lastSeen = useLastSeen(projectRef!)
@@ -83,9 +79,7 @@ export const SecretAPIKeys = () => {
actions={
}
/>
- {isLoadingApiKeys || isLoadingPermissions ? (
-
- ) : !canReadAPIKeys ? (
+ {!canReadAPIKeys && !isLoadingPermissions ? (
@@ -97,6 +91,8 @@ export const SecretAPIKeys = () => {
+ ) : isLoadingApiKeys || isLoadingPermissions ? (
+
) : isErrorApiKeys ? (
) : empty ? (
diff --git a/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts b/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts
index 7cedd8a9f1b..accb9ac4a09 100644
--- a/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts
+++ b/apps/studio/components/interfaces/APIKeys/hooks/useApiKeysVisibility.ts
@@ -19,12 +19,18 @@ interface ApiKeysVisibilityState {
*/
export function useApiKeysVisibility(): ApiKeysVisibilityState {
const { ref: projectRef } = useParams()
- const { can: canReadAPIKeys } = useAsyncCheckPermissions(PermissionAction.READ, 'api_keys')
+ const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckPermissions(
+ PermissionAction.SECRETS_READ,
+ '*'
+ )
- const { data: apiKeysData, isLoading } = useAPIKeysQuery({
- projectRef,
- reveal: false,
- })
+ const { data: apiKeysData, isLoading: isLoadingApiKeys } = useAPIKeysQuery(
+ {
+ projectRef,
+ reveal: false,
+ },
+ { enabled: canReadAPIKeys }
+ )
const publishableApiKeys = useMemo(
() => apiKeysData?.filter(({ type }) => type === 'publishable') ?? [],
@@ -36,14 +42,14 @@ export function useApiKeysVisibility(): ApiKeysVisibilityState {
const hasApiKeys = publishableApiKeys.length > 0
// Can initialize API keys when in rollout, has permissions, not loading, and no API keys yet
- const canInitApiKeys = canReadAPIKeys && !isLoading && !hasApiKeys
+ const canInitApiKeys = canReadAPIKeys && !isLoadingApiKeys && !hasApiKeys
// Disable UI for publishable keys and secrets keys if flag is not enabled OR no API keys created yet
const shouldDisableUI = !hasApiKeys
return {
hasApiKeys,
- isLoading,
+ isLoading: isLoadingPermissions || (canReadAPIKeys && isLoadingApiKeys),
canReadAPIKeys,
canInitApiKeys,
shouldDisableUI,
diff --git a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx
index 10c0e5df990..07ecb3652d1 100644
--- a/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx
+++ b/apps/studio/components/interfaces/App/CommandMenu/ApiKeys.tsx
@@ -1,6 +1,7 @@
import { Key } from 'lucide-react'
import { useMemo } from 'react'
+import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Badge, copyToClipboard } from 'ui'
@@ -24,88 +25,92 @@ export function useApiKeysCommands() {
const { data: project } = useSelectedProjectQuery()
const ref = project?.ref || '_'
- const { data: apiKeys } = useAPIKeysQuery({ projectRef: project?.ref, reveal: true })
- const { anonKey, serviceKey, publishableKey, allSecretKeys } = getKeys(apiKeys)
+ const { canReadAPIKeys } = useApiKeysVisibility()
+ const { data: apiKeys } = useAPIKeysQuery(
+ { projectRef: project?.ref, reveal: true },
+ { enabled: canReadAPIKeys }
+ )
+ const commands = useMemo(() => {
+ const { anonKey, serviceKey, publishableKey, allSecretKeys } = canReadAPIKeys
+ ? getKeys(apiKeys)
+ : {}
- const commands = useMemo(
- () =>
- [
- project &&
- anonKey && {
- id: 'anon-key',
- name: `Copy anonymous API key`,
- action: () => {
- copyToClipboard(anonKey.api_key ?? '')
- setIsOpen(false)
- },
- badge: () => (
-
- Project: {project?.name}
- Public
- {anonKey.type}
-
- ),
- icon: () =>
,
+ return [
+ project &&
+ anonKey && {
+ id: 'anon-key',
+ name: `Copy anonymous API key`,
+ action: () => {
+ copyToClipboard(anonKey.api_key ?? '')
+ setIsOpen(false)
},
- project &&
- serviceKey && {
- id: 'service-key',
- name: `Copy service API key`,
- action: () => {
- copyToClipboard(serviceKey.api_key ?? '')
- setIsOpen(false)
- },
- badge: () => (
-
- Project: {project?.name}
- Secret
- {serviceKey.type}
-
- ),
- icon: () =>
,
- },
- project &&
- publishableKey && {
- id: 'publishable-key',
- name: `Copy publishable key`,
- action: () => {
- copyToClipboard(publishableKey.api_key ?? '')
- setIsOpen(false)
- },
- badge: () => (
-
- Project: {project?.name}
- {publishableKey.type}
-
- ),
- icon: () =>
,
- },
- ...(project && allSecretKeys
- ? allSecretKeys.map((key) => ({
- id: key.id,
- name: `Copy secret key (${key.name})`,
- action: () => {
- copyToClipboard(key.api_key ?? '')
- setIsOpen(false)
- },
- badge: () => (
-
- Project: {project?.name}
- {key.type}
-
- ),
- icon: () =>
,
- }))
- : []),
- !(anonKey || serviceKey) && {
- id: 'api-keys-project-settings',
- name: 'See API keys in Project Settings',
- route: `/project/${ref}/settings/api`,
+ badge: () => (
+
+ Project: {project?.name}
+ Public
+ {anonKey.type}
+
+ ),
icon: () =>
,
},
- ].filter(Boolean) as ICommand[],
- [anonKey, serviceKey, project, setIsOpen]
- )
+ project &&
+ serviceKey && {
+ id: 'service-key',
+ name: `Copy service API key`,
+ action: () => {
+ copyToClipboard(serviceKey.api_key ?? '')
+ setIsOpen(false)
+ },
+ badge: () => (
+
+ Project: {project?.name}
+ Secret
+ {serviceKey.type}
+
+ ),
+ icon: () =>
,
+ },
+ project &&
+ publishableKey && {
+ id: 'publishable-key',
+ name: `Copy publishable key`,
+ action: () => {
+ copyToClipboard(publishableKey.api_key ?? '')
+ setIsOpen(false)
+ },
+ badge: () => (
+
+ Project: {project?.name}
+ {publishableKey.type}
+
+ ),
+ icon: () =>
,
+ },
+ ...(project && allSecretKeys
+ ? allSecretKeys.map((key) => ({
+ id: key.id,
+ name: `Copy secret key (${key.name})`,
+ action: () => {
+ copyToClipboard(key.api_key ?? '')
+ setIsOpen(false)
+ },
+ badge: () => (
+
+ Project: {project?.name}
+ {key.type}
+
+ ),
+ icon: () =>
,
+ }))
+ : []),
+ !(anonKey || serviceKey) && {
+ id: 'api-keys-project-settings',
+ name: 'See API keys in Project Settings',
+ route: `/project/${ref}/settings/api-keys`,
+ icon: () =>
,
+ },
+ ].filter(Boolean) as ICommand[]
+ }, [apiKeys, canReadAPIKeys, project, ref, setIsOpen])
useRegisterPage(
API_KEYS_PAGE_NAME,
@@ -119,7 +124,7 @@ export function useApiKeysCommands() {
},
],
},
- { deps: [commands], enabled: !!project }
+ { deps: [commands], enabled: !!project && commands.length > 0 }
)
useRegisterCommands(
@@ -133,7 +138,7 @@ export function useApiKeysCommands() {
},
],
{
- enabled: !!project,
+ enabled: !!project && commands.length > 0,
orderSection: orderCommandSectionsByPriority,
sectionMeta: { priority: 3 },
}
diff --git a/apps/studio/components/interfaces/Connect/Connect.tsx b/apps/studio/components/interfaces/Connect/Connect.tsx
index 792661303b9..86c648b9746 100644
--- a/apps/studio/components/interfaces/Connect/Connect.tsx
+++ b/apps/studio/components/interfaces/Connect/Connect.tsx
@@ -217,7 +217,7 @@ export const Connect = () => {
return []
}
- const { data: apiKeys } = useAPIKeysQuery({ projectRef })
+ const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
const { anonKey, publishableKey } = canReadAPIKeys
? getKeys(apiKeys)
: { anonKey: null, publishableKey: null }
diff --git a/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanel.tsx b/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanel.tsx
index 1cb9cd8f7c0..c37832da73a 100644
--- a/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanel.tsx
+++ b/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanel.tsx
@@ -44,6 +44,7 @@ import { AnalyticsBucketFields, BigQueryFields } from './DestinationPanelFields'
import { DestinationTypeSelection } from './DestinationTypeSelection'
import { NoDestinationsAvailable } from './NoDestinationsAvailable'
import { PublicationSelection } from './PublicationSelection'
+import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
const formId = 'destination-editor'
@@ -126,7 +127,11 @@ export const DestinationPanel = ({
pipelineId: existingDestination?.pipelineId,
})
- const { data: apiKeys } = useAPIKeysQuery({ projectRef, reveal: true })
+ const { canReadAPIKeys } = useApiKeysVisibility()
+ const { data: apiKeys } = useAPIKeysQuery(
+ { projectRef, reveal: true },
+ { enabled: canReadAPIKeys }
+ )
const { serviceKey } = getKeys(apiKeys)
const catalogToken = serviceKey?.api_key ?? ''
diff --git a/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanelFields.tsx b/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanelFields.tsx
index 4bba98af5c7..67810925baa 100644
--- a/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanelFields.tsx
+++ b/apps/studio/components/interfaces/Database/ETL/DestinationPanel/DestinationPanelFields.tsx
@@ -3,6 +3,7 @@ import { useMemo, useState } from 'react'
import type { UseFormReturn } from 'react-hook-form'
import { useParams } from 'common'
+import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { getCatalogURI } from 'components/interfaces/Storage/StorageSettings/StorageSettings.utils'
import { InlineLink } from 'components/ui/InlineLink'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
@@ -125,7 +126,11 @@ export const AnalyticsBucketFields = ({
const { ref: projectRef } = useParams()
const { data: project } = useSelectedProjectQuery()
- const { data: apiKeys } = useAPIKeysQuery({ projectRef, reveal: true })
+ const { canReadAPIKeys } = useApiKeysVisibility()
+ const { data: apiKeys } = useAPIKeysQuery(
+ { projectRef, reveal: true },
+ { enabled: canReadAPIKeys }
+ )
const { serviceKey } = getKeys(apiKeys)
const serviceApiKey = serviceKey?.api_key ?? ''
diff --git a/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx b/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx
index eedcb49b1cc..b72c5594ea8 100644
--- a/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx
+++ b/apps/studio/components/interfaces/Database/Hooks/FormContents.tsx
@@ -3,6 +3,7 @@ import Image from 'next/legacy/image'
import { MutableRefObject, useEffect } from 'react'
import { useParams } from 'common'
+import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useEdgeFunctionsQuery } from 'data/edge-functions/edge-functions-query'
@@ -50,7 +51,11 @@ export const FormContents = ({
const restUrl = project?.restUrl
const restUrlTld = restUrl ? new URL(restUrl).hostname.split('.').pop() : 'co'
- const { data: keys = [] } = useAPIKeysQuery({ projectRef: ref, reveal: true })
+ const { canReadAPIKeys } = useApiKeysVisibility()
+ const { data: keys = [] } = useAPIKeysQuery(
+ { projectRef: ref, reveal: true },
+ { enabled: canReadAPIKeys }
+ )
const { data: functions = [], isSuccess: isSuccessEdgeFunctions } = useEdgeFunctionsQuery({
projectRef: ref,
})
diff --git a/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx b/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx
index fbf3f3e3dd1..d15c54b571e 100644
--- a/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx
+++ b/apps/studio/components/interfaces/Database/Hooks/HTTPRequestFields.tsx
@@ -2,6 +2,7 @@ import { ChevronDown, Plus, X } from 'lucide-react'
import Link from 'next/link'
import { useParams } from 'common'
+import { useApiKeysVisibility } from 'components/interfaces/APIKeys/hooks/useApiKeysVisibility'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
@@ -51,7 +52,11 @@ const HTTPRequestFields = ({
const { data: selectedProject } = useSelectedProjectQuery()
const { data: functions } = useEdgeFunctionsQuery({ projectRef: ref })
- const { data: apiKeys } = useAPIKeysQuery({ projectRef: ref, reveal: true })
+ const { canReadAPIKeys } = useApiKeysVisibility()
+ const { data: apiKeys } = useAPIKeysQuery(
+ { projectRef: ref, reveal: true },
+ { enabled: canReadAPIKeys }
+ )
const edgeFunctions = functions ?? []
const { serviceKey, secretKey } = getKeys(apiKeys)
diff --git a/apps/studio/components/interfaces/Docs/Authentication.tsx b/apps/studio/components/interfaces/Docs/Authentication.tsx
index 3662e4572f5..43ec3fefbab 100644
--- a/apps/studio/components/interfaces/Docs/Authentication.tsx
+++ b/apps/studio/components/interfaces/Docs/Authentication.tsx
@@ -3,6 +3,7 @@ import Link from 'next/link'
import { useParams } from 'common'
import { getKeys, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
+import { useApiKeysVisibility } from '../APIKeys/hooks/useApiKeysVisibility'
import CodeSnippet from './CodeSnippet'
import Snippets from './Snippets'
@@ -13,7 +14,8 @@ interface AuthenticationProps {
const Authentication = ({ selectedLang, showApiKey }: AuthenticationProps) => {
const { ref: projectRef } = useParams()
- const { data: apiKeys } = useAPIKeysQuery({ projectRef })
+ const { canReadAPIKeys } = useApiKeysVisibility()
+ const { data: apiKeys } = useAPIKeysQuery({ projectRef }, { enabled: canReadAPIKeys })
const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { anonKey, serviceKey } = getKeys(apiKeys)
diff --git a/apps/studio/components/interfaces/Docs/LangSelector.tsx b/apps/studio/components/interfaces/Docs/LangSelector.tsx
index 5b8c7655910..6a053ecfab5 100644
--- a/apps/studio/components/interfaces/Docs/LangSelector.tsx
+++ b/apps/studio/components/interfaces/Docs/LangSelector.tsx
@@ -15,6 +15,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ui'
+import { useApiKeysVisibility } from '../APIKeys/hooks/useApiKeysVisibility'
const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' }
@@ -33,10 +34,14 @@ export const LangSelector = ({
}: LangSelectorProps) => {
const { ref: projectRef } = useParams()
- const { data: apiKeys = [], isLoading: isLoadingAPIKeys } = useAPIKeysQuery({
- projectRef,
- reveal: false,
- })
+ const { canReadAPIKeys } = useApiKeysVisibility()
+ const { data: apiKeys = [], isLoading: isLoadingAPIKeys } = useAPIKeysQuery(
+ {
+ projectRef,
+ reveal: false,
+ },
+ { enabled: canReadAPIKeys }
+ )
const legacyKeys = useMemo(() => apiKeys.filter(({ type }) => type === 'legacy'), [apiKeys])
const publishableKeys = useMemo(
@@ -70,97 +75,101 @@ export const LangSelector = ({
>
Bash
- {selectedLang == 'bash' && !isLoadingAPIKeys && apiKeys && apiKeys.length > 0 && (
-
-
-
- Project API key:
-
-
-
-
-
-
-
- setSelectedApiKey(DEFAULT_KEY)}
- >
- Hide keys
-
+ {selectedLang == 'bash' &&
+ canReadAPIKeys &&
+ !isLoadingAPIKeys &&
+ apiKeys &&
+ apiKeys.length > 0 && (
+
+
+
+ Project API key:
+
+
+
+
+
+
+
+ setSelectedApiKey(DEFAULT_KEY)}
+ >
+ Hide keys
+
- {publishableKeys.length > 0 && (
- <>
-
- Publishable keys
- {publishableKeys.map((key) => {
+ {publishableKeys.length > 0 && (
+ <>
+
+ Publishable keys
+ {publishableKeys.map((key) => {
+ const value = key.api_key
+ return (
+
+ setSelectedApiKey({
+ name: `Publishable key: ${key.name}`,
+ key: value,
+ })
+ }
+ >
+ {key.name}
+
+ )
+ })}
+ >
+ )}
+
+ {secretKeys.length > 0 && (
+ <>
+
+ Secret keys
+ {secretKeys.map((key) => {
+ const value = key.prefix + '...'
+ return (
+
+ setSelectedApiKey({ name: `Secret key: ${key.name}`, key: value })
+ }
+ >
+ {key.name}
+
+ )
+ })}
+ >
+ )}
+
+
+
+
+ JWT-based legacy keys
+ {legacyKeys.map((key) => {
const value = key.api_key
return (
- setSelectedApiKey({
- name: `Publishable key: ${key.name}`,
- key: value,
- })
+ setSelectedApiKey({ name: `Legacy key: ${key.name}`, key: value })
}
>
{key.name}
)
})}
- >
- )}
-
- {secretKeys.length > 0 && (
- <>
-
- Secret keys
- {secretKeys.map((key) => {
- const value = key.prefix + '...'
- return (
-
- setSelectedApiKey({ name: `Secret key: ${key.name}`, key: value })
- }
- >
- {key.name}
-
- )
- })}
- >
- )}
-
-
-
-
- JWT-based legacy keys
- {legacyKeys.map((key) => {
- const value = key.api_key
- return (
-
- setSelectedApiKey({ name: `Legacy key: ${key.name}`, key: value })
- }
- >
- {key.name}
-
- )
- })}
-
-
-
-
-
- )}
+
+
+
+
+
+ )}