Files
Etienne StalmansandJoshen Lim 3ab3e999d3 feat: require current password on reset password (#43085)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES
## What kind of change does this PR introduce?

feature

## What is the current behavior?

Change password only requires the new password

## What is the new behavior?

The current password is required and must be valid before the password
can be changed.

Also hides a revealed password if the user clicks out of the input
field.

## Additional context

Needs the backend flag to be changed, but is safe to merge _before_
backend change is made. Is unsafe to merge _after_ backend flag is
changed (there will be a gap where users can't reset password through
the dashboard).

before:
<img width="486" height="364" alt="Screenshot 2026-02-23 at 09 58 04"
src="https://github.com/user-attachments/assets/7d9fcbc0-5ce9-4190-8bee-a325f3d4452e"
/>
after:
<img width="520" height="471" alt="Screenshot 2026-02-23 at 09 58 56"
src="https://github.com/user-attachments/assets/166e7537-5db8-47b7-a39f-ad55521715f5"
/>

before:
<img width="532" height="486" alt="Screenshot 2026-02-23 at 09 59 53"
src="https://github.com/user-attachments/assets/7445f61d-d354-4f3d-a084-fdf0b7b1ccca"
/>

after: 
<img width="524" height="599" alt="Screenshot 2026-02-23 at 10 00 10"
src="https://github.com/user-attachments/assets/573e9620-b663-4bad-8a7a-d315cc521495"
/>

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
2026-02-27 11:55:39 +01:00

214 lines
7.9 KiB
TypeScript

import { ButtonTooltip } from 'components/ui/ButtonTooltip'
import { useProfileIdentitiesQuery } from 'data/profile/profile-identities-query'
import { useUnlinkIdentityMutation } from 'data/profile/profile-unlink-identity-mutation'
import dayjs from 'dayjs'
import { BASE_PATH } from 'lib/constants'
import { Edit, Unlink } from 'lucide-react'
import Image from 'next/image'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'
import { toast } from 'sonner'
import {
Badge,
Button,
Card,
CardContent,
cn,
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import ConfirmationModal from 'ui-patterns/Dialogs/ConfirmationModal'
import {
PageSection,
PageSectionContent,
PageSectionDescription,
PageSectionMeta,
PageSectionSummary,
PageSectionTitle,
} from 'ui-patterns/PageSection'
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
import {
ChangeEmailAddressForm,
GitHubChangeEmailAddress,
SSOChangeEmailAddress,
} from './ChangeEmailAddress'
const getProviderName = (provider: string) =>
provider === 'github'
? 'GitHub'
: provider.startsWith('sso')
? 'SSO'
: provider.replaceAll('_', ' ')
export const AccountIdentities = () => {
const router = useRouter()
const { data, isPending: isLoading, isSuccess } = useProfileIdentitiesQuery()
const identities = data?.identities ?? []
const isChangeExpired = data?.email_change_sent_at
? dayjs().utc().diff(dayjs(data?.email_change_sent_at).utc(), 'minute') > 10
: false
const [selectedProviderUnlink, setSelectedProviderUnlink] = useState<string>()
const [selectedProviderUpdateEmail, setSelectedProviderUpdateEmail] = useState<string>()
const { mutate: unlinkIdentity, isPending: isUnlinking } = useUnlinkIdentityMutation({
onSuccess: () => {
toast.success(
`Successfully unlinked ${getProviderName(selectedProviderUnlink ?? '')} identity!`
)
setSelectedProviderUnlink(undefined)
},
})
const [, message] = router.asPath.split('#message=')
const onConfirmUnlinkIdentity = async () => {
const identity = identities.find((i) => i.provider === selectedProviderUnlink)
if (identity) unlinkIdentity(identity)
}
useEffect(() => {
if (message) toast.success(message.replaceAll('+', ' '))
}, [message])
return (
<PageSection>
<PageSectionMeta>
<PageSectionSummary>
<PageSectionTitle>Account identities</PageSectionTitle>
<PageSectionDescription>
Manage the providers linked to your Supabase account and update their details.
</PageSectionDescription>
</PageSectionSummary>
</PageSectionMeta>
<PageSectionContent>
<Card>
{isLoading && (
<CardContent>
<ShimmeringLoader />
</CardContent>
)}
{isSuccess && (
<div className="divide-y">
{identities.map((identity) => {
const { identity_id, provider } = identity
const username = identity.identity_data?.user_name
const providerName = getProviderName(provider)
const iconKey =
provider === 'github'
? 'github-icon'
: provider === 'email'
? 'email-icon2'
: 'saml-icon'
return (
<CardContent key={identity_id} className="flex justify-between items-center py-4">
<div className="flex gap-x-4">
<Image
className={cn(iconKey === 'github-icon' ? 'dark:invert' : '')}
src={`${BASE_PATH}/img/icons/${iconKey}.svg`}
width={30}
height={30}
alt={`${identity.provider} icon`}
/>
<div>
<div className="flex items-center gap-x-2">
<p className="text-sm capitalize">{providerName}</p>
{provider === 'email' && data.new_email && !isChangeExpired && (
<Tooltip>
<TooltipTrigger className="flex items-center">
<Badge variant="default">Pending change</Badge>
</TooltipTrigger>
<TooltipContent>Changing to {data.new_email}</TooltipContent>
</Tooltip>
)}
</div>
<p className="text-sm text-foreground-lighter">
{!!username ? <span>{username} • </span> : null}
{identity.email}
</p>
</div>
</div>
<div className="flex items-center gap-x-1">
{provider === 'email' && (
<Button asChild type="default">
<Link href="/reset-password?type=change">Change password</Link>
</Button>
)}
<ButtonTooltip
type="text"
icon={<Edit />}
className="w-7"
onClick={() => setSelectedProviderUpdateEmail(provider)}
tooltip={{ content: { side: 'bottom', text: 'Update email address' } }}
/>
{identities.length > 1 && (
<ButtonTooltip
type="text"
icon={<Unlink />}
className="w-7"
onClick={() => setSelectedProviderUnlink(provider)}
tooltip={{ content: { side: 'bottom', text: 'Unlink identity' } }}
/>
)}
</div>
</CardContent>
)
})}
</div>
)}
</Card>
<Dialog
open={!!selectedProviderUpdateEmail}
onOpenChange={(open: boolean) => {
if (!open) setSelectedProviderUpdateEmail(undefined)
}}
>
<DialogContent>
<DialogHeader className="border-b">
<DialogTitle>
{selectedProviderUpdateEmail !== 'email'
? `Updating email address for ${getProviderName(selectedProviderUpdateEmail ?? '')} identity`
: 'Update email address'}
</DialogTitle>
</DialogHeader>
{selectedProviderUpdateEmail === 'github' ? (
<GitHubChangeEmailAddress />
) : selectedProviderUpdateEmail?.startsWith('sso') ? (
<SSOChangeEmailAddress />
) : (
<ChangeEmailAddressForm onClose={() => setSelectedProviderUpdateEmail(undefined)} />
)}
</DialogContent>
</Dialog>
<ConfirmationModal
variant="warning"
size="small"
loading={isUnlinking}
visible={!!selectedProviderUnlink}
title={`Unlink ${getProviderName(selectedProviderUnlink ?? '')} identity`}
onCancel={() => setSelectedProviderUnlink(undefined)}
onConfirm={onConfirmUnlinkIdentity}
confirmLabel="Unlink identity"
confirmLabelLoading="Unlinking identity"
alert={{
base: { variant: 'warning' },
title: `Confirm to disconnect your ${getProviderName(selectedProviderUnlink ?? '')} identity`,
description: `After disconnecting, you will only be able to sign in via ${selectedProviderUnlink === 'github' ? 'email and password' : 'your GitHub identity'}`,
}}
/>
</PageSectionContent>
</PageSection>
)
}