feat: integrate authentication and password management features, including bcrypt for hashing and NextAuth for session handling

This commit is contained in:
Julien Froidefond
2025-11-30 08:04:06 +01:00
parent 7cb1d5f433
commit d663fbcbd0
30 changed files with 3287 additions and 4164 deletions

View File

@@ -2,7 +2,8 @@
import { useState } from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { signOut } from "next-auth/react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
@@ -16,7 +17,9 @@ import {
ChevronRight,
Settings,
Wand2,
LogOut,
} from "lucide-react";
import { toast } from "sonner";
const navItems = [
{ href: "/", label: "Tableau de bord", icon: LayoutDashboard },
@@ -30,8 +33,21 @@ const navItems = [
export function Sidebar() {
const pathname = usePathname();
const router = useRouter();
const [collapsed, setCollapsed] = useState(false);
const handleSignOut = async () => {
try {
await signOut({ redirect: false });
toast.success("Déconnexion réussie");
router.push("/login");
router.refresh();
} catch (error) {
console.error("Error signing out:", error);
toast.error("Erreur lors de la déconnexion");
}
};
return (
<aside
className={cn(
@@ -82,7 +98,7 @@ export function Sidebar() {
})}
</nav>
<div className="p-2 border-t border-border">
<div className="p-2 border-t border-border space-y-1">
<Link href="/settings">
<Button
variant="ghost"
@@ -95,6 +111,17 @@ export function Sidebar() {
{!collapsed && <span>Paramètres</span>}
</Button>
</Link>
<Button
variant="ghost"
onClick={handleSignOut}
className={cn(
"w-full justify-start gap-3 text-destructive hover:text-destructive hover:bg-destructive/10",
collapsed && "justify-center px-2",
)}
>
<LogOut className="w-5 h-5 shrink-0" />
{!collapsed && <span>Déconnexion</span>}
</Button>
</div>
</aside>
);

View File

@@ -0,0 +1,9 @@
"use client";
import { SessionProvider } from "next-auth/react";
import type { ReactNode } from "react";
export function AuthSessionProvider({ children }: { children: ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}

View File

@@ -2,4 +2,5 @@ export { DataCard } from "./data-card";
export { DangerZoneCard } from "./danger-zone-card";
export { OFXInfoCard } from "./ofx-info-card";
export { BackupCard } from "./backup-card";
export { PasswordCard } from "./password-card";

View File

@@ -0,0 +1,202 @@
"use client";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { Lock, Eye, EyeOff } from "lucide-react";
import { toast } from "sonner";
export function PasswordCard() {
const [oldPassword, setOldPassword] = useState("");
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [showOldPassword, setShowOldPassword] = useState(false);
const [showNewPassword, setShowNewPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const handleChangePassword = async () => {
if (!oldPassword || !newPassword || !confirmPassword) {
toast.error("Veuillez remplir tous les champs");
return;
}
if (newPassword.length < 4) {
toast.error("Le mot de passe doit contenir au moins 4 caractères");
return;
}
if (newPassword !== confirmPassword) {
toast.error("Les mots de passe ne correspondent pas");
return;
}
setLoading(true);
try {
const response = await fetch("/api/auth/change-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
oldPassword,
newPassword,
}),
});
const data = await response.json();
if (data.success) {
toast.success("Mot de passe modifié avec succès");
setOldPassword("");
setNewPassword("");
setConfirmPassword("");
setOpen(false);
} else {
toast.error(data.error || "Erreur lors du changement de mot de passe");
}
} catch (error) {
console.error("Error changing password:", error);
toast.error("Erreur lors du changement de mot de passe");
} finally {
setLoading(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Lock className="w-5 h-5" />
Mot de passe
</CardTitle>
<CardDescription>
Modifiez le mot de passe d'accès à l'application
</CardDescription>
</CardHeader>
<CardContent>
<AlertDialog open={open} onOpenChange={setOpen}>
<AlertDialogTrigger asChild>
<Button variant="outline">Changer le mot de passe</Button>
</AlertDialogTrigger>
<AlertDialogContent className="sm:max-w-md">
<AlertDialogHeader>
<AlertDialogTitle>Changer le mot de passe</AlertDialogTitle>
<AlertDialogDescription>
Entrez votre mot de passe actuel et le nouveau mot de passe.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label htmlFor="old-password">Mot de passe actuel</Label>
<div className="relative">
<Input
id="old-password"
type={showOldPassword ? "text" : "password"}
value={oldPassword}
onChange={(e) => setOldPassword(e.target.value)}
placeholder="Mot de passe actuel"
disabled={loading}
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowOldPassword(!showOldPassword)}
>
{showOldPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="new-password">Nouveau mot de passe</Label>
<div className="relative">
<Input
id="new-password"
type={showNewPassword ? "text" : "password"}
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
placeholder="Nouveau mot de passe"
disabled={loading}
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowNewPassword(!showNewPassword)}
>
{showNewPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="confirm-password">Confirmer le mot de passe</Label>
<div className="relative">
<Input
id="confirm-password"
type={showConfirmPassword ? "text" : "password"}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
placeholder="Confirmer le mot de passe"
disabled={loading}
/>
<Button
type="button"
variant="ghost"
size="sm"
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
>
{showConfirmPassword ? (
<EyeOff className="h-4 w-4" />
) : (
<Eye className="h-4 w-4" />
)}
</Button>
</div>
</div>
</div>
<AlertDialogFooter>
<AlertDialogCancel disabled={loading}>Annuler</AlertDialogCancel>
<AlertDialogAction
onClick={handleChangePassword}
disabled={loading}
>
{loading ? "Modification..." : "Modifier"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</CardContent>
</Card>
);
}