diff --git a/README.md b/README.md index 85083be..6e4fa62 100644 --- a/README.md +++ b/README.md @@ -132,17 +132,22 @@ Ouvrir [http://localhost:3000](http://localhost:3000) ### Avec Docker 1Run le container : - ```bash - docker compose up -d - ``` + +```bash +docker compose up -d +``` + Créer un Super Administrateur à l'intérieur du container : - ```bash - docker exec -it sh - ``` - Puis exécuter (pensez à modifier le nom d'utilisateur et le mot de passe si nécessaire) : - ```bash - node scripts/create-super-admin.js - ``` + +```bash +docker exec -it sh +``` + +Puis exécuter (pensez à modifier le nom d'utilisateur et le mot de passe si nécessaire) : + +```bash +node scripts/create-super-admin.js +``` ## Contribution diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 916667b..54be5c9 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -69,6 +69,9 @@ export default function AdminPage() { id: string; name: string; } | null>(null); + const [currentPassword, setCurrentPassword] = useState(''); + const [changeNewPassword, setChangeNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); useEffect(() => { if (status === 'loading') return; @@ -185,6 +188,8 @@ export default function AdminPage() { }; const handleDelete = async (id: string) => { + // Reject the hour entry then delete + await handleValidate(id, 'REJECTED'); await fetch(`/api/hours/${id}`, { method: 'DELETE', }); @@ -232,6 +237,28 @@ export default function AdminPage() { fetchHours(); }; + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault(); + if (changeNewPassword !== confirmPassword) { + toast.error('Les mots de passe ne correspondent pas'); + return; + } + const res = await fetch('/api/auth/change-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentPassword, newPassword: changeNewPassword }), + }); + if (res.ok) { + toast.success('Mot de passe changé avec succès'); + setCurrentPassword(''); + setChangeNewPassword(''); + setConfirmPassword(''); + } else { + const data = await res.json(); + toast.error(data.error || 'Erreur lors du changement de mot de passe'); + } + }; + if (status === 'loading') return
Chargement...
; const isSuperAdmin = session?.user?.role === 'SUPER_ADMIN'; @@ -347,7 +374,7 @@ export default function AdminPage() { variant="destructive" disabled={hour.userId === session?.user?.id} > - Supprimer + Rejeter )} @@ -401,6 +428,48 @@ export default function AdminPage() { + + + Changer mot de passe + + +
+
+ + setCurrentPassword(e.target.value)} + required + /> +
+
+ + setChangeNewPassword(e.target.value)} + required + /> +
+
+ + setConfirmPassword(e.target.value)} + required + /> +
+ +
+
+
{isSuperAdmin && ( diff --git a/app/api/auth/change-password/route.ts b/app/api/auth/change-password/route.ts new file mode 100644 index 0000000..b256374 --- /dev/null +++ b/app/api/auth/change-password/route.ts @@ -0,0 +1,41 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '../../../../lib/auth'; +import { prisma } from '../../../../lib/prisma'; +import bcrypt from 'bcryptjs'; + +export async function POST(request: NextRequest) { + const { currentPassword, newPassword } = await request.json(); + + const session = await getServerSession(authOptions); + if (!session) { + return NextResponse.json({ error: 'Non authentifié' }, { status: 401 }); + } + + const user = await prisma.user.findUnique({ + where: { id: session.user.id }, + }); + + if (!user) { + return NextResponse.json( + { error: 'Utilisateur non trouvé' }, + { status: 404 }, + ); + } + + const isValid = await bcrypt.compare(currentPassword, user.password); + if (!isValid) { + return NextResponse.json( + { error: 'Mot de passe actuel incorrect' }, + { status: 400 }, + ); + } + + const hashedPassword = await bcrypt.hash(newPassword, 10); + await prisma.user.update({ + where: { id: session.user.id }, + data: { password: hashedPassword }, + }); + + return NextResponse.json({ message: 'Mot de passe changé avec succès' }); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index fbf5cbf..ddd549c 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -22,6 +22,7 @@ import { } from '../../components/ui/card'; import { DatePicker } from '../../components/ui/date-picker'; import { format } from 'date-fns'; +import { toast } from 'sonner'; interface Hour { id: number; @@ -41,6 +42,9 @@ export default function DashboardPage() { const [reason, setReason] = useState(''); const [hoursInput, setHoursInput] = useState(''); const [minutesInput, setMinutesInput] = useState(''); + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); useEffect(() => { if (status === 'loading') return; @@ -72,7 +76,11 @@ export default function DashboardPage() { const res = await fetch('/api/hours', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ date: dateString, duration: totalMinutes, reason }), + body: JSON.stringify({ + date: dateString, + duration: totalMinutes, + reason, + }), }); if (res.ok) { setDate(undefined); @@ -80,6 +88,9 @@ export default function DashboardPage() { setMinutesInput(''); setReason(''); fetchHours(); + toast.success('Heure ajoutée avec succès'); + } else { + toast.error("Erreur lors de l'ajout de l'heure"); } }; @@ -92,6 +103,28 @@ export default function DashboardPage() { fetchHours(); }; + const handleChangePassword = async (e: React.FormEvent) => { + e.preventDefault(); + if (newPassword !== confirmPassword) { + toast.error('Les mots de passe ne correspondent pas'); + return; + } + const res = await fetch('/api/auth/change-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ currentPassword, newPassword }), + }); + if (res.ok) { + toast.success('Mot de passe changé avec succès'); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } else { + const data = await res.json(); + toast.error(data.error || 'Erreur lors du changement de mot de passe'); + } + }; + if (status === 'loading') return
Chargement...
; const isAdmin = @@ -167,7 +200,7 @@ export default function DashboardPage() {
)} - + Liste des heures @@ -215,6 +248,50 @@ export default function DashboardPage() { + {isMember && ( + + + Changer mot de passe + + +
+
+ + setCurrentPassword(e.target.value)} + required + /> +
+
+ + setNewPassword(e.target.value)} + required + /> +
+
+ + setConfirmPassword(e.target.value)} + required + /> +
+ +
+
+
+ )}

Totaux

diff --git a/components/Header.tsx b/components/Header.tsx index 1bcdd8d..e3279b1 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -41,10 +41,7 @@ export default function Header() { {session.user.email} ({session.user.role}) -