140 lines
4.2 KiB
TypeScript
140 lines
4.2 KiB
TypeScript
|
|
"use client";
|
||
|
|
|
||
|
|
import { useState, useEffect } from "react";
|
||
|
|
import { Loader2, Monitor, Smartphone, Trash2, Globe } from "lucide-react";
|
||
|
|
import { toast } from "sonner";
|
||
|
|
import { Button } from "@/components/ui/button";
|
||
|
|
import { Badge } from "@/components/ui/badge";
|
||
|
|
|
||
|
|
interface Session {
|
||
|
|
id: string;
|
||
|
|
ip_address: string;
|
||
|
|
user_agent: string;
|
||
|
|
created_at: string;
|
||
|
|
expires_at: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface ActiveSessionsProps {
|
||
|
|
userId: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
function parseUserAgent(ua: string): { device: string; browser: string } {
|
||
|
|
const isMobile = /mobile|android|iphone|ipad/i.test(ua);
|
||
|
|
const device = isMobile ? "Mobile" : "Desktop";
|
||
|
|
|
||
|
|
let browser = "Unknown";
|
||
|
|
if (ua.includes("Chrome")) browser = "Chrome";
|
||
|
|
else if (ua.includes("Firefox")) browser = "Firefox";
|
||
|
|
else if (ua.includes("Safari")) browser = "Safari";
|
||
|
|
else if (ua.includes("Edge")) browser = "Edge";
|
||
|
|
|
||
|
|
return { device, browser };
|
||
|
|
}
|
||
|
|
|
||
|
|
export function ActiveSessions({ userId }: ActiveSessionsProps) {
|
||
|
|
const [sessions, setSessions] = useState<Session[]>([]);
|
||
|
|
const [isLoading, setIsLoading] = useState(true);
|
||
|
|
const [revokingId, setRevokingId] = useState<string | null>(null);
|
||
|
|
|
||
|
|
async function fetchSessions() {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/admin/users/${userId}`);
|
||
|
|
if (!response.ok) throw new Error("Failed to fetch sessions");
|
||
|
|
const data = await response.json();
|
||
|
|
setSessions(data.sessions || []);
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Error fetching sessions:", error);
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
fetchSessions();
|
||
|
|
}, [userId]);
|
||
|
|
|
||
|
|
async function handleRevoke(sessionId: string) {
|
||
|
|
setRevokingId(sessionId);
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/admin/users/${userId}/sessions/${sessionId}`, {
|
||
|
|
method: "DELETE",
|
||
|
|
});
|
||
|
|
|
||
|
|
if (!response.ok) throw new Error("Failed to revoke session");
|
||
|
|
|
||
|
|
toast.success("Session revoked");
|
||
|
|
fetchSessions();
|
||
|
|
} catch (error) {
|
||
|
|
toast.error("Failed to revoke session");
|
||
|
|
} finally {
|
||
|
|
setRevokingId(null);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (isLoading) {
|
||
|
|
return (
|
||
|
|
<div className="flex justify-center py-8">
|
||
|
|
<Loader2 className="h-6 w-6 animate-spin" />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (sessions.length === 0) {
|
||
|
|
return (
|
||
|
|
<p className="text-center py-8 text-muted-foreground">
|
||
|
|
No active sessions
|
||
|
|
</p>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="space-y-4">
|
||
|
|
{sessions.map((session) => {
|
||
|
|
const { device, browser } = parseUserAgent(session.user_agent || "");
|
||
|
|
const isExpired = new Date(session.expires_at) < new Date();
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
key={session.id}
|
||
|
|
className="flex items-center justify-between p-4 border rounded-lg"
|
||
|
|
>
|
||
|
|
<div className="flex items-center gap-4">
|
||
|
|
<div className="h-10 w-10 rounded-full bg-muted flex items-center justify-center">
|
||
|
|
{device === "Mobile" ? (
|
||
|
|
<Smartphone className="h-5 w-5 text-muted-foreground" />
|
||
|
|
) : (
|
||
|
|
<Monitor className="h-5 w-5 text-muted-foreground" />
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<div>
|
||
|
|
<div className="flex items-center gap-2">
|
||
|
|
<span className="font-medium">{browser} on {device}</span>
|
||
|
|
{isExpired && <Badge variant="outline">Expired</Badge>}
|
||
|
|
</div>
|
||
|
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||
|
|
<Globe className="h-3 w-3" />
|
||
|
|
{session.ip_address || "Unknown IP"}
|
||
|
|
<span>•</span>
|
||
|
|
{new Date(session.created_at).toLocaleDateString()}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
<Button
|
||
|
|
variant="ghost"
|
||
|
|
size="sm"
|
||
|
|
onClick={() => handleRevoke(session.id)}
|
||
|
|
disabled={revokingId === session.id}
|
||
|
|
>
|
||
|
|
{revokingId === session.id ? (
|
||
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||
|
|
) : (
|
||
|
|
<Trash2 className="h-4 w-4" />
|
||
|
|
)}
|
||
|
|
</Button>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|