From 83f523c11a7683cf2681ce9b35898ed8c177afb6 Mon Sep 17 00:00:00 2001 From: Julien Froidefond Date: Thu, 16 Oct 2025 22:27:06 +0200 Subject: [PATCH] feat: implement user account management features including profile display and password change functionality --- src/app/account/page.tsx | 34 +++++ src/app/api/user/password/route.ts | 49 ++++++ src/app/api/user/profile/route.ts | 28 ++++ src/components/account/ChangePasswordForm.tsx | 139 ++++++++++++++++++ src/components/account/UserProfileCard.tsx | 76 ++++++++++ src/components/layout/Sidebar.tsx | 12 +- src/components/ui/badge.tsx | 36 +++++ src/constants/errorCodes.ts | 4 + src/i18n/messages/en/common.json | 1 + src/i18n/messages/fr/common.json | 1 + src/lib/services/user.service.ts | 122 +++++++++++++++ 11 files changed, 501 insertions(+), 1 deletion(-) create mode 100644 src/app/account/page.tsx create mode 100644 src/app/api/user/password/route.ts create mode 100644 src/app/api/user/profile/route.ts create mode 100644 src/components/account/ChangePasswordForm.tsx create mode 100644 src/components/account/UserProfileCard.tsx create mode 100644 src/components/ui/badge.tsx create mode 100644 src/lib/services/user.service.ts diff --git a/src/app/account/page.tsx b/src/app/account/page.tsx new file mode 100644 index 0000000..d816fbe --- /dev/null +++ b/src/app/account/page.tsx @@ -0,0 +1,34 @@ +import { UserProfileCard } from "@/components/account/UserProfileCard"; +import { ChangePasswordForm } from "@/components/account/ChangePasswordForm"; +import { UserService } from "@/lib/services/user.service"; +import { redirect } from "next/navigation"; + +export default async function AccountPage() { + try { + const [profile, stats] = await Promise.all([ + UserService.getUserProfile(), + UserService.getUserStats(), + ]); + + return ( +
+
+
+

Mon compte

+

+ Gérez vos informations personnelles et votre sécurité +

+
+ +
+ + +
+
+
+ ); + } catch (error) { + console.error("Erreur lors du chargement du compte:", error); + redirect("/login"); + } +} diff --git a/src/app/api/user/password/route.ts b/src/app/api/user/password/route.ts new file mode 100644 index 0000000..ce62236 --- /dev/null +++ b/src/app/api/user/password/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from "next/server"; +import { UserService } from "@/lib/services/user.service"; +import { AppError } from "@/utils/errors"; +import { AuthServerService } from "@/lib/services/auth-server.service"; + +export async function PUT(request: NextRequest) { + try { + const body = await request.json(); + const { currentPassword, newPassword } = body; + + if (!currentPassword || !newPassword) { + return NextResponse.json( + { error: "Mots de passe manquants" }, + { status: 400 } + ); + } + + // Vérifier que le nouveau mot de passe est fort + if (!AuthServerService.isPasswordStrong(newPassword)) { + return NextResponse.json( + { + error: "Le nouveau mot de passe doit contenir au moins 8 caractères, une majuscule et un chiffre" + }, + { status: 400 } + ); + } + + await UserService.changePassword(currentPassword, newPassword); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("Erreur lors du changement de mot de passe:", error); + + if (error instanceof AppError) { + return NextResponse.json( + { error: error.message, code: error.code }, + { + status: error.code === "AUTH_INVALID_PASSWORD" ? 400 : + error.code === "AUTH_UNAUTHENTICATED" ? 401 : 500 + } + ); + } + + return NextResponse.json( + { error: "Erreur lors du changement de mot de passe" }, + { status: 500 } + ); + } +} diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts new file mode 100644 index 0000000..de6a55c --- /dev/null +++ b/src/app/api/user/profile/route.ts @@ -0,0 +1,28 @@ +import { NextResponse } from "next/server"; +import { UserService } from "@/lib/services/user.service"; +import { AppError } from "@/utils/errors"; + +export async function GET() { + try { + const [profile, stats] = await Promise.all([ + UserService.getUserProfile(), + UserService.getUserStats(), + ]); + + return NextResponse.json({ ...profile, stats }); + } catch (error) { + console.error("Erreur lors de la récupération du profil:", error); + + if (error instanceof AppError) { + return NextResponse.json( + { error: error.message, code: error.code }, + { status: error.code === "AUTH_UNAUTHENTICATED" ? 401 : 500 } + ); + } + + return NextResponse.json( + { error: "Erreur lors de la récupération du profil" }, + { status: 500 } + ); + } +} diff --git a/src/components/account/ChangePasswordForm.tsx b/src/components/account/ChangePasswordForm.tsx new file mode 100644 index 0000000..84cf3c1 --- /dev/null +++ b/src/components/account/ChangePasswordForm.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useToast } from "@/components/ui/use-toast"; +import { Lock } from "lucide-react"; + +export function ChangePasswordForm() { + const [currentPassword, setCurrentPassword] = useState(""); + const [newPassword, setNewPassword] = useState(""); + const [confirmPassword, setConfirmPassword] = useState(""); + const [isLoading, setIsLoading] = useState(false); + const { toast } = useToast(); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (newPassword !== confirmPassword) { + toast({ + variant: "destructive", + title: "Erreur", + description: "Les mots de passe ne correspondent pas", + }); + return; + } + + if (newPassword.length < 8) { + toast({ + variant: "destructive", + title: "Erreur", + description: "Le mot de passe doit contenir au moins 8 caractères", + }); + return; + } + + setIsLoading(true); + + try { + const response = await fetch("/api/user/password", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ currentPassword, newPassword }), + }); + + if (!response.ok) { + const data = await response.json(); + throw new Error(data.error || "Erreur lors du changement de mot de passe"); + } + + toast({ + title: "Succès", + description: "Votre mot de passe a été modifié avec succès", + }); + + // Reset form + setCurrentPassword(""); + setNewPassword(""); + setConfirmPassword(""); + } catch (error) { + toast({ + variant: "destructive", + title: "Erreur", + description: error instanceof Error ? error.message : "Une erreur est survenue", + }); + } finally { + setIsLoading(false); + } + }; + + return ( + + + Changer le mot de passe + + Assurez-vous d'utiliser un mot de passe fort (8 caractères minimum, une majuscule et un chiffre) + + + +
+
+ +
+ + setCurrentPassword(e.target.value)} + className="pl-9" + required + disabled={isLoading} + /> +
+
+ +
+ +
+ + setNewPassword(e.target.value)} + className="pl-9" + required + disabled={isLoading} + /> +
+
+ +
+ +
+ + setConfirmPassword(e.target.value)} + className="pl-9" + required + disabled={isLoading} + /> +
+
+ + +
+
+
+ ); +} + diff --git a/src/components/account/UserProfileCard.tsx b/src/components/account/UserProfileCard.tsx new file mode 100644 index 0000000..f0a240b --- /dev/null +++ b/src/components/account/UserProfileCard.tsx @@ -0,0 +1,76 @@ +"use client"; + +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Mail, Calendar, Shield, Heart } from "lucide-react"; +import type { UserProfile } from "@/lib/services/user.service"; + +interface UserProfileCardProps { + profile: UserProfile & { stats: { favoritesCount: number; hasPreferences: boolean; hasKomgaConfig: boolean } }; +} + +export function UserProfileCard({ profile }: UserProfileCardProps) { + return ( + + + Informations du compte + Vos informations personnelles + + +
+ +
+

Email

+

{profile.email}

+
+
+ +
+ +
+

Rôles

+
+ {profile.roles.map((role) => ( + + {role.replace("ROLE_", "")} + + ))} +
+
+
+ +
+ +
+

Membre depuis

+

+ {new Date(profile.createdAt).toLocaleDateString("fr-FR", { + year: "numeric", + month: "long", + day: "numeric", + })} +

+
+
+ +
+ +
+

Favoris

+

+ {profile.stats.favoritesCount} séries favorites +

+
+
+ +
+

+ Dernière mise à jour:{" "} + {new Date(profile.updatedAt).toLocaleDateString("fr-FR")} +

+
+
+
+ ); +} + diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 687aa05..e6296d6 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -1,6 +1,6 @@ "use client"; -import { Home, Library, Settings, LogOut, RefreshCw, Star, Download } from "lucide-react"; +import { Home, Library, Settings, LogOut, RefreshCw, Star, Download, User } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; import { cn } from "@/lib/utils"; import { signOut } from "next-auth/react"; @@ -274,6 +274,16 @@ export function Sidebar({ isOpen, onClose, initialLibraries, initialFavorites }:

{t("sidebar.settings.title")}

+