fix: unable to change password when using magic link (#484)

* fix: fixed password not resetting & added password prompt with save guards

* fix: migrated to using setPassword
This commit is contained in:
Morfixx
2026-05-11 05:40:59 -07:00
committed by GitHub
parent 607672718c
commit 71205b67dc
6 changed files with 218 additions and 62 deletions

View File

@@ -85,17 +85,19 @@ export default function AccountSettings() {
{isCredentialsEnabled && (
<div className="mb-8 border-t border-light-300 dark:border-dark-300">
<h2 className="mb-4 mt-8 text-[14px] font-bold text-neutral-900 dark:text-dark-1000">
{t`Change Password`}
{data?.hasPassword ? t`Change Password` : t`Set Password`}
</h2>
<p className="mb-8 text-sm text-neutral-500 dark:text-dark-900">
{t`You are about to change your password.`}
{data?.hasPassword
? t`You are about to change your password.`
: t`Set a password to enable password-based login.`}
</p>
<div className="mt-4">
<Button
variant="secondary"
onClick={() => openModal("CHANGE_PASSWORD")}
>
{t`Change Password`}
{data?.hasPassword ? t`Change Password` : t`Set Password`}
</Button>
</div>
</div>
@@ -113,7 +115,7 @@ export default function AccountSettings() {
modalSize="sm"
isVisible={isOpen && modalContentType === "CHANGE_PASSWORD"}
>
<ChangePasswordFormConfirmation />
<ChangePasswordFormConfirmation hasPassword={data?.hasPassword ?? false} />
</Modal>
{/* Global modals */}

View File

@@ -2,7 +2,6 @@ import { useRouter } from "next/navigation";
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/core/macro";
import { useMutation } from "@tanstack/react-query";
import { env } from "next-runtime-env";
import { useForm } from "react-hook-form";
import { z } from "zod";
@@ -14,27 +13,43 @@ import { useModal } from "~/providers/modal";
import { usePopup } from "~/providers/popup";
import { api } from "~/utils/api";
const FormSchema = z
.object({
currentPassword: z.string().min(1, t`Current password is required`),
const buildSchema = (hasPassword: boolean) => {
const base = z.object({
currentPassword: hasPassword
? z.string().min(1, t`Current password is required`)
: z.string().optional(),
newPassword: z
.string()
.min(8, t`Password must be at least 8 characters`)
.min(1, t`New password is required`),
confirmPassword: z.string().min(1, t`Please confirm your new password`),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: t`Passwords do not match`,
path: ["confirmPassword"],
})
.refine((data) => data.currentPassword !== data.newPassword, {
message: t`New password must be different from current password`,
path: ["newPassword"],
});
type FormValues = z.infer<typeof FormSchema>;
return base
.refine((data) => data.newPassword === data.confirmPassword, {
message: t`Passwords do not match`,
path: ["confirmPassword"],
})
.refine(
(data) => !hasPassword || data.currentPassword !== data.newPassword,
{
message: t`New password must be different from current password`,
path: ["newPassword"],
},
);
};
export function ChangePasswordFormConfirmation() {
type FormValues = {
currentPassword?: string;
newPassword: string;
confirmPassword: string;
};
interface Props {
hasPassword: boolean;
}
export function ChangePasswordFormConfirmation({ hasPassword }: Props) {
const { closeModal } = useModal();
const { showPopup } = usePopup();
const router = useRouter();
@@ -47,15 +62,24 @@ export function ChangePasswordFormConfirmation() {
reset,
setError,
} = useForm<FormValues>({
resolver: zodResolver(FormSchema),
resolver: zodResolver(buildSchema(hasPassword)),
mode: "onChange",
});
const setPasswordMutation = api.user.setPassword.useMutation();
const changePasswordMutation = useMutation({
mutationFn: async (data: FormValues) => {
if (!hasPassword) {
await setPasswordMutation.mutateAsync({
newPassword: data.newPassword,
});
return;
}
const response = await authClient.changePassword({
newPassword: data.newPassword,
currentPassword: data.currentPassword,
currentPassword: data.currentPassword ?? "",
revokeOtherSessions: true,
});
@@ -66,11 +90,19 @@ export function ChangePasswordFormConfirmation() {
onSuccess: async () => {
closeModal();
showPopup({
header: t`Password Changed`,
message: t`Your password has been changed.`,
header: hasPassword ? t`Password Changed` : t`Password Set`,
message: hasPassword
? t`Your password has been changed.`
: t`Your password has been set.`,
icon: "success",
});
// Clear the session prompt flag so future magic link logins
// don't re-show the set-password modal (password is now set)
if (!hasPassword && typeof window !== "undefined") {
sessionStorage.removeItem("set_password_prompted");
}
utils.invalidate();
reset();
router.push("/");
@@ -86,7 +118,7 @@ export function ChangePasswordFormConfirmation() {
} else {
closeModal();
showPopup({
header: t`Error Changing Password`,
header: hasPassword ? t`Error Changing Password` : t`Error Setting Password`,
message: t`An unexpected error occurred. Please try again later.`,
icon: "error",
});
@@ -106,27 +138,33 @@ export function ChangePasswordFormConfirmation() {
return (
<div className="p-5">
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium dark:text-white">{t`Change Password`}</h2>
<h2 className="text-md pb-4 font-medium dark:text-white">
{hasPassword ? t`Change Password` : t`Set Password`}
</h2>
<p className="mb-4 text-sm text-light-900">
{t`Enter your current password and choose a new secure password.`}
{hasPassword
? t`Enter your current password and choose a new secure password.`
: t`You signed in without a password. Set a password to enable password-based login.`}
</p>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="space-y-2">
<div>
<Input
id="currentPassword"
type="password"
{...register("currentPassword")}
placeholder={t`Enter your current password`}
/>
{errors.currentPassword && (
<p className="mt-2 text-xs text-red-400">
{errors.currentPassword.message}
</p>
)}
</div>
{hasPassword && (
<div>
<Input
id="currentPassword"
type="password"
{...register("currentPassword")}
placeholder={t`Enter your current password`}
/>
{errors.currentPassword && (
<p className="mt-2 text-xs text-red-400">
{errors.currentPassword.message}
</p>
)}
</div>
)}
<div>
<Input
@@ -175,7 +213,7 @@ export function ChangePasswordFormConfirmation() {
fullWidth
size="lg"
>
{t`Change Password`}
{hasPassword ? t`Change Password` : t`Set Password`}
</Button>
</div>
</form>