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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion apps/docs/content/guides/deployment/branching/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,59 @@ secret = "env(SUPABASE_AUTH_EXTERNAL_GITHUB_SECRET)"

<Admonition type="note" label="Secret fields">

The `encrypted:` syntax only works for designated "secret" fields in the configuration (like `secret` in auth providers). Using encrypted values in other fields will not be automatically decrypted and may cause issues. For non-secret fields, use environment variables with the `env()` syntax instead.
The `encrypted:` syntax only works for designated "secret" fields in the configuration. Using encrypted values in other fields will not be automatically decrypted and may cause issues. For non-secret fields, use environment variables with the `env()` syntax instead.

The following fields support the `encrypted:` syntax:

**Studio**

- `studio.openai_api_key`

**Database**

- `db.root_key`
- `db.vault.*` (any key in the vault map)

**Auth - Core Keys**

- `auth.publishable_key`
- `auth.secret_key`
- `auth.jwt_secret`
- `auth.anon_key`
- `auth.service_role_key`

**Auth - Email (SMTP)**

- `auth.email.smtp.pass`

**Auth - Captcha**

- `auth.captcha.secret`

**Auth - Hooks**

- `auth.hook.mfa_verification_attempt.secrets`
- `auth.hook.password_verification_attempt.secrets`
- `auth.hook.custom_access_token.secrets`
- `auth.hook.send_sms.secrets`
- `auth.hook.send_email.secrets`
- `auth.hook.before_user_created.secrets`

**Auth - SMS Providers**

- `auth.sms.twilio.auth_token`
- `auth.sms.twilio_verify.auth_token`
- `auth.sms.messagebird.access_key`
- `auth.sms.textlocal.api_key`
- `auth.sms.vonage.api_secret`

**Auth - External OAuth Providers**

- `auth.external.*.secret`

**Edge Runtime**

- `edge_runtime.secrets.*` (any key in the secrets map)

