Add student profile page with password change
- New page /profile: shows name/email and password change form - Uses authClient.changePassword (current + new + confirm) - Student name in header is now a link to /profile Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -44,7 +44,9 @@ export default async function StudentLayout({ children }: { children: React.Reac
|
|||||||
{schoolName}
|
{schoolName}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<span className="text-sm" style={{ color: "var(--muted-foreground)" }}>{session.user.name}</span>
|
<Link href="/profile" className="text-sm hover:underline" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
{session.user.name}
|
||||||
|
</Link>
|
||||||
<LogoutButton />
|
<LogoutButton />
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
import { authClient } from "@/lib/auth-client";
|
||||||
|
|
||||||
|
const inputStyle: React.CSSProperties = {
|
||||||
|
border: "2px solid var(--border)",
|
||||||
|
color: "var(--foreground)",
|
||||||
|
fontFamily: "var(--font-sans)",
|
||||||
|
outline: "none",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ChangePasswordForm() {
|
||||||
|
const [current, setCurrent] = useState("");
|
||||||
|
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 result = await authClient.changePassword({
|
||||||
|
currentPassword: current,
|
||||||
|
newPassword: next,
|
||||||
|
revokeOtherSessions: false,
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
|
||||||
|
if (result.error) {
|
||||||
|
setError("Неверный текущий пароль");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrent("");
|
||||||
|
setNext("");
|
||||||
|
setConfirm("");
|
||||||
|
setSuccess(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="block text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
Текущий пароль
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={current}
|
||||||
|
onChange={(e) => setCurrent(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 text-sm bg-transparent"
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={(e) => (e.target.style.borderColor = "var(--foreground)")}
|
||||||
|
onBlur={(e) => (e.target.style.borderColor = "var(--border)")}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="block text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
Новый пароль
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={next}
|
||||||
|
onChange={(e) => setNext(e.target.value)}
|
||||||
|
required
|
||||||
|
minLength={8}
|
||||||
|
className="w-full px-3 py-2 text-sm bg-transparent"
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={(e) => (e.target.style.borderColor = "var(--foreground)")}
|
||||||
|
onBlur={(e) => (e.target.style.borderColor = "var(--border)")}
|
||||||
|
placeholder="Минимум 8 символов"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5">
|
||||||
|
<label className="block text-xs uppercase tracking-widest font-bold" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
Повторите новый пароль
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={confirm}
|
||||||
|
onChange={(e) => setConfirm(e.target.value)}
|
||||||
|
required
|
||||||
|
className="w-full px-3 py-2 text-sm bg-transparent"
|
||||||
|
style={inputStyle}
|
||||||
|
onFocus={(e) => (e.target.style.borderColor = "var(--foreground)")}
|
||||||
|
onBlur={(e) => (e.target.style.borderColor = "var(--border)")}
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{error && (
|
||||||
|
<p className="text-sm" style={{ color: "var(--destructive)" }}>{error}</p>
|
||||||
|
)}
|
||||||
|
{success && (
|
||||||
|
<p className="text-sm" style={{ color: "var(--foreground)" }}>Пароль успешно изменён.</p>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="btn-aubade justify-center"
|
||||||
|
style={loading ? { opacity: 0.6, cursor: "not-allowed" } : undefined}
|
||||||
|
>
|
||||||
|
{loading ? "Сохранение..." : "Сменить пароль"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { headers } from "next/headers";
|
||||||
|
import { auth } from "@/lib/auth";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { ChangePasswordForm } from "./change-password-form";
|
||||||
|
|
||||||
|
export const metadata = { title: "Профиль" };
|
||||||
|
|
||||||
|
export default async function ProfilePage() {
|
||||||
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
if (!session) redirect("/login");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="max-w-lg mx-auto px-4 py-10 w-full">
|
||||||
|
<h1 className="text-xl font-bold tracking-wide mb-8" style={{ color: "var(--foreground)" }}>
|
||||||
|
Профиль
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="card-aubade p-6 mb-6">
|
||||||
|
<h2 className="text-xs uppercase tracking-widest font-bold mb-4" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
Аккаунт
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-3 text-sm" style={{ color: "var(--foreground)" }}>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span style={{ color: "var(--muted-foreground)" }}>Имя</span>
|
||||||
|
<span>{session.user.name}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<span style={{ color: "var(--muted-foreground)" }}>Email</span>
|
||||||
|
<span>{session.user.email}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="card-aubade p-6">
|
||||||
|
<h2 className="text-xs uppercase tracking-widest font-bold mb-4" style={{ color: "var(--muted-foreground)" }}>
|
||||||
|
Смена пароля
|
||||||
|
</h2>
|
||||||
|
<ChangePasswordForm />
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user