mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 09:55:06 +03:00
## 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 -->
204 lines
6.9 KiB
TypeScript
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>
|
|
)
|
|
}
|