From 873b3dd9f3d4de28dc87719713bae98bb1b8712f Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 27 Nov 2025 13:37:18 +0100 Subject: [PATCH] feat: add profile link to header and implement user profile update functionality with email and password management --- dev.db | Bin 122880 -> 122880 bytes src/actions/profile.ts | 58 ++++++++++++++++ src/app/profile/PasswordForm.tsx | 115 +++++++++++++++++++++++++++++++ src/app/profile/ProfileForm.tsx | 84 ++++++++++++++++++++++ src/app/profile/page.tsx | 75 ++++++++++++++++++++ src/components/layout/Header.tsx | 7 ++ src/services/auth.ts | 78 +++++++++++++++++++++ 7 files changed, 417 insertions(+) create mode 100644 src/actions/profile.ts create mode 100644 src/app/profile/PasswordForm.tsx create mode 100644 src/app/profile/ProfileForm.tsx create mode 100644 src/app/profile/page.tsx diff --git a/dev.db b/dev.db index 562912858b29057c08aba7d14d74a1581479a929..26e613cec59eca4d3a448fec59f9dcf121977aec 100644 GIT binary patch delta 1072 zcmZoTz}|3xeS$Qj_e2?IM(>RYOZvGO`Rf_@>-oa?!Zr&Ec<^lwo2SXBRnN#}FV4Uz z%3$yAo}8PRk!EOUk!E0EU{+{UkY8A6Y*?C{S(=iVl2|;sVV<^?v5A$TrJk9Ixwe6U zm4Shgfsv`Mp`oskd5EDAx+E6^0|O)fV+Q`m{LA^5Zx&RT$v=7ddKX2kR`3E9GxPH@ z@UQ2O+sw0pn_my4EFI_)OOQ*-(@nE03(SjfI&m`NK1mCICKh{pdzc!KW($yJGlTS! zv>d}qm;{FZCfC;iH8Y}XHV0`gO-(PzGfPUst@&Y<E9L)U7L0&z&ndiV_{y?m* zXO?BQXM`Dq;XEc8W_wO#Srq#>U0^W)Mj>i&&ip4k{Rls!1KjfMJOYf1`Qxz$F^cUN zVSr*ghAb#9!5%ycjLXN{`4oV*OlIFF%nCLX6n?B=113WPgNvCzl7W9BeDIymH+C zf$97$w@Q-?D+e%HXf}DmQ+OsYX(WPTx5y+jE6bwDbh=|NqfvyTbBL$EU%YdWt7C|( zOSMvBa!F=>o@a`Zl@csoFzi;+Q7XwS$w>vO0cR5+KPNLUJ2l0#BsJGFrC7--S_u>? z%s>f{U%)ib-wH~xwOF%{sR1N!ZI|n1e8$Ab%AXBRR={xY6}K!9R4FHA8nKksIq4cz>CAZ3O?WoaNM zB$`x|RU~CwBiw3FYLUl1tH$Upt+Jx1N>cJ~<%0RTnT@reKc diff --git a/src/actions/profile.ts b/src/actions/profile.ts new file mode 100644 index 0000000..f4dc3c7 --- /dev/null +++ b/src/actions/profile.ts @@ -0,0 +1,58 @@ +'use server'; + +import { auth } from '@/lib/auth'; +import { updateUserProfile, updateUserPassword, getUserById } from '@/services/auth'; + +export async function getProfileAction() { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié', data: null }; + } + + const user = await getUserById(session.user.id); + if (!user) { + return { success: false, error: 'Utilisateur non trouvé', data: null }; + } + + return { + success: true, + data: { + id: user.id, + name: user.name, + email: user.email, + createdAt: user.createdAt, + }, + }; +} + +export async function updateProfileAction(data: { name?: string; email?: string }) { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + const result = await updateUserProfile(session.user.id, data); + return result; +} + +export async function updatePasswordAction(data: { + currentPassword: string; + newPassword: string; +}) { + const session = await auth(); + if (!session?.user?.id) { + return { success: false, error: 'Non authentifié' }; + } + + if (data.newPassword.length < 6) { + return { success: false, error: 'Le nouveau mot de passe doit faire au moins 6 caractères' }; + } + + const result = await updateUserPassword( + session.user.id, + data.currentPassword, + data.newPassword + ); + return result; +} + diff --git a/src/app/profile/PasswordForm.tsx b/src/app/profile/PasswordForm.tsx new file mode 100644 index 0000000..37b0a69 --- /dev/null +++ b/src/app/profile/PasswordForm.tsx @@ -0,0 +1,115 @@ +'use client'; + +import { useState, useTransition } from 'react'; +import { Input, Button } from '@/components/ui'; +import { updatePasswordAction } from '@/actions/profile'; + +export function PasswordForm() { + const [isPending, startTransition] = useTransition(); + const [currentPassword, setCurrentPassword] = useState(''); + const [newPassword, setNewPassword] = useState(''); + const [confirmPassword, setConfirmPassword] = useState(''); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + const canSubmit = + currentPassword.length > 0 && + newPassword.length >= 6 && + newPassword === confirmPassword; + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setMessage(null); + + if (newPassword !== confirmPassword) { + setMessage({ type: 'error', text: 'Les mots de passe ne correspondent pas' }); + return; + } + + startTransition(async () => { + const result = await updatePasswordAction({ currentPassword, newPassword }); + + if (result.success) { + setMessage({ type: 'success', text: 'Mot de passe modifié avec succès' }); + setCurrentPassword(''); + setNewPassword(''); + setConfirmPassword(''); + } else { + setMessage({ type: 'error', text: result.error || 'Erreur lors de la modification' }); + } + }); + } + + return ( +
+
+ + setCurrentPassword(e.target.value)} + required + /> +
+ +
+ + setNewPassword(e.target.value)} + required + minLength={6} + /> +

Minimum 6 caractères

+
+ +
+ + setConfirmPassword(e.target.value)} + required + /> + {confirmPassword && newPassword !== confirmPassword && ( +

+ Les mots de passe ne correspondent pas +

+ )} +
+ + {message && ( +

+ {message.text} +

+ )} + + +
+ ); +} + diff --git a/src/app/profile/ProfileForm.tsx b/src/app/profile/ProfileForm.tsx new file mode 100644 index 0000000..60b0879 --- /dev/null +++ b/src/app/profile/ProfileForm.tsx @@ -0,0 +1,84 @@ +'use client'; + +import { useState, useTransition } from 'react'; +import { useRouter } from 'next/navigation'; +import { Input, Button } from '@/components/ui'; +import { updateProfileAction } from '@/actions/profile'; + +interface ProfileFormProps { + initialData: { + name: string; + email: string; + }; +} + +export function ProfileForm({ initialData }: ProfileFormProps) { + const router = useRouter(); + const [isPending, startTransition] = useTransition(); + const [name, setName] = useState(initialData.name); + const [email, setEmail] = useState(initialData.email); + const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null); + + const hasChanges = name !== initialData.name || email !== initialData.email; + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setMessage(null); + + startTransition(async () => { + const result = await updateProfileAction({ name, email }); + + if (result.success) { + setMessage({ type: 'success', text: 'Profil mis à jour avec succès' }); + router.refresh(); + } else { + setMessage({ type: 'error', text: result.error || 'Erreur lors de la mise à jour' }); + } + }); + } + + return ( +
+
+ + setName(e.target.value)} + placeholder="Votre nom" + /> +
+ +
+ + setEmail(e.target.value)} + required + /> +
+ + {message && ( +

+ {message.text} +

+ )} + + +
+ ); +} + diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx new file mode 100644 index 0000000..2c18ce2 --- /dev/null +++ b/src/app/profile/page.tsx @@ -0,0 +1,75 @@ +import { auth } from '@/lib/auth'; +import { redirect } from 'next/navigation'; +import { getUserById } from '@/services/auth'; +import { ProfileForm } from './ProfileForm'; +import { PasswordForm } from './PasswordForm'; + +export default async function ProfilePage() { + const session = await auth(); + + if (!session?.user?.id) { + redirect('/login'); + } + + const user = await getUserById(session.user.id); + + if (!user) { + redirect('/login'); + } + + return ( +
+
+

Mon Profil

+

Gérez vos informations personnelles

+
+ +
+ {/* Profile Info */} +
+

+ Informations personnelles +

+ +
+ + {/* Password */} +
+

+ Changer le mot de passe +

+ +
+ + {/* Account Info */} +
+

+ Informations du compte +

+
+
+ ID du compte + {user.id} +
+
+ Membre depuis + + {new Date(user.createdAt).toLocaleDateString('fr-FR', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} + +
+
+
+
+
+ ); +} + diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 78a060d..5c0021c 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -75,6 +75,13 @@ export function Header() { {session.user.email}

+ setMenuOpen(false)} + className="block w-full px-4 py-2 text-left text-sm text-foreground hover:bg-card-hover" + > + 👤 Mon Profil +