mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 18:11:51 +08:00
## Context PR here mainly breaks up the files under `ConnectSheet` to separate the functional logic so that we can write unit tests. No behavior changes intended beyond the bug fixes ## Changes involved - **Test organization:** moved all root-level `ConnectSheet` test files into `ConnectSheet/__tests__/` for consistency with other parts of the codebase that use this convention. - **Bug fix:** read replica label had a stray `}` / missing `)`, rendering as e.g. `Read Replica (us-east-1 - abc123})` instead of `Read Replica (us-east-1 - abc123)`. - **`ConnectSheet.tsx`:** extracted the "hydrate sheet state on open" `useEffect` logic (mode/field/URL param resolution from URL vs. localStorage) into a new `ConnectSheet.utils.ts`, with unit tests - **`useConnectServerEnv.ts`:** fixed two race conditions in the secret reveal/hide flow: - `toggle()` and `getValue()` could each fire a separate reveal request if triggered close together — now deduped to share one in-flight request. - `getValue()` could hide a secret that had just been explicitly revealed by a concurrent `toggle()`, due to reading a stale closure value — now reads the live state via `useLatest`. - Also stopped swallowing the original error on reveal failure (now attached via `cause`). - Added tests for the above, plus the 10s auto-hide timer (previously untested). - **`ConnectStepsSection.tsx`:** extracted `resolveContentPath` and the three inline "show notice" booleans (IPv4 addon, session pooler, self-hosted MCP) into `ConnectStepsSection.utils.ts`, matching the existing pattern for the Data API notice. Added unit tests for all of them. ## To test - [ ] Just a basic smoke test of the Connect sheet should do <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Improved connect setup hydration so saved preferences and URL values are applied more consistently when opening the sheet, including automatic URL backfilling where needed. * Refreshed connection guidance notices (IPv4 add-on, session pooler, and self-hosted MCP) with more consistent logic. * **Bug Fixes** * Fixed secret reveal behavior to keep concurrent reveal actions in sync, handle failures more safely, and ensure auto-hide works reliably. * Corrected the read-replica option label formatting. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
168 lines
5.5 KiB
TypeScript
168 lines
5.5 KiB
TypeScript
import { PermissionAction } from '@supabase/shared-types/out/constants'
|
|
import { useParams } from 'common'
|
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
|
|
import { useRevealedSecret } from '@/components/interfaces/APIKeys/useRevealedSecret'
|
|
import { useAPIKeys } from '@/data/api-keys/api-keys-query'
|
|
import { useProjectApiUrl } from '@/data/config/project-endpoint-query'
|
|
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
|
|
import { useLatest } from '@/hooks/misc/useLatest'
|
|
|
|
const AUTO_HIDE_MS = 10_000
|
|
const SECRET_MASK = '••••••••••••••••••••'
|
|
|
|
export const SERVER_ENV_VARS = {
|
|
url: 'SUPABASE_URL',
|
|
publishableKey: 'SUPABASE_PUBLISHABLE_KEY',
|
|
secretKey: 'SUPABASE_SECRET_KEY',
|
|
jwksUrl: 'SUPABASE_JWKS_URL',
|
|
} as const
|
|
|
|
const JWKS_DISCOVERY_PATH = '/auth/v1/.well-known/jwks.json'
|
|
|
|
export interface ConnectServerEnvSecret {
|
|
exists: boolean
|
|
canReveal: boolean
|
|
isRevealed: boolean
|
|
isRevealing: boolean
|
|
maskedValue: string
|
|
displayValue: string
|
|
toggle: () => Promise<void>
|
|
getValue: () => Promise<string>
|
|
}
|
|
|
|
export interface UseConnectServerEnvResult {
|
|
isLoading: boolean
|
|
canReadAPIKeys: boolean
|
|
apiUrl: string
|
|
publishableKey: string
|
|
|
|
// Public JWKS discovery endpoint, used to verify user JWTs.
|
|
jwksUrl: string
|
|
secret: ConnectServerEnvSecret
|
|
|
|
// Builds the full .env text, revealing the secret key on demand.
|
|
buildEnv: () => Promise<string>
|
|
}
|
|
|
|
export function useConnectServerEnv(): UseConnectServerEnvResult {
|
|
const { ref: projectRef } = useParams()
|
|
|
|
const { can: canReadAPIKeys, isLoading: isLoadingPermission } = useAsyncCheckPermissions(
|
|
PermissionAction.READ,
|
|
'service_api_keys'
|
|
)
|
|
|
|
const [isRevealed, setIsRevealed] = useState(false)
|
|
// So async callbacks below can check the current reveal state after an
|
|
// await, instead of the value closed over when they started.
|
|
const isRevealedRef = useLatest(isRevealed)
|
|
|
|
const { data: apiUrl, isPending: isLoadingUrl } = useProjectApiUrl({ projectRef })
|
|
const resolvedUrl = apiUrl || 'your-project-url'
|
|
const jwksUrl = apiUrl
|
|
? new URL(JWKS_DISCOVERY_PATH, apiUrl).href
|
|
: `your-project-url${JWKS_DISCOVERY_PATH}`
|
|
|
|
const { data: keys, isLoading: isLoadingKeys } = useAPIKeys(
|
|
{ projectRef },
|
|
{ enabled: canReadAPIKeys }
|
|
)
|
|
const publishableKey = keys?.publishableKey?.api_key ?? keys?.anonKey?.api_key ?? ''
|
|
const secretKey = keys?.secretKey
|
|
const maskedValue = secretKey?.api_key
|
|
? `${secretKey.api_key.slice(0, 15)}${SECRET_MASK}`
|
|
: 'your-secret-key'
|
|
|
|
const {
|
|
data: revealedSecret,
|
|
isLoading: isRevealing,
|
|
reveal,
|
|
clear,
|
|
} = useRevealedSecret({ projectRef, id: secretKey?.id })
|
|
|
|
// toggle() and getSecretValue() can both decide to reveal before either
|
|
// resolves (e.g. clicking "Reveal" and "Copy" in quick succession); share
|
|
// one in-flight request rather than firing two and racing to set state.
|
|
const revealPromiseRef = useRef<ReturnType<typeof reveal> | null>(null)
|
|
const revealOnce = useCallback(() => {
|
|
if (!revealPromiseRef.current) {
|
|
revealPromiseRef.current = reveal().finally(() => {
|
|
revealPromiseRef.current = null
|
|
})
|
|
}
|
|
return revealPromiseRef.current
|
|
}, [reveal])
|
|
|
|
// clear() invalidates the in-flight reveal request (by request id) but
|
|
// doesn't know about revealPromiseRef, so a hide immediately followed by
|
|
// another reveal would otherwise reuse that now-invalidated promise
|
|
// instead of firing a fresh request.
|
|
const clearReveal = useCallback(() => {
|
|
revealPromiseRef.current = null
|
|
clear()
|
|
}, [clear])
|
|
|
|
const toggle = useCallback(async () => {
|
|
if (!secretKey || !canReadAPIKeys) return
|
|
if (isRevealed) {
|
|
setIsRevealed(false)
|
|
clearReveal()
|
|
} else {
|
|
setIsRevealed(true)
|
|
try {
|
|
await revealOnce()
|
|
} catch (error) {
|
|
setIsRevealed(false)
|
|
throw new Error('Failed to reveal secret API key', { cause: error })
|
|
}
|
|
}
|
|
}, [secretKey, canReadAPIKeys, isRevealed, clearReveal, revealOnce])
|
|
|
|
const getSecretValue = useCallback(async () => {
|
|
if (!secretKey || !canReadAPIKeys) return 'your-secret-key'
|
|
if (revealedSecret) return revealedSecret
|
|
const value = await revealOnce()
|
|
if (!isRevealedRef.current) clearReveal()
|
|
return value ?? 'your-secret-key'
|
|
}, [secretKey, canReadAPIKeys, revealedSecret, revealOnce, clearReveal])
|
|
|
|
const buildEnv = useCallback(async () => {
|
|
const secretValue = await getSecretValue()
|
|
return [
|
|
`${SERVER_ENV_VARS.url}=${resolvedUrl}`,
|
|
`${SERVER_ENV_VARS.publishableKey}=${publishableKey || 'your-publishable-key'}`,
|
|
`${SERVER_ENV_VARS.secretKey}=${secretValue}`,
|
|
`${SERVER_ENV_VARS.jwksUrl}=${jwksUrl}`,
|
|
].join('\n')
|
|
}, [resolvedUrl, publishableKey, jwksUrl, getSecretValue])
|
|
|
|
useEffect(() => {
|
|
if (!isRevealed || !revealedSecret) return
|
|
const timer = setTimeout(() => {
|
|
setIsRevealed(false)
|
|
clearReveal()
|
|
}, AUTO_HIDE_MS)
|
|
return () => clearTimeout(timer)
|
|
}, [isRevealed, revealedSecret, clearReveal])
|
|
|
|
return {
|
|
isLoading: isLoadingUrl || isLoadingKeys || isLoadingPermission,
|
|
canReadAPIKeys,
|
|
apiUrl: resolvedUrl,
|
|
publishableKey: publishableKey || 'your-publishable-key',
|
|
jwksUrl,
|
|
secret: {
|
|
exists: !!secretKey,
|
|
canReveal: canReadAPIKeys,
|
|
isRevealed,
|
|
isRevealing,
|
|
maskedValue,
|
|
displayValue: isRevealed && revealedSecret ? revealedSecret : maskedValue,
|
|
toggle,
|
|
getValue: getSecretValue,
|
|
},
|
|
buildEnv,
|
|
}
|
|
}
|