wulf-pulse/components/settings/active-sessions.tsx
root 9f912aed24 feat: add authentication, user management, and admin features
Added comprehensive authentication and authorization system:

Authentication System:
- Better Auth integration with session management
- Login/logout pages and API routes
- Middleware for route protection
- Auth utilities and client libraries

User Management:
- User list, detail, and invite pages
- User API endpoints (CRUD operations)
- Session management for users
- Profile settings page

Role-Based Access Control:
- Role management pages (list, create, edit)
- Permission system with granular controls
- Role assignment to users
- Role API endpoints

Admin Features:
- Audit log page for tracking system events
- Admin settings page
- Audit service for logging user actions

Additional Features:
- Quotes management pages and components
- SalesBldr API integration
- Email service for notifications

Configuration & Documentation:
- Updated docker-compose.yml
- MCP server configuration (mcp.json)
- CVE-2025-55182 security review documentation
- Standards guide and PRD documents
- Re-enabling authentication documentation

Database Migrations:
- 012: Auth tables (users, sessions, accounts, verifications)
- 013: Role tables (roles, permissions, role_permissions, user_roles)
- 014: Admin settings table

UI Updates:
- Updated dashboard layout
- Enhanced app layout with auth integration
2026-01-31 12:43:14 -05:00

139 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>
);
}