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 (
+
+ );
+}
+
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 (
+
+ );
+}
+
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
+