"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.
);
}