Merge remote-tracking branch 'origin/master' into kemal/design-249-time-series-queries-exploration

This commit is contained in:
kemal committed 2025-08-11 09:50:39 +01:00
commit b9d01fa360
57 files changed
+974 -682

No files matched your search

+1 -1
View File
@@ -26,7 +26,7 @@ This is made possible if the providers are using JWTs signed with asymmetric key
There are some limitations you should be aware of when using third-party authentication providers with Supabase.
1. The third-party provider must use asymmetrically signed JWTs (exposed as an OIDC Issuer Discovery URL by the third-party authentication provider). Using symmetrically signed JWTs is not possible at this time.
1. The third-party provider must use asymmetrically signed JWTs (exposed as an OIDC Issuer Discovery URL by the third-party authentication provider). The signed JWTs must have a `kid` header parameter to identify which key must be used. Using symmetrically signed JWTs is not possible at this time.
2. The JWT signing keys from the third-party provider are stored in the configuration of your project, and are checked for changes periodically. If you are rotating your keys (when supported) allow up to 30 minutes for the change to be picked up.
3. It is not possible to disable Supabase Auth at this time.
@@ -48,6 +48,9 @@ jobs:
- wait
if: ${{ needs.wait.outputs.status == 'success' }}
runs-on: ubuntu-latest
env:
SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
SUPABASE_PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
steps:
- uses: supabase/setup-cli@v1
with:
@@ -68,6 +68,7 @@ export const MonacoEditor = ({
lineNumbersMinChars: 0,
scrollBeyondLastLine: false,
wordWrap: 'on',
unusualLineTerminators: 'off',
}}
/>
)
@@ -15,7 +15,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useTableRowsCountQuery } from 'data/table-rows/table-rows-count-query'
import { fetchAllTableRows, useTableRowsQuery } from 'data/table-rows/table-rows-query'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { RoleImpersonationState } from 'lib/role-impersonation'
@@ -84,8 +84,10 @@ const DefaultHeader = () => {
const snap = useTableEditorTableStateSnapshot()
const tableEditorSnap = useTableEditorStateSnapshot()
const canCreateColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
const { can: canCreateColumns } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'columns'
)
const { mutate: sendEvent } = useSendEventMutation()
const onAddRow =
@@ -1,13 +1,13 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Trash2 } from 'lucide-react'
import { useState } from 'react'
import { toast } from 'sonner'
import { Trash2 } from 'lucide-react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common/hooks'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
import { useAPIKeyDeleteMutation } from 'data/api-keys/api-key-delete-mutation'
import { APIKeysData } from 'data/api-keys/api-keys-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { DropdownMenuItem } from 'ui'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import TextConfirmModal from 'ui-patterns/Dialogs/TextConfirmModal'
interface APIKeyDeleteDialogProps {
@@ -19,7 +19,10 @@ export const APIKeyDeleteDialog = ({ apiKey, lastSeen }: APIKeyDeleteDialogProps
const { ref: projectRef } = useParams()
const [isOpen, setIsOpen] = useState(false)
const canDeleteAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*')
const { can: canDeleteAPIKeys } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'*'
)
const { mutate: deleteAPIKey, isLoading: isDeletingAPIKey } = useAPIKeyDeleteMutation({
onSuccess: () => {
@@ -35,17 +38,26 @@ export const APIKeyDeleteDialog = ({ apiKey, lastSeen }: APIKeyDeleteDialogProps
return (
<>
<DropdownMenuItem
className="flex gap-2 !pointer-events-auto"
<DropdownMenuItemTooltip
className="flex gap-2"
onClick={async (e) => {
if (canDeleteAPIKeys) {
e.preventDefault()
setIsOpen(true)
}
}}
disabled={!canDeleteAPIKeys}
tooltip={{
content: {
side: 'left',
text: !canDeleteAPIKeys
? 'You need additional permissions to delete API keys'
: undefined,
},
}}
>
<Trash2 className="size-4 text-destructive" strokeWidth={1.5} /> Delete API key
</DropdownMenuItem>
</DropdownMenuItemTooltip>
<TextConfirmModal
visible={isOpen}
onCancel={() => setIsOpen(false)}
@@ -160,12 +160,10 @@ export const ApiKeysCreateCallout = () => {
* Feedback banner for users who have API keys and the feature is rolled out to them
*/
export const ApiKeysFeedbackBanner = () => {
const { hasApiKeys, isInRollout } = useApiKeysVisibility()
const { hasApiKeys } = useApiKeysVisibility()
// Don't show anything if not in rollout or if keys don't exist
if (!isInRollout || !hasApiKeys) {
return null
}
if (!hasApiKeys) return null
return (
<FeatureBanner
@@ -6,7 +6,7 @@ import { useParams } from 'common'
import CopyButton from 'components/ui/CopyButton'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { cn, EyeOffIcon, Input_Shadcn_, Skeleton, WarningIcon } from 'ui'
// to add in later with follow up PR
@@ -27,8 +27,10 @@ export const PublishableAPIKeys = () => {
[apiKeysData]
)
const isPermissionsLoading = !usePermissionsLoaded()
const canReadAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*')
const { can: canReadAPIKeys, isLoading: isPermissionsLoading } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'*'
)
// The default publisahble key will always be the first one
const apiKey = publishableApiKeys[0]
@@ -46,12 +48,12 @@ export const PublishableAPIKeys = () => {
<div className="flex items-center gap-2">
<ApiKeyInput />
<CopyButton
disabled={isPermissionsLoading || isLoadingApiKeys || !canReadAPIKeys}
type="default"
text={apiKey?.api_key}
iconOnly
size={'tiny'}
size="tiny"
type="default"
className="px-2 rounded-full"
disabled={isPermissionsLoading || isLoadingApiKeys || !canReadAPIKeys}
text={apiKey?.api_key}
/>
</div>
</div>
@@ -65,31 +67,29 @@ export const PublishableAPIKeys = () => {
</div>
)}
</div>
<div className="flex flex-col gap-2 max-w-64">
{/* <Separator /> */}
{/* @mildtomato - To add in later with follow up PR */}
{/* <ShowPublicJWTsDialogComposer /> */}
</div>
{/* <CreatePublishableAPIKeyModal /> */}
</div>
</div>
)
}
function ApiKeyInput() {
const ApiKeyInput = () => {
const { ref: projectRef } = useParams()
const {
data: apiKeysData,
isLoading: isApiKeysLoading,
error,
} = useAPIKeysQuery({ projectRef, reveal: false })
const publishableApiKeys = useMemo(
() => apiKeysData?.filter(({ type }) => type === 'publishable') ?? [],
[apiKeysData]
)
const isPermissionsLoading = !usePermissionsLoaded()
const canReadAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*')
const { can: canReadAPIKeys, isLoading: isPermissionsLoading } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'*'
)
// The default publisahble key will always be the first one
const apiKey = publishableApiKeys[0]
@@ -1,15 +1,12 @@
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import dayjs from 'dayjs'
import { useMemo, useRef } from 'react'
import { useParams } from 'common'
import { FormHeader } from 'components/ui/Forms/FormHeader'
import { APIKeysData, useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import useLogsQuery from 'hooks/analytics/useLogsQuery'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { Card, CardContent, EyeOffIcon, Skeleton, WarningIcon, cn } from 'ui'
import {
Table,
@@ -22,9 +19,6 @@ import {
import { APIKeyRow } from './APIKeyRow'
import CreateSecretAPIKeyDialog from './CreateSecretAPIKeyDialog'
dayjs.extend(duration)
dayjs.extend(relativeTime)
interface LastSeenData {
[hash: string]: { timestamp: string }
}
@@ -62,8 +56,10 @@ export const SecretAPIKeys = () => {
error,
} = useAPIKeysQuery({ projectRef, reveal: false })
const isLoadingPermissions = !usePermissionsLoaded()
const canReadAPIKeys = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, '*')
const { can: canReadAPIKeys, isLoading: isLoadingPermissions } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'*'
)
const lastSeen = useLastSeen(projectRef!)
@@ -1,13 +1,11 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useMemo } from 'react'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { useAPIKeysQuery } from 'data/api-keys/api-keys-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useFlag } from 'hooks/ui/useFlag'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
interface ApiKeysVisibilityState {
isInRollout: boolean
hasApiKeys: boolean
isLoading: boolean
canReadAPIKeys: boolean
@@ -21,8 +19,7 @@ interface ApiKeysVisibilityState {
*/
export function useApiKeysVisibility(): ApiKeysVisibilityState {
const { ref: projectRef } = useParams()
const canReadAPIKeys = useCheckPermissions(PermissionAction.READ, 'api_keys')
const isBasicApiKeysEnabled = useFlag('basicApiKeys')
const { can: canReadAPIKeys } = useAsyncCheckProjectPermissions(PermissionAction.READ, 'api_keys')
const { data: apiKeysData, isLoading } = useAPIKeysQuery({
projectRef,
@@ -39,13 +36,12 @@ 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 = isBasicApiKeysEnabled && canReadAPIKeys && !isLoading && !hasApiKeys
const canInitApiKeys = canReadAPIKeys && !isLoading && !hasApiKeys
// Disable UI for publishable keys and secrets keys if flag is not enabled OR no API keys created yet
const shouldDisableUI = !isBasicApiKeysEnabled || !hasApiKeys
const shouldDisableUI = !hasApiKeys
return {
isInRollout: isBasicApiKeysEnabled,
hasApiKeys,
isLoading,
canReadAPIKeys,
@@ -12,7 +12,7 @@ import NoPermission from 'components/ui/NoPermission'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
import {
@@ -43,18 +43,19 @@ const FormSchema = z.object({
export const AdvancedAuthSettingsForm = () => {
const { ref: projectRef } = useParams()
const { data: organization } = useSelectedOrganizationQuery()
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const { can: canReadConfig } = useAsyncCheckProjectPermissions(
PermissionAction.READ,
'custom_config_gotrue'
)
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const [isUpdatingRequestDurationForm, setIsUpdatingRequestDurationForm] = useState(false)
const [isUpdatingDatabaseForm, setIsUpdatingDatabaseForm] = useState(false)
const {
data: authConfig,
error: authConfigError,
isLoading,
isError,
} = useAuthConfigQuery({ projectRef })
const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation()
@@ -24,10 +24,10 @@ import { ProviderForm } from './ProviderForm'
export const AuthProvidersForm = () => {
const { ref: projectRef } = useParams()
const {
isLoading,
error: authConfigError,
isError,
data: authConfig,
error: authConfigError,
isLoading,
isError,
isSuccess,
} = useAuthConfigQuery({ projectRef })
@@ -1,5 +1,6 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Check } from 'lucide-react'
import { useQueryState } from 'nuqs'
import { useEffect, useState } from 'react'
import ReactMarkdown from 'react-markdown'
import { toast } from 'sonner'
@@ -13,9 +14,8 @@ import type { components } from 'data/api'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useCustomDomainsQuery } from 'data/custom-domains/custom-domains-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { BASE_PATH } from 'lib/constants'
import { useQueryState } from 'nuqs'
import { Button, Form, Input, Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from 'ui'
import { Admonition } from 'ui-patterns'
import { NO_REQUIRED_CHARACTERS } from '../Auth.constants'
@@ -23,12 +23,14 @@ import { AuthAlert } from './AuthAlert'
import type { Provider } from './AuthProvidersForm.types'
import FormField from './FormField'
export interface ProviderFormProps {
interface ProviderFormProps {
config: components['schemas']['GoTrueConfigResponse']
provider: Provider
isActive: boolean
}
const doubleNegativeKeys = ['SMS_AUTOCONFIRM']
export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps) => {
const { ref: projectRef } = useParams()
const [urlProvider, setUrlProvider] = useQueryState('provider', { defaultValue: '' })
@@ -36,8 +38,7 @@ export const ProviderForm = ({ config, provider, isActive }: ProviderFormProps)
const [open, setOpen] = useState(false)
const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation()
const doubleNegativeKeys = ['SMS_AUTOCONFIRM']
const canUpdateConfig: boolean = useCheckPermissions(
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
@@ -13,7 +13,7 @@ import { InlineLink } from 'components/ui/InlineLink'
import NoPermission from 'components/ui/NoPermission'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
@@ -29,6 +29,7 @@ import {
WarningIcon,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import ShimmeringLoader from 'ui-patterns/ShimmeringLoader'
import { NO_REQUIRED_CHARACTERS } from '../Auth.constants'
const schema = object({
@@ -41,11 +42,23 @@ const schema = object({
const BasicAuthSettingsForm = () => {
const { ref: projectRef } = useParams()
const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef })
const {
data: authConfig,
error: authConfigError,
isError,
isSuccess,
isLoading,
} = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation()
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const { can: canReadConfig, isSuccess: isPermissionsLoaded } = useAsyncCheckProjectPermissions(
PermissionAction.READ,
'custom_config_gotrue'
)
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const form = useForm({
resolver: yupResolver(schema),
@@ -95,207 +108,223 @@ const BasicAuthSettingsForm = () => {
)
}
if (isError) {
return (
<Alert_Shadcn_ variant="destructive">
<WarningIcon />
<AlertTitle_Shadcn_>Failed to retrieve auth configuration</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>{authConfigError.message}</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)
}
if (!canReadConfig) {
return (
<div className="mt-8">
<NoPermission resourceText="view auth configuration settings" />
</div>
)
}
return (
<ScaffoldSection isFullWidth>
<ScaffoldSectionTitle className="mb-4">User Signups</ScaffoldSectionTitle>
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Card>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="DISABLE_SIGNUP"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Allow new users to sign up"
description="If this is disabled, new users will not be able to sign up to your application"
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="SECURITY_MANUAL_LINKING_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Allow manual linking"
description={
<>
Enable{' '}
<InlineLink
className="text-foreground-light hover:text-foreground"
href="https://supabase.com/docs/guides/auth/auth-identity-linking#manual-linking-beta"
>
manual linking APIs
</InlineLink>{' '}
for your project
</>
}
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="EXTERNAL_ANONYMOUS_USERS_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Allow anonymous sign-ins"
description={
<>
Enable{' '}
<InlineLink
className="text-foreground-light hover:text-foreground"
href="https://supabase.com/docs/guides/auth/auth-anonymous"
>
anonymous sign-ins
</InlineLink>{' '}
for your project
</>
}
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
{isError && (
<Alert_Shadcn_ variant="destructive">
<WarningIcon />
<AlertTitle_Shadcn_>Failed to retrieve auth configuration</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>{authConfigError.message}</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
{form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && (
<Alert_Shadcn_
className="flex w-full items-center justify-between mt-4"
variant="warning"
>
<WarningIcon />
<div>
<AlertTitle_Shadcn_>
Anonymous users will use the <code className="text-xs">authenticated</code>{' '}
role when signing in
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex flex-col gap-y-3">
<p>
As a result, anonymous users will be subjected to RLS policies that apply to
the <code className="text-xs">public</code> and{' '}
<code className="text-xs">authenticated</code> roles. We strongly advise{' '}
<Link
href={`/project/${projectRef}/auth/policies`}
className="text-foreground underline"
>
reviewing your RLS policies
</Link>{' '}
to ensure that access to your data is restricted where required.
</p>
<Button asChild type="default" className="w-min" icon={<ExternalLink />}>
<Link href="https://supabase.com/docs/guides/auth/auth-anonymous#access-control">
View access control docs
</Link>
</Button>
</AlertDescription_Shadcn_>
</div>
</Alert_Shadcn_>
)}
{isPermissionsLoaded && !canReadConfig && (
<div className="mt-8">
<NoPermission resourceText="view auth configuration settings" />
</div>
)}
{!authConfig?.SECURITY_CAPTCHA_ENABLED &&
form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && (
<Alert_Shadcn_ className="mt-4">
{isLoading && (
<Card>
<CardContent className="py-6">
<ShimmeringLoader />
</CardContent>
<CardContent className="py-6">
<ShimmeringLoader />
</CardContent>
<CardContent className="py-6">
<ShimmeringLoader />
</CardContent>
<CardContent className="py-7">
<ShimmeringLoader />
</CardContent>
<CardContent className="py-7"></CardContent>
</Card>
)}
{isSuccess && (
<Form_Shadcn_ {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<Card>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="DISABLE_SIGNUP"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Allow new users to sign up"
description="If this is disabled, new users will not be able to sign up to your application"
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="SECURITY_MANUAL_LINKING_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Allow manual linking"
description={
<>
Enable{' '}
<InlineLink
className="text-foreground-light hover:text-foreground"
href="https://supabase.com/docs/guides/auth/auth-identity-linking#manual-linking-beta"
>
manual linking APIs
</InlineLink>{' '}
for your project
</>
}
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="EXTERNAL_ANONYMOUS_USERS_ENABLED"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Allow anonymous sign-ins"
description={
<>
Enable{' '}
<InlineLink
className="text-foreground-light hover:text-foreground"
href="https://supabase.com/docs/guides/auth/auth-anonymous"
>
anonymous sign-ins
</InlineLink>{' '}
for your project
</>
}
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
{form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && (
<Alert_Shadcn_
className="flex w-full items-center justify-between mt-4"
variant="warning"
>
<WarningIcon />
<AlertTitle_Shadcn_>
We highly recommend{' '}
<InlineLink href={`/project/${projectRef}/auth/protection`}>
enabling captcha
</InlineLink>{' '}
for anonymous sign-ins
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
This will prevent potential abuse on sign-ins which may bloat your database
and incur costs for monthly active users (MAU)
</AlertDescription_Shadcn_>
<div>
<AlertTitle_Shadcn_>
Anonymous users will use the <code className="text-xs">authenticated</code>{' '}
role when signing in
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_ className="flex flex-col gap-y-3">
<p>
As a result, anonymous users will be subjected to RLS policies that apply
to the <code className="text-xs">public</code> and{' '}
<code className="text-xs">authenticated</code> roles. We strongly advise{' '}
<Link
href={`/project/${projectRef}/auth/policies`}
className="text-foreground underline"
>
reviewing your RLS policies
</Link>{' '}
to ensure that access to your data is restricted where required.
</p>
<Button asChild type="default" className="w-min" icon={<ExternalLink />}>
<Link href="https://supabase.com/docs/guides/auth/auth-anonymous#access-control">
View access control docs
</Link>
</Button>
</AlertDescription_Shadcn_>
</div>
</Alert_Shadcn_>
)}
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="MAILER_AUTOCONFIRM"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Confirm email"
description="Users will need to confirm their email address before signing in for the first time"
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
{!authConfig?.SECURITY_CAPTCHA_ENABLED &&
form.watch('EXTERNAL_ANONYMOUS_USERS_ENABLED') && (
<Alert_Shadcn_ className="mt-4">
<WarningIcon />
<AlertTitle_Shadcn_>
We highly recommend{' '}
<InlineLink href={`/project/${projectRef}/auth/protection`}>
enabling captcha
</InlineLink>{' '}
for anonymous sign-ins
</AlertTitle_Shadcn_>
<AlertDescription_Shadcn_>
This will prevent potential abuse on sign-ins which may bloat your database
and incur costs for monthly active users (MAU)
</AlertDescription_Shadcn_>
</Alert_Shadcn_>
)}
</CardContent>
<CardContent>
<FormField_Shadcn_
control={form.control}
name="MAILER_AUTOCONFIRM"
render={({ field }) => (
<FormItemLayout
layout="flex-row-reverse"
label="Confirm email"
description="Users will need to confirm their email address before signing in for the first time"
>
<FormControl_Shadcn_>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
disabled={!canUpdateConfig}
/>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset()}>
Cancel
</Button>
)}
/>
</CardContent>
<CardFooter className="justify-end space-x-2">
{form.formState.isDirty && (
<Button type="default" onClick={() => form.reset()}>
Cancel
<Button
type="primary"
htmlType="submit"
disabled={!canUpdateConfig || isUpdatingConfig || !form.formState.isDirty}
loading={isUpdatingConfig}
>
Save changes
</Button>
)}
<Button
type="primary"
htmlType="submit"
disabled={!canUpdateConfig || isUpdatingConfig || !form.formState.isDirty}
loading={isUpdatingConfig}
>
Save changes
</Button>
</CardFooter>
</Card>
</form>
</Form_Shadcn_>
</CardFooter>
</Card>
</form>
</Form_Shadcn_>
)}
</ScaffoldSection>
)
}
@@ -2,21 +2,19 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Code, Monitor } from 'lucide-react'
import { editor } from 'monaco-editor'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useForm } from 'react-hook-form'
import ReactMarkdown from 'react-markdown'
import { toast } from 'sonner'
import { useForm } from 'react-hook-form'
import { useParams } from 'common'
import CodeEditor from 'components/ui/CodeEditor/CodeEditor'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useValidateSpamMutation, ValidateSpamResponse } from 'data/auth/validate-spam-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import type { FormSchema } from 'types'
import {
Badge,
Button,
Card,
CardContent,
CardFooter,
Form_Shadcn_,
@@ -42,7 +40,10 @@ interface TemplateEditorProps {
const TemplateEditor = ({ template }: TemplateEditorProps) => {
const { ref: projectRef } = useParams()
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
// Add a ref to the code editor
const editorRef = useRef<editor.IStandaloneCodeEditor>()
@@ -4,7 +4,7 @@ import { ChevronDown } from 'lucide-react'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import {
Button,
@@ -33,7 +33,10 @@ export const AddHookDropdown = ({
const { data: organization } = useSelectedOrganizationQuery()
const { data: authConfig } = useAuthConfigQuery({ projectRef })
const canUpdateAuthHook = useCheckPermissions(PermissionAction.AUTH_EXECUTE, '*')
const { can: canUpdateAuthHook } = useAsyncCheckProjectPermissions(
PermissionAction.AUTH_EXECUTE,
'*'
)
const hooks: Hook[] = HOOKS_DEFINITIONS.map((definition) => {
return {
@@ -1,11 +1,11 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Check, Webhook } from 'lucide-react'
import { Badge, Input, copyToClipboard } from 'ui'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { Hook } from './hooks.constants'
import { DocsButton } from 'components/ui/DocsButton'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { Badge, Input, copyToClipboard } from 'ui'
import { Hook } from './hooks.constants'
interface HookCardProps {
hook: Hook
@@ -13,7 +13,10 @@ interface HookCardProps {
}
export const HookCard = ({ hook, onSelect }: HookCardProps) => {
const canUpdateAuthHook = useCheckPermissions(PermissionAction.AUTH_EXECUTE, '*')
const { can: canUpdateAuthHook } = useAsyncCheckProjectPermissions(
PermissionAction.AUTH_EXECUTE,
'*'
)
return (
<div className="bg-surface-100 border-default overflow-hidden border shadow px-5 py-4 flex flex-row first:rounded-t-md last:rounded-b-md space-x-4">
@@ -11,7 +11,7 @@ import NoPermission from 'components/ui/NoPermission'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
import {
@@ -87,8 +87,14 @@ const MfaAuthSettingsForm = () => {
const [isUpdatingTotpForm, setIsUpdatingTotpForm] = useState(false)
const [isUpdatingPhoneForm, setIsUpdatingPhoneForm] = useState(false)
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const { can: canReadConfig } = useAsyncCheckProjectPermissions(
PermissionAction.READ,
'custom_config_gotrue'
)
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const { data: organization } = useSelectedOrganizationQuery()
const isProPlanAndUp = organization?.plan?.id !== 'free'
@@ -1,11 +1,11 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { Check, ChevronsUpDown } from 'lucide-react'
import { useEffect, useState } from 'react'
import { UseFormReturn } from 'react-hook-form'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useDatabaseRolesQuery } from 'data/database-roles/database-roles-query'
import { useTablesQuery } from 'data/tables/tables-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import {
Button,
@@ -62,7 +62,10 @@ export const PolicyDetailsV2 = ({
}: PolicyDetailsV2Props) => {
const { data: project } = useSelectedProjectQuery()
const [open, setOpen] = useState(false)
const canUpdatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const { can: canUpdatePolicies } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'tables'
)
const { data: tables, isSuccess: isSuccessTables } = useTablesQuery({
projectRef: project?.ref,
@@ -15,7 +15,7 @@ import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useDatabasePolicyUpdateMutation } from 'data/database-policies/database-policy-update-mutation'
import { databasePoliciesKeys } from 'data/database-policies/keys'
import { QueryResponseError, useExecuteSqlMutation } from 'data/sql/execute-sql-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import {
Button,
@@ -67,7 +67,10 @@ export const PolicyEditorPanel = memo(function ({
const queryClient = useQueryClient()
const { data: selectedProject } = useSelectedProjectQuery()
const canUpdatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const { can: canUpdatePolicies } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'tables'
)
// [Joshen] Hyrid form fields, just spit balling to get a decent POC out
const [using, setUsing] = useState('')
@@ -6,7 +6,7 @@ import { Edit, MoreVertical, Trash } from 'lucide-react'
import { DropdownMenuItemTooltip } from 'components/ui/DropdownMenuItemTooltip'
import Panel from 'components/ui/Panel'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import {
@@ -38,7 +38,10 @@ const PolicyRow = ({
onSelectDeletePolicy = noop,
}: PolicyRowProps) => {
const aiSnap = useAiAssistantStateSnapshot()
const canUpdatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies')
const { can: canUpdatePolicies } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'policies'
)
const { data: project } = useSelectedProjectQuery()
const { data: authConfig } = useAuthConfigQuery({ projectRef: project?.ref })
@@ -5,7 +5,7 @@ import { Lock, Unlock } from 'lucide-react'
import { useParams } from 'common'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { EditorTablePageLink } from 'data/prefetchers/project.$ref.editor.$id'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { AiIconAnimation, Badge } from 'ui'
@@ -35,8 +35,14 @@ const PolicyTableRowHeader = ({
const { ref } = useParams()
const aiSnap = useAiAssistantStateSnapshot()
const canCreatePolicies = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'policies')
const canToggleRLS = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const { can: canCreatePolicies } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'policies'
)
const { can: canToggleRLS } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'tables'
)
const isRealtimeSchema = table.schema === 'realtime'
const isRealtimeMessagesTable = isRealtimeSchema && table.name === 'messages'
@@ -12,7 +12,7 @@ import { InlineLink } from 'components/ui/InlineLink'
import NoPermission from 'components/ui/NoPermission'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
@@ -69,18 +69,25 @@ const schema = object({
const ProtectionAuthSettingsForm = () => {
const { ref: projectRef } = useParams()
const {
data: authConfig,
error: authConfigError,
isLoading,
isError,
} = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation()
const [isUpdatingProtection, setIsUpdatingProtection] = useState(false)
const { data: authConfig, error: authConfigError, isError } = useAuthConfigQuery({ projectRef })
const { mutate: updateAuthConfig, isLoading: isUpdatingConfig } = useAuthConfigUpdateMutation({
onError: (error) => {
toast.error(`Failed to update settings: ${error?.message}`)
},
onSuccess: () => {
toast.success('Successfully updated settings')
},
})
const [hidden, setHidden] = useState(true)
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const { can: canReadConfig } = useAsyncCheckProjectPermissions(
PermissionAction.READ,
'custom_config_gotrue'
)
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const protectionForm = useForm({
resolver: yupResolver(schema),
@@ -102,7 +109,7 @@ const ProtectionAuthSettingsForm = () => {
})
useEffect(() => {
if (authConfig && !isUpdatingProtection) {
if (authConfig && !isUpdatingConfig) {
protectionForm.reset({
DISABLE_SIGNUP: !authConfig.DISABLE_SIGNUP,
EXTERNAL_ANONYMOUS_USERS_ENABLED: authConfig.EXTERNAL_ANONYMOUS_USERS_ENABLED || false,
@@ -120,11 +127,9 @@ const ProtectionAuthSettingsForm = () => {
PASSWORD_HIBP_ENABLED: authConfig.PASSWORD_HIBP_ENABLED || false,
})
}
}, [authConfig, isUpdatingProtection])
}, [authConfig, isUpdatingConfig])
const onSubmitProtection = (values: any) => {
setIsUpdatingProtection(true)
const payload = { ...values }
payload.DISABLE_SIGNUP = !values.DISABLE_SIGNUP
// The backend uses empty string to represent no required characters in the password
@@ -132,19 +137,7 @@ const ProtectionAuthSettingsForm = () => {
payload.PASSWORD_REQUIRED_CHARACTERS = ''
}
updateAuthConfig(
{ projectRef: projectRef!, config: payload },
{
onError: (error) => {
toast.error(`Failed to update settings: ${error?.message}`)
setIsUpdatingProtection(false)
},
onSuccess: () => {
toast.success('Successfully updated settings')
setIsUpdatingProtection(false)
},
}
)
updateAuthConfig({ projectRef: projectRef!, config: payload })
}
if (isError) {
@@ -282,10 +275,8 @@ const ProtectionAuthSettingsForm = () => {
<Button
type="primary"
htmlType="submit"
disabled={
!canUpdateConfig || isUpdatingProtection || !protectionForm.formState.isDirty
}
loading={isUpdatingProtection}
disabled={!canUpdateConfig || isUpdatingConfig || !protectionForm.formState.isDirty}
loading={isUpdatingConfig}
>
Save changes
</Button>
@@ -13,7 +13,7 @@ import NoPermission from 'components/ui/NoPermission'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import {
Button,
Card,
@@ -32,8 +32,14 @@ import { isSmtpEnabled } from '../SmtpForm/SmtpForm.utils'
const RateLimits = () => {
const { ref: projectRef } = useParams()
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const { can: canReadConfig } = useAsyncCheckProjectPermissions(
PermissionAction.READ,
'custom_config_gotrue'
)
const {
data: authConfig,
@@ -3,7 +3,7 @@ import { Globe, Trash } from 'lucide-react'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { EmptyListState } from 'components/ui/States'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { Button, Checkbox_Shadcn_ } from 'ui'
import { ValueContainer } from './ValueContainer'
@@ -24,7 +24,10 @@ export const RedirectUrlList = ({
onSelectRemoveURLs,
onSelectClearSelection,
}: RedirectUrlListProps) => {
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
// [Joshen] One for next time: maybe shift this into a reusable logic since it
// seems like we can use this in multiple places for future
@@ -11,7 +11,7 @@ import NoPermission from 'components/ui/NoPermission'
import UpgradeToPro from 'components/ui/UpgradeToPro'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from 'lib/constants'
import {
@@ -68,8 +68,14 @@ const SessionsAuthSettingsForm = () => {
const [isUpdatingRefreshTokens, setIsUpdatingRefreshTokens] = useState(false)
const [isUpdatingUserSessions, setIsUpdatingUserSessions] = useState(false)
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const { can: canReadConfig } = useAsyncCheckProjectPermissions(
PermissionAction.READ,
'custom_config_gotrue'
)
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const { data: organization } = useSelectedOrganizationQuery()
const isProPlanAndUp = organization?.plan?.id !== 'free'
@@ -1,16 +1,16 @@
import { yupResolver } from '@hookform/resolvers/yup'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { AlertCircle } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { object, string } from 'yup'
import { yupResolver } from '@hookform/resolvers/yup'
import { useParams } from 'common'
import { ScaffoldSection, ScaffoldSectionTitle } from 'components/layouts/Scaffold'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
@@ -24,7 +24,7 @@ import {
Form_Shadcn_,
Input_Shadcn_,
} from 'ui'
import { AlertCircle } from 'lucide-react'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
const schema = object({
SITE_URL: string().required('Must have a Site URL'),
@@ -36,7 +36,10 @@ const SiteUrl = () => {
const { mutate: updateAuthConfig } = useAuthConfigUpdateMutation()
const [isUpdatingSiteUrl, setIsUpdatingSiteUrl] = useState(false)
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const siteUrlForm = useForm({
resolver: yupResolver(schema),
@@ -1,18 +1,18 @@
import { yupResolver } from '@hookform/resolvers/yup'
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { AlertTriangle, Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
import { useEffect, useState } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import * as yup from 'yup'
import { useParams } from 'common'
import { ScaffoldContainer, ScaffoldSection } from 'components/layouts/Scaffold'
import NoPermission from 'components/ui/NoPermission'
import { useAuthConfigQuery } from 'data/auth/auth-config-query'
import { useAuthConfigUpdateMutation } from 'data/auth/auth-config-update-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { AlertTriangle, Eye, EyeOff } from 'lucide-react'
import Link from 'next/link'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import {
AlertDescription_Shadcn_,
AlertTitle_Shadcn_,
@@ -30,7 +30,7 @@ import {
WarningIcon,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { urlRegex } from './../Auth.constants'
import { urlRegex } from '../Auth.constants'
import { defaultDisabledSmtpFormValues } from './SmtpForm.constants'
import { generateFormValues, isSmtpEnabled } from './SmtpForm.utils'
@@ -53,8 +53,14 @@ const SmtpForm = () => {
const [enableSmtp, setEnableSmtp] = useState(false)
const [hidden, setHidden] = useState(true)
const canReadConfig = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const { can: canReadConfig } = useAsyncCheckProjectPermissions(
PermissionAction.READ,
'custom_config_gotrue'
)
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
const smtpSchema = yup.object({
SMTP_ADMIN_EMAIL: yup.string().when('ENABLE_SMTP', {
@@ -17,7 +17,7 @@ import {
ThirdPartyAuthIntegration,
useThirdPartyAuthIntegrationsQuery,
} from 'data/third-party-auth/integrations-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { cn } from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { AddIntegrationDropdown } from './AddIntegrationDropdown'
@@ -49,7 +49,10 @@ export const ThirdPartyAuthForm = () => {
useState<ThirdPartyAuthIntegration>()
const { mutateAsync: deleteIntegration } = useDeleteThirdPartyAuthIntegrationMutation()
const canUpdateConfig = useCheckPermissions(PermissionAction.UPDATE, 'custom_config_gotrue')
const { can: canUpdateConfig } = useAsyncCheckProjectPermissions(
PermissionAction.UPDATE,
'custom_config_gotrue'
)
if (isError) {
return (
@@ -2,7 +2,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants'
import { ChevronDown, Mail, UserPlus } from 'lucide-react'
import { useState } from 'react'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import {
Button,
DropdownMenu,
@@ -17,8 +17,14 @@ import CreateUserModal from './CreateUserModal'
import InviteUserModal from './InviteUserModal'
const AddUserDropdown = () => {
const canInviteUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'invite_user')
const canCreateUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'create_user')
const { can: canInviteUsers } = useAsyncCheckProjectPermissions(
PermissionAction.AUTH_EXECUTE,
'invite_user'
)
const { can: canCreateUsers } = useAsyncCheckProjectPermissions(
PermissionAction.AUTH_EXECUTE,
'create_user'
)
const [inviteVisible, setInviteVisible] = useState(false)
const [createVisible, setCreateVisible] = useState(false)
@@ -7,7 +7,7 @@ import * as z from 'zod'
import { useParams } from 'common'
import { useUserCreateMutation } from 'data/auth/user-create-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import {
Button,
Checkbox_Shadcn_,
@@ -38,7 +38,10 @@ const CreateUserFormSchema = z.object({
const CreateUserModal = ({ visible, setVisible }: CreateUserModalProps) => {
const { ref: projectRef } = useParams()
const canCreateUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'create_user')
const { can: canCreateUsers } = useAsyncCheckProjectPermissions(
PermissionAction.AUTH_EXECUTE,
'create_user'
)
const { mutate: createUser, isLoading: isCreatingUser } = useUserCreateMutation({
onSuccess(res) {
@@ -4,7 +4,7 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import { useUserInviteMutation } from 'data/auth/user-invite-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { Button, Form, Input, Modal } from 'ui'
export type InviteUserModalProps = {
@@ -22,7 +22,10 @@ const InviteUserModal = ({ visible, setVisible }: InviteUserModalProps) => {
setVisible(false)
},
})
const canInviteUsers = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'invite_user')
const { can: canInviteUsers } = useAsyncCheckProjectPermissions(
PermissionAction.AUTH_EXECUTE,
'invite_user'
)
const validate = (values: any) => {
const errors: any = {}
@@ -15,7 +15,7 @@ import { useUserSendMagicLinkMutation } from 'data/auth/user-send-magic-link-mut
import { useUserSendOTPMutation } from 'data/auth/user-send-otp-mutation'
import { useUserUpdateMutation } from 'data/auth/user-update-mutation'
import { User } from 'data/auth/users-infinite-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { BASE_PATH } from 'lib/constants'
import { timeout } from 'lib/helpers'
import { Button, cn, Separator } from 'ui'
@@ -59,12 +59,24 @@ export const UserOverview = ({ user, onDeleteSuccess }: UserOverviewProps) => {
}
)
const canUpdateUser = useCheckPermissions(PermissionAction.AUTH_EXECUTE, '*')
const canSendMagicLink = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'send_magic_link')
const canSendRecovery = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'send_recovery')
const canSendOtp = useCheckPermissions(PermissionAction.AUTH_EXECUTE, 'send_otp')
const canRemoveUser = useCheckPermissions(PermissionAction.TENANT_SQL_DELETE, 'auth.users')
const canRemoveMFAFactors = useCheckPermissions(
const { can: canUpdateUser } = useAsyncCheckProjectPermissions(PermissionAction.AUTH_EXECUTE, '*')
const { can: canSendMagicLink } = useAsyncCheckProjectPermissions(
PermissionAction.AUTH_EXECUTE,
'send_magic_link'
)
const { can: canSendRecovery } = useAsyncCheckProjectPermissions(
PermissionAction.AUTH_EXECUTE,
'send_recovery'
)
const { can: canSendOtp } = useAsyncCheckProjectPermissions(
PermissionAction.AUTH_EXECUTE,
'send_otp'
)
const { can: canRemoveUser } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_DELETE,
'auth.users'
)
const { can: canRemoveMFAFactors } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_DELETE,
'auth.mfa_factors'
)
@@ -5,7 +5,7 @@ import { toast } from 'sonner'
import { useParams } from 'common'
import Table from 'components/to-be-cleaned/Table'
import AlertError from 'components/ui/AlertError'
import { useDeleteDestinationMutation } from 'data/replication/delete-destination-mutation'
import { useDeleteDestinationPipelineMutation } from 'data/replication/delete-destination-pipeline-mutation'
import { useReplicationPipelineStatusQuery } from 'data/replication/pipeline-status-query'
import { Pipeline } from 'data/replication/pipelines-query'
import { useStopPipelineMutation } from 'data/replication/stop-pipeline-mutation'
@@ -69,7 +69,7 @@ export const DestinationRow = ({
: PipelineStatusRequestStatus.None
const { mutateAsync: stopPipeline } = useStopPipelineMutation()
const { mutateAsync: deleteDestination } = useDeleteDestinationMutation({})
const { mutateAsync: deleteDestinationPipeline } = useDeleteDestinationPipelineMutation({})
const pipelineStatus = pipelineStatusData?.status
const statusName = getStatusName(pipelineStatus)
@@ -84,9 +84,11 @@ export const DestinationRow = ({
try {
await stopPipeline({ projectRef, pipelineId: pipeline.id })
// deleting the destination also deletes the pipeline because of cascade delete
// so we don't need to call deletePipeline explicitly
await deleteDestination({ projectRef, destinationId: destinationId })
await deleteDestinationPipeline({
projectRef,
destinationId: destinationId,
pipelineId: pipeline.id,
})
} catch (error) {
toast.error(PIPELINE_ERROR_MESSAGES.DELETE_DESTINATION)
}
@@ -1,158 +1,207 @@
import { isEmpty } from 'lodash'
import { Eye, EyeOff } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useState } from 'react'
import { EyeOff, Eye } from 'lucide-react'
import { type SubmitHandler, useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { toast } from 'sonner'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
Form_Shadcn_,
FormControl_Shadcn_,
FormField_Shadcn_,
Input_Shadcn_,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { GenericSkeletonLoader } from 'components/ui/ShimmeringLoader'
import { useVaultSecretDecryptedValueQuery } from 'data/vault/vault-secret-decrypted-value-query'
import { useVaultSecretUpdateMutation } from 'data/vault/vault-secret-update-mutation'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import type { VaultSecret } from 'types'
import { Button, Form, Input, Modal } from 'ui'
interface EditSecretModalProps {
selectedSecret: VaultSecret | undefined
visible: boolean
secret: VaultSecret
onClose: () => void
}
const EditSecretModal = ({ selectedSecret, onClose }: EditSecretModalProps) => {
const { data: project } = useSelectedProjectQuery()
const SecretSchema = z.object({
name: z.string().min(1, 'Please provide a name for your secret'),
description: z.string().optional(),
secret: z.string().min(1, 'Please enter your secret value'),
})
const formId = 'edit-vault-secret-form'
const EditSecretModal = ({ visible, secret, onClose }: EditSecretModalProps) => {
const [showSecretValue, setShowSecretValue] = useState(false)
const { mutateAsync: updateSecret } = useVaultSecretUpdateMutation()
let INITIAL_VALUES = {
name: selectedSecret?.name ?? '',
description: selectedSecret?.description ?? '',
secret: selectedSecret?.decryptedSecret ?? '',
const { data: project } = useSelectedProjectQuery()
const { data, isLoading: isLoadingSecretValue } = useVaultSecretDecryptedValueQuery(
{
projectRef: project?.ref,
id: secret.id,
connectionString: project?.connectionString,
},
{ enabled: !!project?.ref }
)
const values = {
name: secret.name ?? '',
description: secret.description ?? '',
secret: secret.decryptedSecret ?? data ?? '',
}
const form = useForm<z.infer<typeof SecretSchema>>({
resolver: zodResolver(SecretSchema),
defaultValues: values,
values,
})
useEffect(() => {
if (selectedSecret !== undefined) {
setShowSecretValue(false)
}
}, [selectedSecret])
const { mutate: updateSecret, isLoading: isSubmitting } = useVaultSecretUpdateMutation()
const validate = (values: any) => {
const errors: any = {}
if (values.name.length === 0) errors.name = 'Please provide a name for your secret'
if (values.secret.length === 0) errors.secret = 'Please enter your secret value'
return errors
}
const onUpdateSecret = async (values: any, { setSubmitting }: any) => {
const onSubmit: SubmitHandler<z.infer<typeof SecretSchema>> = async (values) => {
if (!project) return console.error('Project is required')
try {
const payload: Partial<VaultSecret> = {}
if (values.name !== selectedSecret?.name) payload.name = values.name
if (values.description !== selectedSecret?.description)
payload.description = values.description
payload.secret = values.secret
const payload: Partial<VaultSecret> = {
secret: values.secret,
}
if (values.name !== secret.name) payload.name = values.name
if (values.description !== secret.description) payload.description = values.description
if (!isEmpty(payload) && selectedSecret) {
setSubmitting(true)
const res = await updateSecret({
if (Object.keys(payload).length > 0) {
updateSecret(
{
projectRef: project.ref,
connectionString: project?.connectionString,
id: selectedSecret.id,
id: secret.id,
...payload,
})
if (!res.error) {
toast.success('Successfully updated secret')
setSubmitting(false)
onClose()
} else {
toast.error(`Failed to update secret: ${res.error.message}`)
setSubmitting(false)
},
{
onSuccess: () => {
toast.success('Successfully updated secret')
onClose()
},
onError: (error) => {
toast.error(`Failed to update secret: ${error.message}`)
},
}
}
} finally {
)
}
}
return (
<Modal
hideFooter
size="medium"
visible={selectedSecret !== undefined}
onCancel={onClose}
header={<h5 className="text-sm text-foreground">Edit secret</h5>}
<Dialog
open={visible}
onOpenChange={(open) => {
if (!open) {
form.reset()
onClose()
}
}}
>
<Form
id="add-new-secret-form"
initialValues={INITIAL_VALUES}
validate={validate}
validateOnBlur={false}
onSubmit={onUpdateSecret}
>
{({ isSubmitting, resetForm }: any) => {
const {
data,
isLoading: isLoadingSecretValue,
isSuccess: isSuccessSecretValue,
// [Joshen] JFYI this is breaking rules of hooks, will be fixed once we move to
// using react hook form instead
// eslint-disable-next-line react-hooks/rules-of-hooks
} = useVaultSecretDecryptedValueQuery(
{
projectRef: project?.ref!,
id: selectedSecret?.id!,
connectionString: project?.connectionString,
},
{ enabled: selectedSecret !== undefined && !!(project?.ref && selectedSecret?.id) }
)
// [Joshen] JFYI this is breaking rules of hooks, will be fixed once we move to
// using react hook form instead
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
if (selectedSecret !== undefined && isSuccessSecretValue) {
resetForm({
values: { ...INITIAL_VALUES, secret: data },
initialValues: { ...INITIAL_VALUES, secret: data },
})
}
}, [selectedSecret, isSuccessSecretValue])
return isLoadingSecretValue ? (
<Modal.Content>
<GenericSkeletonLoader />
</Modal.Content>
) : (
<>
<Modal.Content className="space-y-4">
<Input id="name" label="Name" />
<Input id="description" label="Description" labelOptional="Optional" />
<Input
id="secret"
type={showSecretValue ? 'text' : 'password'}
label="Secret value"
actions={
<div className="mr-1">
<Button
type="default"
icon={showSecretValue ? <EyeOff /> : <Eye />}
onClick={() => setShowSecretValue(!showSecretValue)}
/>
</div>
}
/>
</Modal.Content>
<Modal.Separator />
<Modal.Content className="flex items-center justify-end space-x-2">
<Button type="default" disabled={isSubmitting} onClick={() => onClose()}>
Cancel
</Button>
<Button htmlType="submit" disabled={isSubmitting} loading={isSubmitting}>
Update secret
</Button>
</Modal.Content>
</>
)
}}
</Form>
</Modal>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit secret</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
{isLoadingSecretValue ? (
<DialogSection>
<GenericSkeletonLoader />
</DialogSection>
) : (
<>
<DialogSection>
<Form_Shadcn_ {...form}>
<form
id={formId}
className="flex flex-col gap-4"
autoComplete="off"
onSubmit={form.handleSubmit(onSubmit)}
>
<FormField_Shadcn_
key="name"
name="name"
control={form.control}
render={({ field }) => (
<FormItemLayout name="name" label="Name">
<FormControl_Shadcn_>
<Input_Shadcn_ id="name" {...field} />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
key="description"
name="description"
control={form.control}
render={({ field }) => (
<FormItemLayout
name="description"
label="Description"
labelOptional="Optional"
>
<FormControl_Shadcn_>
<Input_Shadcn_ id="description" {...field} data-lpignore="true" />
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
<FormField_Shadcn_
key="secret"
name="secret"
control={form.control}
render={({ field }) => (
<FormItemLayout name="secret" label="Secret value">
<FormControl_Shadcn_>
<div className="relative">
<Input_Shadcn_
id="secret"
type={showSecretValue ? 'text' : 'password'}
{...field}
data-lpignore="true"
/>
<Button
type="default"
title={showSecretValue ? `Hide secret value` : `Show secret value`}
aria-label={
showSecretValue ? `Hide secret value` : `Show secret value`
}
className="absolute right-2 top-1 px-3 py-2"
icon={showSecretValue ? <EyeOff /> : <Eye />}
onClick={() => setShowSecretValue(!showSecretValue)}
/>
</div>
</FormControl_Shadcn_>
</FormItemLayout>
)}
/>
</form>
</Form_Shadcn_>
</DialogSection>
<DialogFooter>
<Button
type="default"
disabled={isSubmitting}
onClick={() => {
form.reset()
onClose()
}}
>
Cancel
</Button>
<Button form={formId} htmlType="submit" loading={isSubmitting}>
Update secret
</Button>
</DialogFooter>
</>
)}
</DialogContent>
</Dialog>
)
}
@@ -19,17 +19,17 @@ import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { Edit3, Eye, EyeOff, Key, Loader, MoreVertical, Trash } from 'lucide-react'
import type { VaultSecret } from 'types'
import EditSecretModal from './EditSecretModal'
interface SecretRowProps {
secret: VaultSecret
onSelectEdit: (secret: VaultSecret) => void
onSelectRemove: (secret: VaultSecret) => void
}
const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) => {
const SecretRow = ({ secret, onSelectRemove }: SecretRowProps) => {
const { ref } = useParams()
const { data: project } = useSelectedProjectQuery()
const [modal, setModal] = useState<string | null>(null)
const [revealSecret, setRevealSecret] = useState(false)
const name = secret?.name ?? 'No name provided'
@@ -46,6 +46,8 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
}
)
const onCloseModal = () => setModal(null)
return (
<div className="px-6 py-4 flex items-center space-x-4">
<div className="space-y-1 min-w-[35%] max-w-[35%]">
@@ -94,9 +96,10 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
{secret.updated_at === secret.created_at ? 'Added' : 'Updated'} on{' '}
{dayjs(secret.updated_at).format('MMM D, YYYY')}
</p>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="text" className="px-1" icon={<MoreVertical />} />
<Button title="Manage Secret" type="text" className="px-1" icon={<MoreVertical />} />
</DropdownMenuTrigger>
<DropdownMenuContent side="bottom" align="end" className="w-32">
<Tooltip>
@@ -104,7 +107,7 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
<DropdownMenuItem
className="space-x-2"
disabled={!canManageSecrets}
onClick={() => onSelectEdit(secret)}
onClick={() => setModal(`edit`)}
>
<Edit3 size="14" />
<p>Edit</p>
@@ -136,6 +139,8 @@ const SecretRow = ({ secret, onSelectEdit, onSelectRemove }: SecretRowProps) =>
</Tooltip>
</DropdownMenuContent>
</DropdownMenu>
<EditSecretModal visible={modal === `edit`} secret={secret} onClose={onCloseModal} />
</div>
</div>
)
@@ -9,22 +9,29 @@ import { DocsButton } from 'components/ui/DocsButton'
import { useVaultSecretsQuery } from 'data/vault/vault-secrets-query'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import type { VaultSecret } from 'types'
import { Button, Input, Listbox, Separator } from 'ui'
import {
Button,
Input,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Separator,
} from 'ui'
import AddNewSecretModal from './AddNewSecretModal'
import DeleteSecretModal from './DeleteSecretModal'
import EditSecretModal from './EditSecretModal'
import SecretRow from './SecretRow'
import type { VaultSecret } from 'types'
export const SecretsManagement = () => {
const { search } = useParams()
const { data: project } = useSelectedProjectQuery()
const [searchValue, setSearchValue] = useState<string>('')
const [selectedSort, setSelectedSort] = useState<'updated_at' | 'name'>('updated_at')
const [showAddSecretModal, setShowAddSecretModal] = useState(false)
const [selectedSecretToEdit, setSelectedSecretToEdit] = useState<VaultSecret>()
const [selectedSecretToRemove, setSelectedSecretToRemove] = useState<VaultSecret>()
const [selectedSort, setSelectedSort] = useState('updated_at')
const canManageSecrets = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
@@ -49,7 +56,7 @@ export const SecretsManagement = () => {
if (selectedSort === 'updated_at') {
return Number(new Date(s.updated_at))
} else {
return s[selectedSort]
return s[selectedSort as keyof VaultSecret]
}
}
)
@@ -81,26 +88,21 @@ export const SecretsManagement = () => {
: []
}
/>
<div className="md:w-44">
<Listbox size="tiny" value={selectedSort} onChange={setSelectedSort}>
<Listbox.Option
id="updated_at"
className="max-w-[180px]"
value="updated_at"
label="Sort by updated at"
>
<Select_Shadcn_ value={selectedSort} onValueChange={setSelectedSort}>
<SelectTrigger_Shadcn_ size="tiny" className="md:w-44">
<SelectValue_Shadcn_ asChild>
<>Sort by {selectedSort}</>
</SelectValue_Shadcn_>
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectItem_Shadcn_ value="updated_at" className="text-xs">
Updated at
</Listbox.Option>
<Listbox.Option
id="name"
className="max-w-[180px]"
value="name"
label="Sort by name"
>
</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="name" className="text-xs">
Name
</Listbox.Option>
</Listbox>
</div>
</SelectItem_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</div>
<div className="flex items-center gap-x-2">
<DocsButton href="https://supabase.com/docs/guides/database/vault" />
@@ -134,11 +136,7 @@ export const SecretsManagement = () => {
{secrets.map((secret, idx) => {
return (
<Fragment key={`secret-${idx}`}>
<SecretRow
secret={secret}
onSelectEdit={setSelectedSecretToEdit}
onSelectRemove={setSelectedSecretToRemove}
/>
<SecretRow secret={secret} onSelectRemove={setSelectedSecretToRemove} />
{idx !== secrets.length - 1 && <Separator />}
</Fragment>
)
@@ -167,10 +165,6 @@ export const SecretsManagement = () => {
</div>
</div>
<EditSecretModal
selectedSecret={selectedSecretToEdit}
onClose={() => setSelectedSecretToEdit(undefined)}
/>
<DeleteSecretModal
selectedSecret={selectedSecretToRemove}
onClose={() => setSelectedSecretToRemove(undefined)}
@@ -0,0 +1,103 @@
import { useState } from 'react'
import { describe, expect, it, beforeEach, vi } from 'vitest'
import { mockAnimationsApi } from 'jsdom-testing-mocks'
import { screen, waitFor, fireEvent } from '@testing-library/dom'
import userEvent from '@testing-library/user-event'
import { render } from 'tests/helpers'
import { addAPIMock } from 'tests/lib/msw'
import { ProjectContextProvider } from 'components/layouts/ProjectLayout/ProjectContext'
import EditSecretModal from '../EditSecretModal'
import { routerMock } from 'tests/lib/route-mock'
const secret = {
id: '47ca58b4-01c5-4a71-8814-c73856b02e0e',
name: 'test',
description: 'new text',
secret: 'NASR0SoksURJ0OorMJ9FzraTzcqSWk5u1PQa2r4c3w9rUVc=',
created_at: '2025-07-13 12:57:47.62208+00',
updated_at: '2025-07-13 14:51:37.818223+00',
}
const Page = ({ onClose }: { onClose: () => void }) => {
const [open, setOpen] = useState(false)
return (
<ProjectContextProvider projectRef="default">
<button onClick={() => setOpen(true)}>Open</button>
<EditSecretModal
visible={open}
secret={secret}
onClose={() => {
setOpen(false)
onClose()
}}
/>
</ProjectContextProvider>
)
}
mockAnimationsApi()
describe(`EditSecretModal`, () => {
beforeEach(() => {
// useSelectedProjectQuery -> useParams
routerMock.setCurrentUrl(`/project/default/integrations/vault/secrets`)
// 'http://localhost:3000/api/platform/projects/default'
addAPIMock({
method: `get`,
path: `/platform/projects/:ref`,
// @ts-expect-error
response: {
cloud_provider: 'localhost',
id: 1,
inserted_at: '2021-08-02T06:40:40.646Z',
name: 'Default Project',
organization_id: 1,
ref: 'default',
region: 'local',
status: 'ACTIVE_HEALTHY',
},
})
// 'http://localhost:3000/api/platform/pg-meta/default/query?key=projects-default-secrets-47ca58b4-01c5-4a71-8814-c73856b02e0e'
// 'http://localhost:3000/api/platform/pg-meta/default/query?key='
// useVaultSecretDecryptedValueQuery + useVaultSecretUpdateMutation
// both call the same endpoint but execute different SQL queries
addAPIMock({
method: `post`,
path: `/platform/pg-meta/:ref/query`,
// @ts-expect-error this path erroneously has a `never` return type when it should be `unknown` since it executes a SQL query
response: [{ decrypted_secret: 'bar', update_secret: '' }],
})
})
it(`renders a modal pre-filled with the secret's values`, async () => {
const onClose = vi.fn()
render(<Page onClose={onClose} />)
const openButton = screen.getByRole(`button`, { name: `Open` })
await userEvent.click(openButton)
await screen.findByRole(`dialog`)
const nameInput = screen.getByLabelText(`Name`)
const descriptionInput = screen.getByLabelText(`Description`)
const valueInput = screen.getByLabelText(`Secret value`)
const togglePasswordButton = screen.getByRole(`button`, { name: `Show secret value` })
const submitButton = screen.getByRole(`button`, { name: `Update secret` })
expect(nameInput).toHaveValue(secret.name)
expect(descriptionInput).toHaveValue(secret.description)
expect(valueInput).toHaveAttribute(`type`, `password`)
await userEvent.click(togglePasswordButton)
expect(valueInput).toHaveAttribute(`type`, `text`)
await userEvent.type(nameInput, `updated-name`)
await userEvent.clear(descriptionInput)
await userEvent.type(valueInput, `qux`)
fireEvent.click(submitButton)
await waitFor(() => expect(onClose).toHaveBeenCalledOnce())
})
})
@@ -1,5 +1,4 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useMemo, useState } from 'react'
import { toast } from 'sonner'
@@ -24,8 +23,6 @@ import {
Textarea,
} from 'ui'
dayjs.extend(relativeTime)
const RSA_JWK_REQUIRED_PROPERTIES = ['kty', 'n', 'e', 'p', 'q', 'd', 'dq', 'dp', 'qi']
const EC_JWK_REQUIRED_PROPERTIES = ['kty', 'crv', 'x', 'y', 'd']
const ALLOWED_JWK_PROPERTIES = new Set([
@@ -21,7 +21,7 @@ import {
} from 'data/table-editor/table-editor-types'
import { useTableUpdateMutation } from 'data/tables/table-update-mutation'
import { useSendEventMutation } from 'data/telemetry/send-event-mutation'
import { useCheckPermissions } from 'hooks/misc/useCheckPermissions'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useSelectedOrganizationQuery } from 'hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
@@ -107,9 +107,13 @@ const GridHeaderActions = ({ table }: GridHeaderActionsProps) => {
},
})
const canSqlWriteTables = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const canSqlWriteColumns = useCheckPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'columns')
const isReadOnly = !canSqlWriteTables && !canSqlWriteColumns
const { can: canSqlWriteTables, isLoading: isLoadingPermissions } =
useAsyncCheckProjectPermissions(PermissionAction.TENANT_SQL_ADMIN_WRITE, 'tables')
const { can: canSqlWriteColumns } = useAsyncCheckProjectPermissions(
PermissionAction.TENANT_SQL_ADMIN_WRITE,
'columns'
)
const isReadOnly = !isLoadingPermissions && !canSqlWriteTables && !canSqlWriteColumns
// This will change when we allow autogenerated API docs for schemas other than `public`
const doesHaveAutoGeneratedAPIDocs = table.schema === 'public'
@@ -1,5 +1,4 @@
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { AnimatePresence, motion } from 'framer-motion'
import Link from 'next/link'
@@ -9,8 +8,6 @@ import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { editorEntityTypes, useTabsStateSnapshot } from 'state/tabs'
import { useEditorType } from '../editors/EditorsLayout.hooks'
dayjs.extend(relativeTime)
export function RecentItems() {
const { ref } = useParams()
const tabs = useTabsStateSnapshot()
@@ -218,7 +218,6 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
onFinish: handleChatFinish,
})
const formattedError = tryParseJson(error?.message ?? {})
const isChatLoading = chatStatus === 'submitted' || chatStatus === 'streaming'
const updateMessage = useCallback(
@@ -446,10 +445,6 @@ export const AIAssistant = ({ className }: AIAssistantProps) => {
Sorry, I'm having trouble responding right now. If the error persists while
retrying, you may try clearing the conversation's messages and try again.
</p>
<p className="text-foreground-lighter text-xs mt-1">
Error: {formattedError.message}
</p>
</div>
</div>
<div className="flex items-center gap-x-2">
@@ -106,7 +106,7 @@ export const getContextualInvalidationKeys = ({
}
export const onErrorChat = (error: Error) => {
const parsedError = tryParseJson(error.message)
const parsedError = error ? tryParseJson(error.message) : undefined
try {
handleError(parsedError?.error || parsedError || error)
@@ -1,6 +1,7 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { JwtSecretUpdateStatus } from '@supabase/shared-types/out/events'
import { AlertCircle, Loader2 } from 'lucide-react'
import Link from 'next/link'
import { useMemo } from 'react'
import { toast } from 'sonner'
@@ -9,8 +10,6 @@ import Panel from 'components/ui/Panel'
import { useJwtSecretUpdatingStatusQuery } from 'data/config/jwt-secret-updating-status-query'
import { useProjectSettingsV2Query } from 'data/config/project-settings-v2-query'
import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'
import { useFlag } from 'hooks/ui/useFlag'
import Link from 'next/link'
import { Input } from 'ui'
import { getLastUsedAPIKeys, useLastUsedAPIKeysLogQuery } from './DisplayApiSettings.utils'
import { ToggleLegacyApiKeysPanel } from './ToggleLegacyApiKeys'
@@ -26,8 +25,6 @@ export const DisplayApiSettings = ({
}) => {
const { ref: projectRef } = useParams()
const newApiKeysInRollOut = useFlag('basicApiKeys')
const {
data: settings,
isError: isProjectSettingsError,
@@ -217,31 +214,20 @@ export const DisplayApiSettings = ({
))
)}
{showNotice ? (
newApiKeysInRollOut ? (
<Panel.Notice
className="border-t"
title="API keys have moved"
badgeLabel={'Changelog'}
description={`
<Panel.Notice
className="border-t"
title="API keys have moved"
badgeLabel="Changelog"
description={`
\`anon\` and \`service_role\` API keys can now be replaced with \`publishable\` and \`secret\` API keys.
`}
href="https://github.com/orgs/supabase/discussions/29260"
buttonText="Read the announcement"
/>
) : (
<Panel.Notice
className="border-t"
title="New API keys coming Q2 2025"
description={`
\`anon\` and \`service_role\` API keys will be changing to \`publishable\` and \`secret\` API keys.
`}
href="https://github.com/orgs/supabase/discussions/29260"
buttonText="Read the announcement"
/>
)
) : null}
href="https://github.com/orgs/supabase/discussions/29260"
buttonText="Read the announcement"
/>
) : (
<ToggleLegacyApiKeysPanel />
)}
</Panel>
{newApiKeysInRollOut && !showNotice && <ToggleLegacyApiKeysPanel />}
</>
)
}
@@ -1,13 +1,8 @@
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useRef } from 'react'
import useLogsQuery from 'hooks/analytics/useLogsQuery'
dayjs.extend(duration)
dayjs.extend(relativeTime)
export function useLastUsedAPIKeysLogQuery(projectRef: string) {
const now = useRef(new Date()).current
return useLogsQuery(projectRef, {
@@ -1,60 +0,0 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import type { ResponseError } from 'types'
import { replicationKeys } from './keys'
import { handleError, del } from 'data/fetchers'
export type DeleteDestinationParams = {
projectRef: string
destinationId: number
}
async function deleteDestination(
{ projectRef, destinationId: destinationId }: DeleteDestinationParams,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
const { data, error } = await del('/platform/replication/{ref}/destinations/{destination_id}', {
params: { path: { ref: projectRef, destination_id: destinationId } },
signal,
})
if (error) {
handleError(error)
}
return data
}
type DeleteDestinationData = Awaited<ReturnType<typeof deleteDestination>>
export const useDeleteDestinationMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<DeleteDestinationData, ResponseError, DeleteDestinationParams>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<DeleteDestinationData, ResponseError, DeleteDestinationParams>(
(vars) => deleteDestination(vars),
{
async onSuccess(data, variables, context) {
const { projectRef, destinationId } = variables
await queryClient.invalidateQueries(replicationKeys.destinations(projectRef))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to delete destination: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
@@ -0,0 +1,64 @@
import { useMutation, UseMutationOptions, useQueryClient } from '@tanstack/react-query'
import { toast } from 'sonner'
import type { ResponseError } from 'types'
import { replicationKeys } from './keys'
import { handleError, del } from 'data/fetchers'
export type DeleteDestinationPipelineParams = {
projectRef: string
destinationId: number
pipelineId: number
}
async function deleteDestinationPipeline(
{ projectRef, destinationId, pipelineId }: DeleteDestinationPipelineParams,
signal?: AbortSignal
) {
if (!projectRef) throw new Error('projectRef is required')
const { data, error } = await del(
'/platform/replication/{ref}/destinations-pipelines/{destination_id}/{pipeline_id}',
{
params: { path: { ref: projectRef, destination_id: destinationId, pipeline_id: pipelineId } },
signal,
}
)
if (error) {
handleError(error)
}
return data
}
type DeleteDestinationPipelineData = Awaited<ReturnType<typeof deleteDestinationPipeline>>
export const useDeleteDestinationPipelineMutation = ({
onSuccess,
onError,
...options
}: Omit<
UseMutationOptions<DeleteDestinationPipelineData, ResponseError, DeleteDestinationPipelineParams>,
'mutationFn'
> = {}) => {
const queryClient = useQueryClient()
return useMutation<DeleteDestinationPipelineData, ResponseError, DeleteDestinationPipelineParams>(
(vars) => deleteDestinationPipeline(vars),
{
async onSuccess(data, variables, context) {
const { projectRef } = variables
await queryClient.invalidateQueries(replicationKeys.destinations(projectRef))
await onSuccess?.(data, variables, context)
},
async onError(data, variables, context) {
if (onError === undefined) {
toast.error(`Failed to delete destination and pipeline: ${data.message}`)
} else {
onError(data, variables, context)
}
},
...options,
}
)
}
+6 -6
View File
@@ -612,12 +612,12 @@ export const getConnectionLimits = (computeSize: string = 'medium') => {
small: { direct: 90, pooler: 400 },
medium: { direct: 120, pooler: 600 },
large: { direct: 160, pooler: 800 },
xl: { direct: 240, pooler: 1000 },
'2xl': { direct: 380, pooler: 1500 },
'4xl': { direct: 480, pooler: 3000 },
'8xl': { direct: 490, pooler: 6000 },
'12xl': { direct: 500, pooler: 9000 },
'16xl': { direct: 500, pooler: 12000 },
xlarge: { direct: 240, pooler: 1000 },
'2xlarge': { direct: 380, pooler: 1500 },
'4xlarge': { direct: 480, pooler: 3000 },
'8xlarge': { direct: 490, pooler: 6000 },
'12xlarge': { direct: 500, pooler: 9000 },
'16xlarge': { direct: 500, pooler: 12000 },
}
return (
@@ -134,7 +134,10 @@ export function useGetProjectPermissions(
}
/**
* @deprecated Use useAsyncCheckProjectPermissions instead
* @deprecated If checking for project permissions, use useAsyncCheckProjectPermissions instead so that we can always
* check for loading states to not prematurely show "no perms" UIs. We'll also need a separate async check for org perms too
*
* Use `import { useAsyncCheckProjectPermissions } from 'hooks/misc/useCheckPermissions'` instead
*/
export function useCheckPermissions(
action: string,
+2
View File
@@ -24,6 +24,7 @@ import { Hydrate, QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import duration from 'dayjs/plugin/duration'
import relativeTime from 'dayjs/plugin/relativeTime'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
@@ -56,6 +57,7 @@ dayjs.extend(customParseFormat)
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(relativeTime)
dayjs.extend(duration)
loader.config({
// [Joshen] Attempt for offline support/bypass ISP issues is to store the assets required for monaco
+1 -1
View File
@@ -83,7 +83,7 @@ async function handlePost(req: NextApiRequest, res: NextApiResponse) {
}
// [Joshen] Am also filtering out any tool calls which state is "input-streaming"
// this happens when a user stops the assistant response while the tool is being called
if (msg && msg.role === 'assistant') {
if (msg && msg.role === 'assistant' && msg.parts) {
const cleanedParts = msg.parts.filter((part: any) => {
return !(part.type.startsWith('tool-') && part.state === 'input-streaming')
})
+11 -13
View File
@@ -90,18 +90,7 @@ import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { InfoTooltip } from 'ui-patterns/info-tooltip'
const sizes: DesiredInstanceSize[] = [
'micro',
'small',
'medium',
'large',
'xlarge',
'2xlarge',
'4xlarge',
'8xlarge',
'12xlarge',
'16xlarge',
]
const sizes: DesiredInstanceSize[] = ['micro', 'small', 'medium']
const sizesWithNoCostConfirmationRequired: DesiredInstanceSize[] = ['micro', 'small']
@@ -783,7 +772,7 @@ const Wizard: NextPageWithLayout = () => {
CPU
</span>
<p
className="text-xs text-muted instance-details"
className="text-xs text-foreground-light instance-details"
translate="no"
>
${instanceSizeSpecs[option].priceHourly}/hour (~$
@@ -794,6 +783,15 @@ const Wizard: NextPageWithLayout = () => {
</SelectItem_Shadcn_>
)
})}
<SelectItem_Shadcn_
key={'disabled'}
value={'disabled'}
disabled
>
<div className="flex items-center justify-center w-full">
<span>Larger instance sizes available after creation</span>
</div>
</SelectItem_Shadcn_>
</SelectGroup_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
@@ -1,21 +1,10 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { AuthProvidersForm, BasicAuthSettingsForm } from 'components/interfaces/Auth'
import { AuthProvidersLayout } from 'components/layouts/AuthLayout/AuthProvidersLayout'
import DefaultLayout from 'components/layouts/DefaultLayout'
import { ScaffoldContainer } from 'components/layouts/Scaffold'
import NoPermission from 'components/ui/NoPermission'
import { useCheckPermissions, usePermissionsLoaded } from 'hooks/misc/useCheckPermissions'
import type { NextPageWithLayout } from 'types'
const ProvidersPage: NextPageWithLayout = () => {
const canReadAuthSettings = useCheckPermissions(PermissionAction.READ, 'custom_config_gotrue')
const isPermissionsLoaded = usePermissionsLoaded()
if (isPermissionsLoaded && !canReadAuthSettings) {
return <NoPermission isFullPage resourceText="access your project's auth provider settings" />
}
return (
<ScaffoldContainer>
<BasicAuthSettingsForm />
@@ -1,5 +1,4 @@
import {
ApiKeysComingSoonBanner,
ApiKeysCreateCallout,
ApiKeysFeedbackBanner,
} from 'components/interfaces/APIKeys/ApiKeysIllustrations'
@@ -14,11 +13,10 @@ import type { NextPageWithLayout } from 'types'
import { Separator } from 'ui'
const ApiKeysNewPage: NextPageWithLayout = () => {
const { isInRollout, shouldDisableUI, canInitApiKeys } = useApiKeysVisibility()
const { shouldDisableUI, canInitApiKeys } = useApiKeysVisibility()
return (
<>
{!isInRollout && <ApiKeysComingSoonBanner />}
{canInitApiKeys && <ApiKeysCreateCallout />}
<ApiKeysFeedbackBanner />
<DisableInteraction disabled={shouldDisableUI} className="flex flex-col gap-8">
+8 -5
View File
@@ -44,11 +44,14 @@ export const addAPIMock = <P extends Endpoints | `${Endpoints}?${string}`, M ext
method,
path,
response,
}: {
method: M
path: P
response: SuccessResponse<TrimQueryParams<P>, M> | HttpResponseResolver
}) => {
}: SuccessResponse<TrimQueryParams<P>, M> extends never
? // Don't require a mocked response when the API doesn't return one
{ method: M; path: P; response?: never }
: {
method: M
path: P
response: SuccessResponse<TrimQueryParams<P>, M> | HttpResponseResolver
}) => {
const fullPath = `${API_URL}${path}`
console.log('[MSW] Adding mock:', method.toUpperCase(), fullPath)
+21 -6
View File
@@ -1,11 +1,26 @@
import { glob } from 'fs/promises'
import _routerMock from 'next-router-mock'
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes'
import { normalize, posix, sep, join } from 'path'
export const routerMock = _routerMock
routerMock.useParser(
createDynamicRouteParser([
// These paths should match those found in the `/pages` folder:
'/projects/[ref]',
])
)
const pipe =
<T>(...fns: Array<(arg: T) => T>) =>
(value: T) =>
fns.reduce((acc, fn) => fn(acc), value)
// normalize file paths to posix format (windows FS support)
const toPosix = (str: string) => str.replaceAll(sep, posix.sep)
const pagesRoot = toPosix(join(process.cwd(), `./pages`))
// remove the base filepath
const removePrefix = (str: string) => str.replace(pagesRoot, ``)
// remove the file extension or index segment
const removeExt = (str: string) => str.replace(/.tsx|\/index.tsx/, ``)
// Glob the `pages/` directory for all dynamic route segments
const paths = [
...(await Array.fromAsync(glob(`${pagesRoot}/**/*\].tsx`))),
...(await Array.fromAsync(glob(`${pagesRoot}/**/**\]/**/index.tsx`))),
].map(pipe(normalize, toPosix, removePrefix, removeExt))
routerMock.useParser(createDynamicRouteParser(paths))
+2
View File
@@ -35,3 +35,5 @@ Object.defineProperties(globalThis, {
ReadableStream: { value: ReadableStream },
TransformStream: { value: TransformStream },
})
window.HTMLElement.prototype.hasPointerCapture = vi.fn()
+46 -4
View File
@@ -3434,7 +3434,8 @@ export interface paths {
put?: never
/** Update a replication destination and pipeline. */
post: operations['ReplicationDestinationsPipelinesController_updateDestinationPipeline']
delete?: never
/** Delete a replication destination and pipeline. */
delete: operations['ReplicationDestinationsPipelinesController_deleteDestinationPipeline']
options?: never
head?: never
patch?: never
@@ -4808,6 +4809,8 @@ export interface components {
}
| {
billing_email: string | null
/** @enum {string|null} */
billing_partner: 'fly' | 'aws' | 'vercel_marketplace' | null
id: number
is_owner: boolean
name: string
@@ -6582,6 +6585,8 @@ export interface components {
}
OrganizationResponse: {
billing_email: string | null
/** @enum {string|null} */
billing_partner: 'fly' | 'aws' | 'vercel_marketplace' | null
id: number
is_owner: boolean
name: string
@@ -6634,9 +6639,8 @@ export interface components {
}
OrganizationSlugResponse: {
billing_email: string | null
billing_metadata: {
[key: string]: unknown
} | null
/** @enum {string|null} */
billing_partner: 'fly' | 'aws' | 'vercel_marketplace' | null
has_oriole_project: boolean
id: number
name: string
@@ -19126,6 +19130,44 @@ export interface operations {
}
}
}
ReplicationDestinationsPipelinesController_deleteDestinationPipeline: {
parameters: {
query?: never
header?: never
path: {
/** @description Destination id */
destination_id: number
/** @description Pipeline id */
pipeline_id: number
/** @description Project reference */
ref: string
}
cookie?: never
}
requestBody?: never
responses: {
/** @description Returned when the replication destination and pipeline are deleted. */
201: {
headers: {
[name: string]: unknown
}
content?: never
}
403: {
headers: {
[name: string]: unknown
}
content?: never
}
/** @description Returned when the API fails to delete the replication destination or pipeline. */
500: {
headers: {
[name: string]: unknown
}
content?: never
}
}
}
ReplicationDestinationsController_getDestination: {
parameters: {
query?: never