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
216 lines
6.7 KiB
TypeScript
216 lines
6.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { Loader2, Trash2, Monitor, Smartphone, Globe } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
|
|
interface Session {
|
|
id: string;
|
|
ip_address: string;
|
|
user_agent: string;
|
|
created_at: string;
|
|
expires_at: string;
|
|
}
|
|
|
|
interface UserSessionsProps {
|
|
sessions: Session[];
|
|
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 UserSessions({ sessions, userId }: UserSessionsProps) {
|
|
const router = useRouter();
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
const [showRevokeAllDialog, setShowRevokeAllDialog] = useState(false);
|
|
const [revokingSessionId, setRevokingSessionId] = useState<string | null>(null);
|
|
|
|
async function handleRevokeSession(sessionId: string) {
|
|
setRevokingSessionId(sessionId);
|
|
try {
|
|
const response = await fetch(`/api/admin/users/${userId}/sessions/${sessionId}`, {
|
|
method: "DELETE",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || "Failed to revoke session");
|
|
}
|
|
|
|
toast.success("Session revoked");
|
|
router.refresh();
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : "An error occurred");
|
|
} finally {
|
|
setRevokingSessionId(null);
|
|
}
|
|
}
|
|
|
|
async function handleRevokeAllSessions() {
|
|
setIsLoading(true);
|
|
try {
|
|
const response = await fetch(`/api/admin/users/${userId}/sessions`, {
|
|
method: "DELETE",
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const data = await response.json();
|
|
throw new Error(data.error || "Failed to revoke sessions");
|
|
}
|
|
|
|
toast.success("All sessions revoked");
|
|
router.refresh();
|
|
} catch (error) {
|
|
toast.error(error instanceof Error ? error.message : "An error occurred");
|
|
} finally {
|
|
setIsLoading(false);
|
|
setShowRevokeAllDialog(false);
|
|
}
|
|
}
|
|
|
|
if (sessions.length === 0) {
|
|
return (
|
|
<div className="text-center py-8 text-muted-foreground">
|
|
No active sessions
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<div className="flex justify-end">
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={() => setShowRevokeAllDialog(true)}
|
|
disabled={isLoading}
|
|
>
|
|
Revoke All Sessions
|
|
</Button>
|
|
</div>
|
|
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Device</TableHead>
|
|
<TableHead>IP Address</TableHead>
|
|
<TableHead>Created</TableHead>
|
|
<TableHead>Expires</TableHead>
|
|
<TableHead className="w-[100px]"></TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{sessions.map((session) => {
|
|
const { device, browser } = parseUserAgent(session.user_agent || "");
|
|
const isExpired = new Date(session.expires_at) < new Date();
|
|
|
|
return (
|
|
<TableRow key={session.id}>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
{device === "Mobile" ? (
|
|
<Smartphone className="h-4 w-4 text-muted-foreground" />
|
|
) : (
|
|
<Monitor className="h-4 w-4 text-muted-foreground" />
|
|
)}
|
|
<span>{browser}</span>
|
|
{isExpired && (
|
|
<Badge variant="outline" className="text-xs">Expired</Badge>
|
|
)}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell>
|
|
<div className="flex items-center gap-2">
|
|
<Globe className="h-4 w-4 text-muted-foreground" />
|
|
{session.ip_address || "Unknown"}
|
|
</div>
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground">
|
|
{new Date(session.created_at).toLocaleString()}
|
|
</TableCell>
|
|
<TableCell className="text-muted-foreground">
|
|
{new Date(session.expires_at).toLocaleString()}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => handleRevokeSession(session.id)}
|
|
disabled={revokingSessionId === session.id}
|
|
>
|
|
{revokingSessionId === session.id ? (
|
|
<Loader2 className="h-4 w-4 animate-spin" />
|
|
) : (
|
|
<Trash2 className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
|
|
<AlertDialog open={showRevokeAllDialog} onOpenChange={setShowRevokeAllDialog}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Revoke All Sessions</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to revoke all sessions for this user? They will
|
|
be signed out from all devices.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel disabled={isLoading}>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleRevokeAllSessions}
|
|
disabled={isLoading}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Revoking...
|
|
</>
|
|
) : (
|
|
"Revoke All"
|
|
)}
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
}
|