Rewrite password change form to use Server Action
Replaces client-side fetch with a proper Server Action + useActionState. Uncontrolled inputs fix agent-browser testing and improve reliability. Server action verifies bcrypt hash directly via Prisma. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import { prisma } from "@/lib/prisma";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
|
export async function changePasswordAction(_prevState: unknown, formData: FormData) {
|
||||||
|
const current = (formData.get("currentPassword") as string) ?? "";
|
||||||
|
const next = (formData.get("newPassword") as string) ?? "";
|
||||||
|
const confirm = (formData.get("confirmPassword") as string) ?? "";
|
||||||
|
|
||||||
|
if (!current || !next || !confirm) return { error: "Заполните все поля" };
|
||||||
|
if (next !== confirm) return { error: "Пароли не совпадают" };
|
||||||
|
if (next.length < 8) return { error: "Новый пароль должен быть не короче 8 символов" };
|
||||||
|
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
if (!session) return { error: "Сессия истекла, войдите заново" };
|
||||||
|
|
||||||
|
const account = await prisma.account.findFirst({
|
||||||
|
where: { userId: session.user.id, providerId: "credential" },
|
||||||
|
});
|
||||||
|
if (!account?.password) return { error: "Аккаунт не найден" };
|
||||||
|
|
||||||
|
const valid = await bcrypt.compare(current, account.password);
|
||||||
|
if (!valid) return { error: "Неверный текущий пароль" };
|
||||||
|
|
||||||
|
const hash = await bcrypt.hash(next, 10);
|
||||||
|
await prisma.account.update({
|
||||||
|
where: { id: account.id },
|
||||||
|
data: { password: hash },
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useActionState } from "react";
|
||||||
|
import { changePasswordAction } from "./actions";
|
||||||
|
|
||||||
const inputStyle: React.CSSProperties = {
|
const inputStyle: React.CSSProperties = {
|
||||||
border: "2px solid var(--border)",
|
border: "2px solid var(--border)",
|
||||||
@@ -10,56 +11,17 @@ const inputStyle: React.CSSProperties = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function ChangePasswordForm() {
|
export function ChangePasswordForm() {
|
||||||
const [current, setCurrent] = useState("");
|
const [state, formAction, isPending] = useActionState(changePasswordAction, null);
|
||||||
const [next, setNext] = useState("");
|
|
||||||
const [confirm, setConfirm] = useState("");
|
|
||||||
const [loading, setLoading] = useState(false);
|
|
||||||
const [error, setError] = useState("");
|
|
||||||
const [success, setSuccess] = useState(false);
|
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
|
||||||
e.preventDefault();
|
|
||||||
setError("");
|
|
||||||
setSuccess(false);
|
|
||||||
|
|
||||||
if (next !== confirm) {
|
|
||||||
setError("Пароли не совпадают");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (next.length < 8) {
|
|
||||||
setError("Новый пароль должен быть не короче 8 символов");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true);
|
|
||||||
const res = await fetch("/api/auth/change-password", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ currentPassword: current, newPassword: next, revokeOtherSessions: false }),
|
|
||||||
});
|
|
||||||
setLoading(false);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
setError("Неверный текущий пароль");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setCurrent("");
|
|
||||||
setNext("");
|
|
||||||
setConfirm("");
|
|
||||||
setSuccess(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form action={formAction} className="space-y-4">
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="block text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
|
<label className="block text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
|
||||||
Текущий пароль
|
Текущий пароль
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={current}
|
name="currentPassword"
|
||||||
onChange={(e) => setCurrent(e.target.value)}
|
|
||||||
required
|
required
|
||||||
className="w-full px-3 py-2 text-sm bg-transparent"
|
className="w-full px-3 py-2 text-sm bg-transparent"
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
@@ -74,8 +36,7 @@ export function ChangePasswordForm() {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={next}
|
name="newPassword"
|
||||||
onChange={(e) => setNext(e.target.value)}
|
|
||||||
required
|
required
|
||||||
minLength={8}
|
minLength={8}
|
||||||
className="w-full px-3 py-2 text-sm bg-transparent"
|
className="w-full px-3 py-2 text-sm bg-transparent"
|
||||||
@@ -91,8 +52,7 @@ export function ChangePasswordForm() {
|
|||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={confirm}
|
name="confirmPassword"
|
||||||
onChange={(e) => setConfirm(e.target.value)}
|
|
||||||
required
|
required
|
||||||
className="w-full px-3 py-2 text-sm bg-transparent"
|
className="w-full px-3 py-2 text-sm bg-transparent"
|
||||||
style={inputStyle}
|
style={inputStyle}
|
||||||
@@ -101,19 +61,19 @@ export function ChangePasswordForm() {
|
|||||||
placeholder="••••••••"
|
placeholder="••••••••"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
{state?.error && (
|
||||||
<p className="text-sm" style={{ color: "var(--destructive)" }}>{error}</p>
|
<p className="text-sm" style={{ color: "var(--destructive)" }}>{state.error}</p>
|
||||||
)}
|
)}
|
||||||
{success && (
|
{state?.success && (
|
||||||
<p className="text-sm" style={{ color: "var(--foreground)" }}>Пароль успешно изменён.</p>
|
<p className="text-sm" style={{ color: "var(--foreground)" }}>Пароль успешно изменён.</p>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={isPending}
|
||||||
className="btn-aubade justify-center"
|
className="btn-aubade justify-center"
|
||||||
style={loading ? { opacity: 0.6, cursor: "not-allowed" } : undefined}
|
style={isPending ? { opacity: 0.6, cursor: "not-allowed" } : undefined}
|
||||||
>
|
>
|
||||||
{loading ? "Сохранение..." : "Сменить пароль"}
|
{isPending ? "Сохранение..." : "Сменить пароль"}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user