feat: integrate authentication and password management features, including bcrypt for hashing and NextAuth for session handling
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
9
components/providers/session-provider.tsx
Normal file
9
components/providers/session-provider.tsx
Normal 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>;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
202
components/settings/password-card.tsx
Normal file
202
components/settings/password-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user