Files
Monica Khoury be9c67a761 fix: warn that resetting the DB password affects pooler and read replicas (#50649)
## What kind of change does this PR introduce?

Fixes:
[FE-3607](https://linear.app/supabase/issue/FE-3607/re-proper-ui-warning-when-changing-db-password).

## What is the current behavior?

`ResetDbPasswordDialog` (the "Reset database password" dialog, reachable
from Database Settings and the Connect sheet) only validates password
strength. It has no warning about what actually happens when the
password changes: the same password is shared across the direct
connection, the pooler, read replicas, and third-party integrations
(Warehouse, ORMs, backend services) that have it configured. Resetting
it silently breaks any of those still using the old value.

## What is the new behavior?

Adds an `Admonition` warning inside the shared `ResetDbPasswordDialog`
component explaining that the pooler, read replicas, and any
app/ORM/tool using the old password will be disconnected. Added to the
shared dialog itself (not the Database Settings page wrapper) so both
places it's embedded, the Database Settings page and the Connect sheet's
direct-connection step, get the warning automatically.

## Additional context

No behavior change to the reset flow itself, copy-only addition. No new
tests added since this doesn't introduce new logic/branches.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **Improvements**
- Added a warning to the database password reset dialog explaining that
the password is shared across all connection methods.
- Clarified that resetting the password disconnects the pooler, read
replicas, and applications, ORMs, or tools using the previous password.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-21 17:13:33 +03:00

204 lines
6.9 KiB
TypeScript

import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import type { ChangeEvent, ComponentProps, ReactNode } from 'react'
import { useEffect, useRef, useState } from 'react'
import { toast } from 'sonner'
import {
Button,
Dialog,
DialogContent,
DialogFooter,
DialogHeader,
DialogSection,
DialogSectionSeparator,
DialogTitle,
DialogTrigger,
} from 'ui'
import { Admonition } from 'ui-patterns/Admonition'
import { Input } from 'ui-patterns/DataInputs/Input'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
import { PasswordStrengthBar } from '@/components/ui/PasswordStrengthBar'
import { useDatabasePasswordResetMutation } from '@/data/database/database-password-reset-mutation'
import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions'
import { useIsProjectActive, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { DEFAULT_MINIMUM_PASSWORD_STRENGTH } from '@/lib/constants'
import { passwordStrength, PasswordStrengthScore } from '@/lib/password-strength'
import { generateStrongPassword } from '@/lib/project'
export type ResetDbPasswordDialogProps = {
disabled?: boolean
onPasswordReset?: (password: string) => void
triggerClassName?: string
triggerIcon?: ReactNode
triggerLabel?: string
triggerVariant?: ComponentProps<typeof ButtonTooltip>['variant']
}
export const ResetDbPasswordDialog = ({
disabled = false,
onPasswordReset,
triggerClassName,
triggerIcon,
triggerLabel = 'Reset password',
triggerVariant = 'default',
}: ResetDbPasswordDialogProps) => {
const { ref } = useParams()
const isProjectActive = useIsProjectActive()
const { data: project } = useSelectedProjectQuery()
const { can: canResetDbPassword } = useAsyncCheckPermissions(
PermissionAction.UPDATE,
'projects',
{
resource: {
project_id: project?.id,
},
}
)
const [showResetDbPass, setShowResetDbPass] = useState<boolean>(false)
const [password, setPassword] = useState<string>('')
const [passwordStrengthMessage, setPasswordStrengthMessage] = useState<string>('')
const [passwordStrengthWarning, setPasswordStrengthWarning] = useState<string>('')
const [passwordStrengthScore, setPasswordStrengthScore] = useState(0)
const latestPasswordStrengthValueRef = useRef('')
const passwordStrengthResultValueRef = useRef('')
const { mutate: resetDatabasePassword, isPending: isUpdatingPassword } =
useDatabasePasswordResetMutation({
onSuccess: async (_data, variables) => {
toast.success('Successfully updated database password')
onPasswordReset?.(variables.password)
setShowResetDbPass(false)
},
})
useEffect(() => {
if (showResetDbPass) {
setPassword('')
setPasswordStrengthMessage('')
setPasswordStrengthWarning('')
setPasswordStrengthScore(0)
latestPasswordStrengthValueRef.current = ''
passwordStrengthResultValueRef.current = ''
}
}, [showResetDbPass])
async function checkPasswordStrength(value: string) {
latestPasswordStrengthValueRef.current = value
const { message, warning, strength } = await passwordStrength(value)
if (latestPasswordStrengthValueRef.current !== value) return
passwordStrengthResultValueRef.current = value
setPasswordStrengthScore(strength)
setPasswordStrengthWarning(warning)
setPasswordStrengthMessage(message)
}
const onDbPassChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value
setPassword(value)
if (value == '') {
latestPasswordStrengthValueRef.current = value
passwordStrengthResultValueRef.current = value
setPasswordStrengthScore(-1)
setPasswordStrengthMessage('')
setPasswordStrengthWarning('')
} else checkPasswordStrength(value)
}
const confirmResetDbPass = async () => {
if (!ref) return console.error('Project ref is required')
if (
passwordStrengthResultValueRef.current === password &&
passwordStrengthScore >= DEFAULT_MINIMUM_PASSWORD_STRENGTH
) {
resetDatabasePassword({ ref, password })
}
}
function generatePassword() {
const password = generateStrongPassword()
setPassword(password)
checkPasswordStrength(password)
}
return (
<Dialog open={showResetDbPass} onOpenChange={(open) => setShowResetDbPass(open)}>
<DialogTrigger asChild>
<ButtonTooltip
variant={triggerVariant}
className={triggerClassName}
icon={triggerIcon}
disabled={!canResetDbPassword || !isProjectActive || disabled}
tooltip={{
content: {
side: 'bottom',
text: !canResetDbPassword
? 'You need additional permissions to reset the database password'
: !isProjectActive
? 'Unable to reset database password as project is not active'
: undefined,
},
}}
>
{triggerLabel}
</ButtonTooltip>
</DialogTrigger>
<DialogContent size="medium">
<DialogHeader>
<DialogTitle>Reset database password</DialogTitle>
</DialogHeader>
<DialogSectionSeparator />
<DialogSection className="w-full space-y-8">
<Admonition type="warning" title="This password is shared across every connection method">
Resetting it will disconnect the pooler, read replicas, and any app, ORM, or tool still
using the old password. Update it everywhere before switching over.
</Admonition>
<FormItemLayout
layout="vertical"
isReactForm={false}
error={passwordStrengthWarning}
description={
<PasswordStrengthBar
passwordStrengthScore={passwordStrengthScore as PasswordStrengthScore}
passwordStrengthMessage={passwordStrengthMessage}
password={password}
generateStrongPassword={generatePassword}
/>
}
>
<Input
copy={password.length > 0}
aria-invalid={!!passwordStrengthWarning}
type="password"
placeholder="Type in a strong password"
value={password}
autoComplete="off"
onChange={onDbPassChange}
/>
</FormItemLayout>
</DialogSection>
<DialogFooter>
<Button disabled={isUpdatingPassword} onClick={() => setShowResetDbPass(false)}>
Cancel
</Button>
<Button
variant="primary"
loading={isUpdatingPassword}
disabled={isUpdatingPassword}
onClick={() => confirmResetDbPass()}
>
Reset password
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
)
}