mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
Merge remote-tracking branch 'origin/master' into kemal/design-249-time-series-queries-exploration
This commit is contained in:
commit
b9d01fa360
57 files changed
+974
-682
No files matched your search
+1
-1
@@ -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'
|
||||
)
|
||||
|
||||
+223
-194
@@ -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'
|
||||
|
||||
+6
-3
@@ -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 })
|
||||
|
||||
+9
-3
@@ -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'
|
||||
|
||||
+23
-32
@@ -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
|
||||
|
||||
+9
-3
@@ -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)
|
||||
}
|
||||
|
||||
+176
-127
@@ -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>
|
||||
)
|
||||
|
||||
+27
-33
@@ -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)}
|
||||
|
||||
+103
@@ -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())
|
||||
})
|
||||
})
|
||||
-3
@@ -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,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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))
|
||||
@@ -35,3 +35,5 @@ Object.defineProperties(globalThis, {
|
||||
ReadableStream: { value: ReadableStream },
|
||||
TransformStream: { value: TransformStream },
|
||||
})
|
||||
|
||||
window.HTMLElement.prototype.hasPointerCapture = vi.fn()
|
||||
+46
-4
@@ -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
|
||||
|
||||
Reference in new issue
Block a user