</Admonition>

Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { useQueryClient } from '@tanstack/react-query'
import { isEmpty } from 'lodash'
import { compact, isEmpty, mapValues } from 'lodash'
import { Edit, Trash } from 'lucide-react'
import { useCallback, useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'

import { UUID_REGEX } from '@/lib/constants'
import { FormSection, FormSectionContent, FormSectionLabel } from 'components/ui/Forms/FormSection'
import { invalidateSchemasQuery } from 'data/database/schemas-query'
import { useFDWUpdateMutation } from 'data/fdw/fdw-update-mutation'
import { FDW } from 'data/fdw/fdws-query'
import { getDecryptedValue } from 'data/vault/vault-secret-decrypted-value-query'
import { useVaultSecretsQuery } from 'data/vault/vault-secrets-query'
import { getDecryptedValues } from 'data/vault/vault-secret-decrypted-value-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import { useConfirmOnClose, type ConfirmOnCloseModalProps } from 'hooks/ui/useConfirmOnClose'
import { Button, Form, Input, SheetFooter, SheetHeader, SheetTitle } from 'ui'
Expand Down Expand Up @@ -44,11 +44,6 @@ export const EditWrapperSheet = ({
const queryClient = useQueryClient()
const { data: project } = useSelectedProjectQuery()

const { data: secrets, isPending: isSecretsLoading } = useVaultSecretsQuery({
projectRef: project?.ref,
connectionString: project?.connectionString,
})

const { mutate: updateFDW, isPending: isSaving } = useFDWUpdateMutation({
onSuccess: () => {
toast.success(`Successfully updated ${wrapperMeta?.label} foreign data wrapper`)
Expand All @@ -75,7 +70,7 @@ export const EditWrapperSheet = ({
...convertKVStringArrayToJson(wrapper?.server_options ?? []),
}

const onUpdateTable = (values: any) => {
const onUpdateTable = (values: FormattedWrapperTable) => {
setWrapperTables((prev) => {
// if the new values have tableIndex, we are editing an existing table
if (values.tableIndex !== undefined) {
Expand All @@ -91,9 +86,9 @@ export const EditWrapperSheet = ({
setSelectedTableToEdit(undefined)
}

const onSubmit = async (values: any) => {
const onSubmit = async (values: Record<string, string>) => {
const validate = makeValidateRequired(wrapperMeta.server.options)
const errors: any = validate(values)
const errors = validate(values)

const { wrapper_name } = values
if (wrapper_name.length === 0) errors.name = 'Please provide a name for your wrapper'
Expand Down Expand Up @@ -137,7 +132,15 @@ export const EditWrapperSheet = ({
onSubmit={onSubmit}
className="h-full flex flex-col"
>
{({ values, initialValues, resetForm }: any) => {
{({
values,
initialValues,
resetForm,
}: {
values: Record<string, string>
initialValues: Record<string, string>
resetForm: (value: Record<string, Record<string, string>>) => void
}) => {
// [Alaister] although this "technically" is breaking the rules of React hooks
// it won't error because the hooks are always rendered in the same order
// eslint-disable-next-line react-hooks/rules-of-hooks
Expand All @@ -152,50 +155,60 @@ export const EditWrapperSheet = ({
const hasChanges = hasFormChanges || hasTableChanges
hasChangesRef.current = hasChanges

const encryptedOptions = wrapperMeta.server.options.filter((option) => option.encrypted)

// [Alaister] although this "technically" is breaking the rules of React hooks
// it won't error because the hooks are always rendered in the same order
// eslint-disable-next-line react-hooks/rules-of-hooks
useEffect(() => {
const fetchEncryptedValues = async () => {
setLoadingSecrets(true)
// If the secrets haven't loaded, escape and run the effect again when they're loaded
if (isSecretsLoading) {
return
}
const fetchEncryptedValues = async (ids: string[]) => {
try {
setLoadingSecrets(true)
// If the secrets haven't loaded, escape and run the effect again when they're loaded
const decryptedValues = await getDecryptedValues({
projectRef: project?.ref,
connectionString: project?.connectionString,
ids: ids,
})

const res = await Promise.all(
encryptedOptions.map(async (option) => {
const secret = secrets?.find(
(secret) => secret.name === `${wrapper.name}_${option.name}`
)
if (secret !== undefined) {
const value = await getDecryptedValue({
projectRef: project?.ref,
connectionString: project?.connectionString,
id: secret.id,
})
return { [option.name]: value[0]?.decrypted_secret ?? '' }
} else {
return { [option.name]: '' }
}
// replace all values which are in the decryptedValues object with the decrypted value
const transformValues = (values: Record<string, string>) => {
return mapValues(values, (value) => {
return decryptedValues[value] ?? value
})
}

resetForm({
values: transformValues(values),
initialValues: transformValues(initialValues),
})
)
const secretValues = res.reduce((a: any, b: any) => {
const [key] = Object.keys(b)
return { ...a, [key]: b[key] }
}, {})
} catch (error) {
toast.error('Failed to fetch encrypted values')
} finally {
setLoadingSecrets(false)
}
}

resetForm({
values: { ...values, ...secretValues },
initialValues: { ...initialValues, ...secretValues },
const encryptedOptions = wrapperMeta.server.options.filter(
(option) => option.encrypted
)

const encryptedIdsToFetch = compact(
encryptedOptions.map((option) => {
const value = initialValues[option.name]
return value ?? null
})
setLoadingSecrets(false)
}
).filter((x) => UUID_REGEX.test(x))
// [Joshen] ^ Validate UUID to filter out already decrypted values

if (encryptedOptions.length > 0) fetchEncryptedValues()
}, [isSecretsLoading])
if (encryptedIdsToFetch.length > 0) {
fetchEncryptedValues(encryptedIdsToFetch)
}
/**
* [Joshen] We're deliberately not adding values and initialValues to the dependency array here
* as we only want to fetch the encrypted values once on load + values and initialValues will be updated
* as a result of that
*/
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [project?.ref, project?.connectionString])

return (
<>
Expand Down Expand Up @@ -279,8 +292,7 @@ export const EditWrapperSheet = ({
Target: {target}
</p>
<p className="text-sm text-foreground-light">
Columns:{' '}
{table.columns.map((column: any) => column.name).join(', ')}
Columns: {table.columns.map((column) => column.name).join(', ')}
</p>
</div>
<div className="flex items-center space-x-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,15 @@ import { formatWrapperTables } from './Wrappers.utils'

interface WrapperRowProps {
wrapper: FDW
wrappers: FDW[]
selectedWrapperToEdit?: FDW
selectedWrapperToDelete?: FDW
setSelectedWrapperToEdit: (value: string | null) => void
setSelectedWrapperToDelete: (value: string | null) => void
deletingWrapperIdRef: MutableRefObject<string | null>
}

const WrapperRow = ({
export const WrapperRow = ({
wrapper,
wrappers,
selectedWrapperToEdit,
selectedWrapperToDelete,
setSelectedWrapperToEdit,
Expand Down Expand Up @@ -88,17 +86,20 @@ const WrapperRow = ({

<TableCell className="space-y-2 !p-4">
{_tables?.map((table) => {
const target = table.table ?? table.object
const target = table.table ?? table.object ?? table.src_key

return (
<div key={table.id} className="flex items-center -space-x-3">
<Badge className="bg-surface-300 bg-opacity-100 pr-1 gap-2 font-mono text-[0.75rem] h-6 text-foreground">
<div key={table.id} className="flex items-center">
<Badge className="bg-surface-300 bg-opacity-100 gap-2 font-mono text-[0.75rem] h-6 text-foreground rounded-r-none">
<div className="relative w-3 h-3 flex items-center justify-center">
{integration.icon({ className: 'p-0' })}
</div>
<Tooltip>
<TooltipTrigger className="truncate max-w-28">{target}</TooltipTrigger>
<TooltipContent className="max-w-64 whitespace-pre-wrap break-words">
<TooltipContent
side="bottom"
className="max-w-64 whitespace-pre-wrap break-words"
>
{target}
</TooltipContent>
</Tooltip>
Expand All @@ -110,13 +111,16 @@ const WrapperRow = ({
</Badge>

<Link href={`/project/${ref}/editor/${table.id}`}>
<Badge className="transition hover:bg-surface-300 pl-5 rounded-l-none gap-2 h-6 font-mono text-[0.75rem] border-l-0">
<Badge className="transition hover:bg-surface-300 px-2 rounded-l-none gap-1.5 h-6 font-mono text-[0.75rem] border-l-0">
<Table2 size={12} strokeWidth={1.5} className="text-foreground-lighter/50" />
<Tooltip>
<TooltipTrigger className="truncate max-w-28">
{table.schema}.{table.table_name}
</TooltipTrigger>
<TooltipContent className="max-w-64 whitespace-pre-wrap break-words">
<TooltipContent
side="bottom"
className="max-w-64 whitespace-pre-wrap break-words"
>
{table.schema}.{table.table_name}
</TooltipContent>
</Tooltip>
Expand All @@ -129,7 +133,6 @@ const WrapperRow = ({
<TableCell>
{encryptedMetadata.map((metadata) => (
<div key={metadata.name} className="flex items-center space-x-2 text-sm">
{/* <p className="text-foreground-light">{metadata.label}:</p> */}
<Link
href={`/project/${ref}/settings/vault/secrets?search=${wrapper.name}_${metadata.name}`}
className="transition text-foreground-light hover:text-foreground flex items-center space-x-2 max-w-28"
Expand Down Expand Up @@ -212,5 +215,3 @@ const WrapperRow = ({
</>
)
}

export default WrapperRow
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
TableRow,
} from 'ui'
import { INTEGRATIONS } from '../Landing/Integrations.constants'
import WrapperRow from './WrapperRow'
import { WrapperRow } from './WrapperRow'
import { wrapperMetaComparator } from './Wrappers.utils'

interface WrapperTableProps {
Expand Down Expand Up @@ -91,7 +91,6 @@ export const WrapperTable = ({ isLatest = false }: WrapperTableProps) => {
<WrapperRow
key={x.id}
wrapper={x}
wrappers={wrappers}
selectedWrapperToEdit={selectedWrapperToEdit}
selectedWrapperToDelete={selectedWrapperToDelete}
setSelectedWrapperToEdit={setSelectedWrapperToEdit}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@ import {
Form_Shadcn_,
Input_Shadcn_,
PrePostTab,
Separator,
Skeleton,
Switch,
WarningIcon,
cn,
} from 'ui'
import { GenericSkeletonLoader } from 'ui-patterns'
import { Admonition } from 'ui-patterns/admonition'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
// This will change when we allow autogenerated API docs for schemas other than `public`
const doesHaveAutoGeneratedAPIDocs = table.schema === 'public'

const { hasLint: tableHasLints } = getEntityLintDetails(
table.name,
'rls_disabled_in_public',
['ERROR'],
lints,
table.schema
)

const { hasLint: viewHasLints, matchingLint: matchingViewLint } = getEntityLintDetails(
table.name,
'security_definer_view',
Expand Down Expand Up @@ -301,7 +309,7 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
</Button>
)}
</>
) : (
) : tableHasLints ? (
<Popover_Shadcn_ modal={false} open={showWarning} onOpenChange={setShowWarning}>
<PopoverTrigger_Shadcn_ asChild>
<Button type="danger" icon={<Lock strokeWidth={1.5} />}>
Expand Down Expand Up @@ -338,7 +346,7 @@ export const GridHeaderActions = ({ table, isRefetching }: GridHeaderActionsProp
</div>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
) : null
) : null}

{isTable && isIndexAdvisorAvailable && !isIndexAdvisorEnabled && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type { ForeignKeyConstraint } from 'data/database/foreign-key-constraints
import { databaseKeys } from 'data/database/keys'
import { ENTITY_TYPE } from 'data/entity-types/entity-type-constants'
import { entityTypeKeys } from 'data/entity-types/keys'
import { lintKeys } from 'data/lint/keys'
import { privilegeKeys } from 'data/privileges/keys'
import { tableEditorKeys } from 'data/table-editor/keys'
import { isTableLike } from 'data/table-editor/table-editor-types'
Expand Down Expand Up @@ -530,6 +531,7 @@ export const SidePanelEditor = ({
queryClient.invalidateQueries({
queryKey: privilegeKeys.tablePrivilegesList(project?.ref),
}),
queryClient.invalidateQueries({ queryKey: lintKeys.lint(project?.ref) }),
])

// Show success toast after everything is complete
Expand Down Expand Up @@ -583,6 +585,7 @@ export const SidePanelEditor = ({
queryClient.invalidateQueries({
queryKey: privilegeKeys.tablePrivilegesList(project?.ref),
}),
queryClient.invalidateQueries({ queryKey: lintKeys.lint(project?.ref) }),
])

toast.success(
Expand Down
Loading
Loading