Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 100 additions & 47 deletions apps/studio/components/interfaces/Docs/LangSelector.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,49 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Key } from 'lucide-react'
import { useMemo } from 'react'

import { useParams } from 'common'
import type { showApiKey } from 'components/interfaces/Docs/Docs.types'
import { getAPIKeys, useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'ui'

const DEFAULT_KEY = { name: 'hide', key: 'SUPABASE_KEY' }

interface LangSelectorProps {
selectedLang: string
showApiKey: showApiKey
selectedApiKey: showApiKey
setSelectedLang: (selectedLang: string) => void
setShowApiKey: (showApiKey: showApiKey) => void
setSelectedApiKey: (showApiKey: showApiKey) => void
}

const LangSelector = ({
selectedLang,
showApiKey,
selectedApiKey,
setSelectedLang,
setShowApiKey,
setSelectedApiKey,
}: LangSelectorProps) => {
const { ref: projectRef } = useParams()
const canReadServiceKey = useCheckPermissions(
PermissionAction.READ,
'service_api_keys.service_role_key'
)

const { data: settings } = useProjectSettingsV2Query({ projectRef })
const { anonKey: anonApiKey, serviceKey: serviceApiKey } = getAPIKeys(settings)
const { data: apiKeys = [], isLoading: isLoadingAPIKeys } = useAPIKeysQuery({
projectRef,
reveal: false,
})

const legacyKeys = useMemo(() => apiKeys.filter(({ type }) => type === 'legacy'), [apiKeys])
const publishableKeys = useMemo(
() => apiKeys.filter(({ type }) => type === 'publishable'),
[apiKeys]
)
const secretKeys = useMemo(() => apiKeys.filter(({ type }) => type === 'secret'), [apiKeys])

return (
<div className="p-1 w-1/2 ml-auto">
Expand All @@ -62,48 +70,93 @@ const LangSelector = ({
>
Bash
</button>
{selectedLang == 'bash' && (
<div className="flex">
{selectedLang == 'bash' && !isLoadingAPIKeys && apiKeys && apiKeys.length > 0 && (
<div className="flex gap-x-1">
<div className="flex items-center gap-2 p-1 pl-2 text-xs text-foreground-lighter">
<Key size={12} strokeWidth={1.5} />
<span>Project API key :</span>
<span>Project API key:</span>
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="default">{showApiKey.name}</Button>
<Button type="outline">
{selectedApiKey.name === 'hide' ? 'Hide keys' : selectedApiKey.name}
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" side="bottom">
<>
<DropdownMenuItem key="hide" onClick={() => setShowApiKey(DEFAULT_KEY)}>
hide
</DropdownMenuItem>
{anonApiKey && (
<DropdownMenuItem
key="anon"
onClick={() =>
setShowApiKey({
key: anonApiKey.api_key ?? '-',
name: 'anon (public)',
})
}
>
<p>anon (public)</p>
</DropdownMenuItem>
<DropdownMenuRadioGroup value={selectedApiKey.key}>
<DropdownMenuRadioItem
key="hide"
value={DEFAULT_KEY.key}
onClick={() => setSelectedApiKey(DEFAULT_KEY)}
>
Hide keys
</DropdownMenuRadioItem>

{publishableKeys.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Publishable keys</DropdownMenuLabel>
{publishableKeys.map((key) => {
const value = key.api_key
return (
<DropdownMenuRadioItem
key={key.id}
value={value}
onClick={() =>
setSelectedApiKey({
name: `Publishable key: ${key.name}`,
key: value,
})
}
>
{key.name}
</DropdownMenuRadioItem>
)
})}
</>
)}
{canReadServiceKey && (
<DropdownMenuItem
key="service"
onClick={() =>
setShowApiKey({
key: serviceApiKey?.api_key ?? '-',
name: 'service_role (secret)',
})
}
>
<p>service_role (secret)</p>
</DropdownMenuItem>

{secretKeys.length > 0 && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Secret keys</DropdownMenuLabel>
{secretKeys.map((key) => {
const value = key.prefix + '...'
return (
<DropdownMenuRadioItem
key={key.id}
value={value}
onClick={() =>
setSelectedApiKey({ name: `Secret key: ${key.name}`, key: value })
}
>
{key.name}
</DropdownMenuRadioItem>
)
})}
</>
)}
</>

<DropdownMenuSeparator />

<DropdownMenuGroup>
<DropdownMenuLabel>JWT-based legacy keys</DropdownMenuLabel>
{legacyKeys.map((key) => {
const value = key.api_key
return (
<DropdownMenuRadioItem
key={key.id}
value={value}
onClick={() =>
setSelectedApiKey({ name: `Legacy key: ${key.name}`, key: value })
}
>
{key.name}
</DropdownMenuRadioItem>
)
})}
</DropdownMenuGroup>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import { useSelectedOrganization } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
import { prettifyJSON } from 'lib/helpers'
import { getRoleImpersonationJWT } from 'lib/role-impersonation'
import { useGetImpersonatedRoleState } from 'state/role-impersonation-state'
import {
RoleImpersonationStateContextProvider,
useGetImpersonatedRoleState,
} from 'state/role-impersonation-state'
import {
Badge,
Button,
Expand Down Expand Up @@ -416,7 +419,12 @@ export const EdgeFunctionTesterSheet = ({ visible, onClose }: EdgeFunctionTester

<SheetFooter className="px-5 py-3 border-t">
<div className="flex items-center gap-2">
<RoleImpersonationPopover portal={false} />
{/* [Alaister]: We're using a fresh context here as edge functions don't allow impersonating users. */}
<RoleImpersonationStateContextProvider
key={`role-impersonation-state-${projectRef}`}
>
<RoleImpersonationPopover portal={false} disallowAuthenticatedOption={true} />
</RoleImpersonationStateContextProvider>
<Button
type="primary"
htmlType="submit"
Expand Down
2 changes: 1 addition & 1 deletion apps/studio/components/interfaces/Home/Home.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export const EXAMPLE_PROJECTS = [
framework: 'nextjs',
title: 'Next.js Subscription and Auth',
description: 'The all-in-one starter kit for high-performance SaaS applications.',
url: 'https://github.com/vercel/nextjs-subscription-payments',
url: 'https://github.com/nextjs/saas-starter',
type: 'app',
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ export const BillingSettings = () => {
</>
)}

{isBillingCreditsEnabledOnProfileLevel && isNotOrgWithPartnerBilling && (
{isBillingCreditsEnabledOnProfileLevel && (
<>
<ScaffoldDivider />

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,7 @@ export const SubscriptionPlanUpdateDialog = ({
<PaymentMethodSelection
ref={paymentMethodSelection}
selectedPaymentMethod={selectedPaymentMethod}
onSelectPaymentMethod={() => {}}
onSelectPaymentMethod={(pm) => setSelectedPaymentMethod(pm)}
createPaymentMethodInline={
subscriptionPreview.pending_subscription_flow === true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@ export interface RoleImpersonationPopoverProps {
serviceRoleLabel?: string
variant?: 'regular' | 'connected-on-right' | 'connected-on-left' | 'connected-on-both'
align?: 'center' | 'start' | 'end'
disallowAuthenticatedOption?: boolean
}

const RoleImpersonationPopover = ({
portal = true,
serviceRoleLabel,
variant = 'regular',
align = 'end',
disallowAuthenticatedOption = false,
}: RoleImpersonationPopoverProps) => {
const state = useRoleImpersonationStateSnapshot()

Expand Down Expand Up @@ -64,7 +66,10 @@ const RoleImpersonationPopover = ({
side="bottom"
align={align}
>
<RoleImpersonationSelector serviceRoleLabel={serviceRoleLabel} />
<RoleImpersonationSelector
serviceRoleLabel={serviceRoleLabel}
disallowAuthenticatedOption={disallowAuthenticatedOption}
/>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import UserImpersonationSelector from './UserImpersonationSelector'
export interface RoleImpersonationSelectorProps {
serviceRoleLabel?: string
padded?: boolean
disallowAuthenticatedOption?: boolean
}

const RoleImpersonationSelector = ({
serviceRoleLabel,
padded = true,
disallowAuthenticatedOption = false,
}: RoleImpersonationSelectorProps) => {
const state = useRoleImpersonationStateSnapshot()

Expand Down Expand Up @@ -81,38 +83,47 @@ const RoleImpersonationSelector = ({
icon={<AnonIcon isSelected={selectedOption === 'anon'} />}
/>

<RoleImpersonationRadio
value="authenticated"
isSelected={
selectedOption === 'authenticated' &&
(isAuthenticatedOptionFullySelected || 'partially')
}
onSelectedChange={onSelectedChange}
icon={<AuthenticatedIcon isSelected={selectedOption === 'authenticated'} />}
/>
{!disallowAuthenticatedOption && (
<RoleImpersonationRadio
value="authenticated"
isSelected={
selectedOption === 'authenticated' &&
(isAuthenticatedOptionFullySelected || 'partially')
}
onSelectedChange={onSelectedChange}
icon={<AuthenticatedIcon isSelected={selectedOption === 'authenticated'} />}
/>
)}
</fieldset>
</form>

{selectedOption === 'service_role' && (
<p className="text-foreground-light text-sm">
The default Postgres/superuser role. This has admin privileges.
The default Postgres/superuser role.
{disallowAuthenticatedOption ? <br /> : ' '}
This has admin privileges.
<br />
It will bypass Row Level Security (RLS) policies.
</p>
)}

{selectedOption === 'anon' && (
<p className="text-foreground-light text-sm">
For "anonymous access". This is the role which the API (PostgREST) will use when a user
For "anonymous access".
{disallowAuthenticatedOption ? <br /> : ' '}
This is the role which the API (PostgREST)
<br />
is not logged in. It will respect Row Level Security (RLS) policies.
will use when a user is not logged in.
{disallowAuthenticatedOption ? <br /> : ' '}
It will respect Row Level Security (RLS) policies.
</p>
)}

{selectedOption === 'authenticated' && (
<p className="text-foreground-light text-sm">
For "authenticated access". This is the role which the API (PostgREST) will use when
<br /> a user is logged in. It will respect Row Level Security (RLS) policies.
For "authenticated access". This is the role which the API (PostgREST)
<br />
will use when a user is logged in. It will respect Row Level Security (RLS) policies.
</p>
)}
</div>
Expand Down
3 changes: 3 additions & 0 deletions apps/studio/data/config/project-settings-v2-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ export const useProjectSettingsV2Query = <TData = ProjectSettingsData>(
)
}

/**
* @deprecated Use api-keys-query instead!
*/
export const getAPIKeys = (settings?: ProjectSettings) => {
const anonKey = (settings?.service_api_keys ?? []).find((x) => x.tags === 'anon')
const serviceKey = (settings?.service_api_keys ?? []).find((x) => x.tags === 'service_role')
Expand Down
Loading
Loading