Refactor Header component to improve code readability by formatting Link elements and adding a new settings link for user navigation.
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m12s

This commit is contained in:
Julien Froidefond
2026-02-20 13:31:23 +01:00
parent e30cfedea8
commit 65fee6baf7
3 changed files with 218 additions and 6 deletions

View File

@@ -0,0 +1,60 @@
import { NextRequest, NextResponse } from "next/server";
import { auth } from "@/auth";
import { prisma } from "@/lib/db";
import bcrypt from "bcryptjs";
export async function PATCH(req: NextRequest) {
const session = await auth();
if (!session?.user?.id) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
const { currentPassword, newPassword } = await req.json();
if (!currentPassword || !newPassword) {
return NextResponse.json(
{ error: "Mot de passe actuel et nouveau requis" },
{ status: 400 }
);
}
if (newPassword.length < 8) {
return NextResponse.json(
{ error: "Le nouveau mot de passe doit faire au moins 8 caractères" },
{ status: 400 }
);
}
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { passwordHash: true },
});
if (!user?.passwordHash) {
return NextResponse.json(
{ error: "Compte sans mot de passe (connexion SSO)" },
{ status: 400 }
);
}
const ok = await bcrypt.compare(String(currentPassword), user.passwordHash);
if (!ok) {
return NextResponse.json(
{ error: "Mot de passe actuel incorrect" },
{ status: 401 }
);
}
const passwordHash = await bcrypt.hash(String(newPassword), 10);
await prisma.user.update({
where: { id: session.user.id },
data: { passwordHash },
});
return NextResponse.json({ ok: true });
} catch (e) {
console.error("Password change error:", e);
return NextResponse.json(
{ error: "Erreur lors du changement de mot de passe" },
{ status: 500 }
);
}
}

129
src/app/settings/page.tsx Normal file
View File

@@ -0,0 +1,129 @@
"use client";
import { useState } from "react";
import Link from "next/link";
export default function SettingsPage() {
const [currentPassword, setCurrentPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState("");
const [success, setSuccess] = useState(false);
const [loading, setLoading] = useState(false);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setError("");
setSuccess(false);
if (newPassword !== confirmPassword) {
setError("Les deux nouveaux mots de passe ne correspondent pas");
return;
}
if (newPassword.length < 8) {
setError("Le mot de passe doit faire au moins 8 caractères");
return;
}
setLoading(true);
try {
const res = await fetch("/api/users/me/password", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
currentPassword,
newPassword,
}),
});
const data = await res.json();
if (!res.ok) {
setError(data.error ?? "Erreur");
return;
}
setSuccess(true);
setCurrentPassword("");
setNewPassword("");
setConfirmPassword("");
} catch {
setError("Erreur de connexion");
} finally {
setLoading(false);
}
}
return (
<div className="mx-auto max-w-md">
<h1 className="mb-6 font-mono text-lg font-medium text-zinc-800 dark:text-zinc-100">
Paramètres
</h1>
<section className="rounded-lg border border-zinc-200 dark:border-zinc-600 bg-white dark:bg-zinc-800/50 p-4">
<h2 className="mb-4 font-mono text-sm font-medium text-zinc-700 dark:text-zinc-300">
Changer mon mot de passe
</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="mb-1 block font-mono text-xs text-zinc-600 dark:text-zinc-400">
Mot de passe actuel
</label>
<input
type="password"
value={currentPassword}
onChange={(e) => setCurrentPassword(e.target.value)}
required
autoComplete="current-password"
className="w-full rounded border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500/30"
/>
</div>
<div>
<label className="mb-1 block font-mono text-xs text-zinc-600 dark:text-zinc-400">
Nouveau mot de passe
</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
required
minLength={8}
autoComplete="new-password"
className="w-full rounded border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500/30"
/>
</div>
<div>
<label className="mb-1 block font-mono text-xs text-zinc-600 dark:text-zinc-400">
Confirmer le nouveau mot de passe
</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
minLength={8}
autoComplete="new-password"
className="w-full rounded border border-zinc-300 dark:border-zinc-600 bg-white dark:bg-zinc-800 px-3 py-2 text-sm text-zinc-900 dark:text-zinc-100 placeholder-zinc-400 focus:border-cyan-500 focus:ring-1 focus:ring-cyan-500/30"
/>
</div>
{error && (
<p className="font-mono text-xs text-red-500">{error}</p>
)}
{success && (
<p className="font-mono text-xs text-emerald-600 dark:text-emerald-400">
Mot de passe modifié.
</p>
)}
<button
type="submit"
disabled={loading}
className="rounded border border-cyan-500/50 bg-cyan-500/10 px-3 py-2 font-mono text-sm text-cyan-600 dark:text-cyan-400 hover:bg-cyan-500/20 disabled:opacity-50"
>
{loading ? "..." : "Modifier le mot de passe"}
</button>
</form>
</section>
<p className="mt-6 font-mono text-xs text-zinc-500 dark:text-zinc-400">
<Link href="/dashboard" className="text-cyan-600 dark:text-cyan-400 hover:underline">
Retour au dashboard
</Link>
</p>
</div>
);
}

View File

@@ -10,24 +10,44 @@ export function Header() {
return (
<header className="sticky top-0 z-50 border-b border-zinc-200 bg-white dark:border-zinc-700 dark:bg-zinc-900 shadow-sm dark:shadow-none backdrop-blur-sm">
<div className="mx-auto flex h-12 max-w-6xl items-center justify-between px-4">
<Link href="/" className="font-mono text-sm font-medium text-zinc-900 dark:text-zinc-50 tracking-tight">
<Link
href="/"
className="font-mono text-sm font-medium text-zinc-900 dark:text-zinc-50 tracking-tight"
>
iag-eval
</Link>
<nav className="flex items-center gap-6 font-mono text-xs">
{status === "authenticated" ? (
<>
<Link href="/dashboard" className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors">
<Link
href="/dashboard"
className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors"
>
/dashboard
</Link>
<Link href="/evaluations/new" className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors">
<Link
href="/evaluations/new"
className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors"
>
/new
</Link>
{session?.user?.role === "admin" && (
<Link href="/admin" className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors">
<Link
href="/admin"
className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors"
>
/admin
</Link>
)}
<span className="text-zinc-400 dark:text-zinc-500">{session?.user?.email}</span>
<Link
href="/settings"
className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors"
>
/paramètres
</Link>
<span className="text-zinc-400 dark:text-zinc-500">
{session?.user?.email}
</span>
<button
type="button"
onClick={async () => {
@@ -40,7 +60,10 @@ export function Header() {
</button>
</>
) : (
<Link href="/auth/login" className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors">
<Link
href="/auth/login"
className="text-zinc-500 hover:text-cyan-600 dark:text-zinc-400 dark:hover:text-cyan-400 transition-colors"
>
Se connecter
</Link>
)}