mirror of
https://github.com/supabase/supabase.git
synced 2026-10-10 20:05:06 +03:00
Makes it possible to reset password from the connect sheet. Once reset the password is shown temporarily in the connection string for copy. The copy prompt action does not copy the password. <img width="1043" height="953" alt="image" src="https://github.com/user-attachments/assets/fe1a33bb-839f-47e3-b07f-7a5fa1df2b8d" /> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Session Pooler notice for direct connections on IPv4 networks * Option to show a temporary database password during direct-connection setup * **Improvements** * New password reset dialog with strength checks and generation * Connection-copy behavior now redacts temporary passwords and produces cleaner copy prompts * **Tests** * Added tests covering connection-string password insertion/replacement and copy-prompt behavior <!-- end of auto-generated comment: release notes by coderabbit.ai -->
203 lines
6.6 KiB
TypeScript
203 lines
6.6 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 { 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
|
|
triggerType?: ComponentProps<typeof ButtonTooltip>['type']
|
|
}
|
|
|
|
export const ResetDbPasswordDialog = ({
|
|
disabled = false,
|
|
onPasswordReset,
|
|
triggerClassName,
|
|
triggerIcon,
|
|
triggerLabel = 'Reset password',
|
|
triggerType = '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
|
|
type={triggerType}
|
|
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">
|
|
<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
|
|
type="default"
|
|
disabled={isUpdatingPassword}
|
|
onClick={() => setShowResetDbPass(false)}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
type="primary"
|
|
loading={isUpdatingPassword}
|
|
disabled={isUpdatingPassword}
|
|
onClick={() => confirmResetDbPass()}
|
|
>
|
|
Reset password
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
)
|
|
}
|