"use client"; import { useState, useEffect } from "react"; import { motion, AnimatePresence } from "framer-motion"; import { Monitor, Smartphone, Tablet, Globe, MapPin, Clock, ShieldCheck, ShieldX, LogOut, Wifi, ChevronDown, Bot, AlertTriangle, Shield, } from "lucide-react"; import toast from "react-hot-toast"; import Modal from "../ui/Modal"; import Button from "../ui/Button"; import { EmptyState } from "../ui/PageHeader"; interface DeviceSession { id: string; sessionToken: string; ip: string; device: string; browser: string; os: string; country: string; city: string; status: string; aiDecision: string | null; aiReason: string | null; aiRiskLevel: string | null; createdAt: string; endedAt: string | null; isCurrent: boolean; } function DeviceIcon({ device, className = "w-5 h-5" }: { device: string; className?: string }) { if (device === "Mobile") return ; if (device === "Tablet") return ; return ; } function RiskBadge({ level }: { level: string | null }) { if (!level || level === "low") return null; const cls = level === "high" ? "bg-rose-500/10 text-rose-600 dark:text-rose-400 ring-rose-500/20" : "bg-amber-500/10 text-amber-600 dark:text-amber-400 ring-amber-500/20"; return ( {level === "high" ? "Risque élevé" : "Risque moyen"} ); } function formatRelative(dateStr: string): string { const date = new Date(dateStr); const diff = Date.now() - date.getTime(); const mins = Math.floor(diff / 60000); const hours = Math.floor(diff / 3600000); const days = Math.floor(diff / 86400000); if (mins < 1) return "à l'instant"; if (mins < 60) return `il y a ${mins} min`; if (hours < 24) return `il y a ${hours} h`; if (days === 1) return "hier"; if (days < 7) return `il y a ${days} jours`; return date.toLocaleDateString("fr-CA", { day: "numeric", month: "short", year: "numeric" }); } function SessionSkeleton() { return (
{[0, 1, 2].map((i) => (
))}
); } export default function DevicesPanel() { const [sessions, setSessions] = useState([]); const [loading, setLoading] = useState(true); const [revokingId, setRevokingId] = useState(null); const [expandedId, setExpandedId] = useState(null); const [toRevoke, setToRevoke] = useState(null); const fetchSessions = async () => { try { const res = await fetch("/api/auth/devices"); if (res.ok) setSessions(await res.json()); } catch {} setLoading(false); }; useEffect(() => { fetchSessions(); }, []); const handleRevoke = async (session: DeviceSession) => { if (session.isCurrent) { toast.error("Impossible de révoquer la session active"); return; } setRevokingId(session.id); setToRevoke(null); try { const res = await fetch(`/api/auth/devices/${session.id}`, { method: "DELETE" }); if (res.ok) { toast.success("Session révoquée"); setSessions((prev) => prev.map((s) => (s.id === session.id ? { ...s, endedAt: new Date().toISOString() } : s))); } else toast.error("Erreur lors de la révocation"); } catch { toast.error("Erreur réseau"); } setRevokingId(null); }; const refused = sessions.filter((s) => s.status === "refused_by_ai"); const current = sessions.find((s) => s.isCurrent && s.status === "active" && !s.endedAt); const active = sessions.filter((s) => s.status === "active" && !s.endedAt && !s.isCurrent); const past = sessions.filter((s) => (s.status === "active" && s.endedAt) || s.status === "ended"); if (loading) return ; if (sessions.length === 0) { return ; } const SessionCard = ({ s, i }: { s: DeviceSession; i: number }) => { const isRefused = s.status === "refused_by_ai"; const isActive = s.status === "active" && !s.endedAt; const isExpanded = expandedId === s.id; return (
{isRefused ? : }
{s.browser} · {s.os} {isRefused && ( Refusé par IA )} {!isRefused && }
{s.ip} {(s.country || s.city) && ( {[s.city, s.country].filter(Boolean).join(", ")} )} {formatRelative(s.createdAt)} {s.endedAt && déconnecté {formatRelative(s.endedAt)}}
{s.aiReason && ( )} {isActive && !s.isCurrent && ( )}
{isExpanded && s.aiReason && (
Analyse IA — {isRefused ? "connexion refusée" : `risque ${s.aiRiskLevel || "faible"}`} {s.aiReason}
)}
); }; return (
{/* Appareil courant */} {current && (
{current.browser} · {current.os} Cet appareil
{current.ip} {(current.country || current.city) && {[current.city, current.country].filter(Boolean).join(", ")}} connecté {formatRelative(current.createdAt)}
)}
Chaque connexion est analysée en temps réel par l'IA (appareil, emplacement, historique) pour détecter toute activité suspecte.
{refused.length > 0 && (

Connexions refusées ({refused.length})

{refused.map((s, i) => )}
)} {active.length > 0 && (

Autres sessions actives ({active.length})

{active.map((s, i) => )}
)} {past.length > 0 && (

Historique ({past.length})

{past.map((s, i) => )}
)} setToRevoke(null)} title="Révoquer cette session ?" description={toRevoke ? `${toRevoke.browser} · ${toRevoke.os} — ${toRevoke.ip}` : undefined} footer={ <> } >

L'appareil sera déconnecté immédiatement et devra se reconnecter.

); }