From f1e9e3f8d4d8f7d4f194c00b9b2b2caf652a2ee0 Mon Sep 17 00:00:00 2001 From: Pierre Date: Sun, 8 Feb 2026 14:21:07 +0100 Subject: [PATCH] Added Function to config Profil --- app/api/auth/me/route.ts | 1 + app/api/user/profile/route.ts | 131 +++++++++ app/api/users/route.ts | 1 + app/dashboard/parametres/compte/page.tsx | 24 ++ components/ConfigurationContent.tsx | 15 +- components/DashboardLayout.tsx | 22 +- components/ModifierCompteContent.tsx | 351 +++++++++++++++++++++++ components/ParametresContent.tsx | 20 +- lib/auth.ts | 1 + prisma/dev.db | Bin 196608 -> 237568 bytes prisma/schema.prisma | 1 + 11 files changed, 557 insertions(+), 10 deletions(-) create mode 100644 app/api/user/profile/route.ts create mode 100644 app/dashboard/parametres/compte/page.tsx create mode 100644 components/ModifierCompteContent.tsx diff --git a/app/api/auth/me/route.ts b/app/api/auth/me/route.ts index b0591ea..e7352dc 100644 --- a/app/api/auth/me/route.ts +++ b/app/api/auth/me/route.ts @@ -17,6 +17,7 @@ export async function GET() { id: true, email: true, name: true, + photoUrl: true, roleId: true, role: { select: { diff --git a/app/api/user/profile/route.ts b/app/api/user/profile/route.ts new file mode 100644 index 0000000..fcef41f --- /dev/null +++ b/app/api/user/profile/route.ts @@ -0,0 +1,131 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { getCurrentUser } from '@/lib/auth'; +import { prisma } from '@/lib/prisma'; +import { hashPassword, verifyPassword } from '@/lib/auth'; + +// GET - Récupérer le profil de l'utilisateur +export async function GET() { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const userProfile = await prisma.user.findUnique({ + where: { id: user.id }, + select: { + id: true, + email: true, + name: true, + photoUrl: true, + role: { + select: { + id: true, + name: true, + description: true, + }, + }, + }, + }); + + return NextResponse.json(userProfile); + } catch (error) { + console.error('Erreur lors de la récupération du profil:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} + +// PUT - Mettre à jour le profil de l'utilisateur +export async function PUT(request: NextRequest) { + try { + const user = await getCurrentUser(); + if (!user) { + return NextResponse.json({ error: 'Non autorisé' }, { status: 401 }); + } + + const body = await request.json(); + const { name, email, photoUrl, currentPassword, newPassword } = body; + + // Récupérer l'utilisateur complet pour vérifier le mot de passe si nécessaire + const currentUser = await prisma.user.findUnique({ + where: { id: user.id }, + }); + + if (!currentUser) { + return NextResponse.json({ error: 'Utilisateur non trouvé' }, { status: 404 }); + } + + // Vérifier si l'email est déjà utilisé par un autre utilisateur + if (email && email !== currentUser.email) { + const existingUser = await prisma.user.findUnique({ + where: { email }, + }); + if (existingUser) { + return NextResponse.json( + { error: 'Cet email est déjà utilisé' }, + { status: 400 } + ); + } + } + + // Vérifier le mot de passe actuel si un nouveau mot de passe est fourni + if (newPassword) { + if (!currentPassword) { + return NextResponse.json( + { error: 'Le mot de passe actuel est requis pour changer le mot de passe' }, + { status: 400 } + ); + } + + const isValidPassword = await verifyPassword(currentPassword, currentUser.password); + if (!isValidPassword) { + return NextResponse.json( + { error: 'Mot de passe actuel incorrect' }, + { status: 400 } + ); + } + } + + // Préparer les données à mettre à jour + const updateData: any = {}; + if (name !== undefined) updateData.name = name; + if (email !== undefined) updateData.email = email; + if (photoUrl !== undefined) updateData.photoUrl = photoUrl; + if (newPassword) { + updateData.password = await hashPassword(newPassword); + } + + // Mettre à jour l'utilisateur + const updatedUser = await prisma.user.update({ + where: { id: user.id }, + data: updateData, + select: { + id: true, + email: true, + name: true, + photoUrl: true, + role: { + select: { + id: true, + name: true, + description: true, + }, + }, + }, + }); + + return NextResponse.json({ + success: true, + user: updatedUser, + }); + } catch (error) { + console.error('Erreur lors de la mise à jour du profil:', error); + return NextResponse.json( + { error: 'Erreur serveur' }, + { status: 500 } + ); + } +} diff --git a/app/api/users/route.ts b/app/api/users/route.ts index 2adba1b..e6e47f6 100644 --- a/app/api/users/route.ts +++ b/app/api/users/route.ts @@ -15,6 +15,7 @@ export async function GET(request: NextRequest) { id: true, email: true, name: true, + photoUrl: true, roleId: true, role: { select: { diff --git a/app/dashboard/parametres/compte/page.tsx b/app/dashboard/parametres/compte/page.tsx new file mode 100644 index 0000000..f036e35 --- /dev/null +++ b/app/dashboard/parametres/compte/page.tsx @@ -0,0 +1,24 @@ +import { redirect } from 'next/navigation'; +import { getCurrentUser } from '@/lib/auth'; +import { hasPageAccess } from '@/lib/permissions'; +import DashboardLayout from '@/components/DashboardLayout'; +import ModifierCompteContent from '@/components/ModifierCompteContent'; + +export default async function ModifierComptePage() { + const user = await getCurrentUser(); + + if (!user) { + redirect('/login'); + } + + const hasAccess = await hasPageAccess(user.id, '/dashboard/parametres'); + if (!hasAccess) { + redirect('/login'); + } + + return ( + + + + ); +} diff --git a/components/ConfigurationContent.tsx b/components/ConfigurationContent.tsx index 9d25cd1..a8eac70 100644 --- a/components/ConfigurationContent.tsx +++ b/components/ConfigurationContent.tsx @@ -345,6 +345,7 @@ export default function ConfigurationContent() { id: string; email: string; name: string | null; + photoUrl?: string | null; roleId: string | null; role: { id: string; @@ -495,9 +496,17 @@ export default function ConfigurationContent() { >
{/* Avatar */} -
- {getUserInitials(user.name, user.email)} -
+ {user.photoUrl ? ( + {user.name + ) : ( +
+ {getUserInitials(user.name, user.email)} +
+ )} {/* Informations */}
diff --git a/components/DashboardLayout.tsx b/components/DashboardLayout.tsx index de35636..6a8e8e2 100644 --- a/components/DashboardLayout.tsx +++ b/components/DashboardLayout.tsx @@ -10,6 +10,7 @@ interface User { id: string; email: string; name: string | null; + photoUrl?: string | null; roleId?: string | null; } @@ -71,7 +72,14 @@ export default function DashboardLayout({ user, children }: DashboardLayoutProps fetcher ); + // Récupérer le profil complet de l'utilisateur pour la photo + const { data: userProfile } = useSWR<{ photoUrl?: string | null }>( + '/api/user/profile', + fetcher + ); + const accessiblePages = userPagesData?.pages || []; + const userPhotoUrl = userProfile?.photoUrl; // Calculer le nombre total de messages non lus const totalUnreadCount = Array.isArray(conversations) @@ -377,9 +385,17 @@ export default function DashboardLayout({ user, children }: DashboardLayoutProps onClick={() => setShowProfileMenu(!showProfileMenu)} className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-50 border border-gray-200 transition-all duration-200 hover:shadow-sm group" > -
- {getUserInitials()} -
+ {userPhotoUrl ? ( + {user.name + ) : ( +
+ {getUserInitials()} +
+ )}
{user.name || 'Utilisateur'}
{user.email}
diff --git a/components/ModifierCompteContent.tsx b/components/ModifierCompteContent.tsx new file mode 100644 index 0000000..6805122 --- /dev/null +++ b/components/ModifierCompteContent.tsx @@ -0,0 +1,351 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useRouter } from 'next/navigation'; +import { useNotification } from './NotificationProvider'; +import useSWR from 'swr'; + +interface UserProfile { + id: string; + email: string; + name: string | null; + photoUrl: string | null; + role: { + id: string; + name: string; + description: string | null; + } | null; +} + +const fetcher = (url: string) => fetch(url).then((res) => res.json()); + +export default function ModifierCompteContent() { + const router = useRouter(); + const { showNotification } = useNotification(); + const { data: userProfile, mutate } = useSWR('/api/user/profile', fetcher); + + const [loading, setLoading] = useState(false); + const [formData, setFormData] = useState({ + name: '', + email: '', + photoUrl: '', + currentPassword: '', + newPassword: '', + confirmPassword: '', + }); + + useEffect(() => { + if (userProfile) { + setFormData({ + name: userProfile.name || '', + email: userProfile.email || '', + photoUrl: userProfile.photoUrl || '', + currentPassword: '', + newPassword: '', + confirmPassword: '', + }); + } + }, [userProfile]); + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + }; + + const handleImageUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + // Vérifier la taille du fichier (max 5MB) + if (file.size > 5 * 1024 * 1024) { + showNotification('L\'image est trop grande (max 5MB)', 'error'); + return; + } + + // Vérifier le type de fichier + if (!file.type.startsWith('image/')) { + showNotification('Veuillez sélectionner une image', 'error'); + return; + } + + const reader = new FileReader(); + reader.onloadend = () => { + setFormData((prev) => ({ ...prev, photoUrl: reader.result as string })); + }; + reader.readAsDataURL(file); + } + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + + try { + // Vérifier que les mots de passe correspondent si un nouveau mot de passe est fourni + if (formData.newPassword) { + if (formData.newPassword !== formData.confirmPassword) { + showNotification('Les mots de passe ne correspondent pas', 'error'); + setLoading(false); + return; + } + + if (formData.newPassword.length < 6) { + showNotification('Le mot de passe doit contenir au moins 6 caractères', 'error'); + setLoading(false); + return; + } + } + + const updateData: any = { + name: formData.name.trim() || null, + email: formData.email.trim(), + photoUrl: formData.photoUrl || null, + }; + + if (formData.newPassword) { + updateData.currentPassword = formData.currentPassword; + updateData.newPassword = formData.newPassword; + } + + const response = await fetch('/api/user/profile', { + method: 'PUT', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(updateData), + }); + + const data = await response.json(); + + if (response.ok) { + showNotification('Profil mis à jour avec succès', 'success'); + mutate(); + // Réinitialiser les champs de mot de passe + setFormData((prev) => ({ + ...prev, + currentPassword: '', + newPassword: '', + confirmPassword: '', + })); + } else { + showNotification(data.error || 'Erreur lors de la mise à jour', 'error'); + } + } catch (error) { + console.error('Erreur:', error); + showNotification('Erreur lors de la mise à jour', 'error'); + } finally { + setLoading(false); + } + }; + + const getUserInitials = () => { + if (userProfile?.name) { + const names = userProfile.name.split(' '); + if (names.length >= 2) { + return `${names[0].charAt(0)}${names[1].charAt(0)}`.toUpperCase(); + } + return userProfile.name.charAt(0).toUpperCase(); + } + return userProfile?.email?.charAt(0).toUpperCase() || 'U'; + }; + + if (!userProfile) { + return ( +
+
Chargement...
+
+ ); + } + + return ( +
+
+ +

Modifier mon compte

+

+ Mettez à jour vos informations personnelles et votre mot de passe +

+
+ +
+
+ {/* Photo de profil */} +
+ +
+
+ {formData.photoUrl ? ( + Photo de profil + ) : ( +
+ {getUserInitials()} +
+ )} +
+
+ + {formData.photoUrl && ( + + )} +

JPG, PNG ou GIF (max 5MB)

+
+
+
+ + {/* Nom et prénom */} +
+ + +
+ + {/* Email */} +
+ + +
+ + {/* Section changement de mot de passe */} +
+

Changer le mot de passe

+

+ Laissez ces champs vides si vous ne souhaitez pas changer votre mot de passe +

+ +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ + {/* Boutons d'action */} +
+ + +
+
+
+
+ ); +} diff --git a/components/ParametresContent.tsx b/components/ParametresContent.tsx index aa79f2f..e08a4fe 100644 --- a/components/ParametresContent.tsx +++ b/components/ParametresContent.tsx @@ -10,6 +10,7 @@ interface User { id: string; email: string; name: string | null; + photoUrl?: string | null; roleId: string | null; role: { id: string; @@ -328,16 +329,27 @@ export default function ParametresContent() {
-
- {getUserInitials()} -
+ {user?.photoUrl ? ( + {user.name + ) : ( +
+ {getUserInitials()} +
+ )}

{user?.name || 'Utilisateur'}

{user?.email}

-