wulf-pulse/components/admin/audit/audit-log-table.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

265 lines
9.1 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { Loader2, Filter, Calendar } from "lucide-react";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
interface AuditLog {
id: string;
timestamp: string;
user_id: string | null;
user_email: string | null;
user_name: string | null;
action: string;
resource: string;
resource_id: string | null;
details: Record<string, unknown> | null;
ip_address: string | null;
}
interface Pagination {
page: number;
limit: number;
total: number;
totalPages: number;
}
interface Filters {
actions: string[];
resources: string[];
}
const actionColors: Record<string, string> = {
create: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
update: "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
delete: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
sign_in: "bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200",
sign_out: "bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200",
sign_in_failed: "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200",
ban: "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
unban: "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
role_change: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
};
export function AuditLogTable() {
const router = useRouter();
const searchParams = useSearchParams();
const [logs, setLogs] = useState<AuditLog[]>([]);
const [pagination, setPagination] = useState<Pagination | null>(null);
const [filters, setFilters] = useState<Filters>({ actions: [], resources: [] });
const [isLoading, setIsLoading] = useState(true);
const [actionFilter, setActionFilter] = useState(searchParams.get("action") || "all");
const [resourceFilter, setResourceFilter] = useState(searchParams.get("resource") || "all");
async function fetchLogs() {
setIsLoading(true);
try {
const params = new URLSearchParams();
if (actionFilter && actionFilter !== "all") params.set("action", actionFilter);
if (resourceFilter && resourceFilter !== "all") params.set("resource", resourceFilter);
params.set("page", searchParams.get("page") || "1");
const response = await fetch(`/api/admin/audit-log?${params.toString()}`);
if (!response.ok) throw new Error("Failed to fetch audit logs");
const data = await response.json();
setLogs(data.logs);
setPagination(data.pagination);
setFilters(data.filters);
} catch (error) {
console.error("Error fetching audit logs:", error);
} finally {
setIsLoading(false);
}
}
useEffect(() => {
fetchLogs();
}, [searchParams]);
function handleFilterChange(type: "action" | "resource", value: string) {
const params = new URLSearchParams(searchParams.toString());
if (value && value !== "all") {
params.set(type, value);
} else {
params.delete(type);
}
params.set("page", "1");
if (type === "action") setActionFilter(value);
if (type === "resource") setResourceFilter(value);
router.push(`/admin/audit-log?${params.toString()}`);
}
function formatAction(action: string): string {
return action.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
}
return (
<div className="space-y-4">
{/* Filters */}
<div className="flex gap-4">
<Select value={actionFilter} onValueChange={(v) => handleFilterChange("action", v)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by action" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Actions</SelectItem>
{filters.actions.map((action) => (
<SelectItem key={action} value={action}>
{formatAction(action)}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={resourceFilter} onValueChange={(v) => handleFilterChange("resource", v)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filter by resource" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Resources</SelectItem>
{filters.resources.map((resource) => (
<SelectItem key={resource} value={resource}>
{resource.charAt(0).toUpperCase() + resource.slice(1)}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* Table */}
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Timestamp</TableHead>
<TableHead>User</TableHead>
<TableHead>Action</TableHead>
<TableHead>Resource</TableHead>
<TableHead>Details</TableHead>
<TableHead>IP Address</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8">
<Loader2 className="h-6 w-6 animate-spin mx-auto" />
</TableCell>
</TableRow>
) : logs.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="text-center py-8 text-muted-foreground">
No audit logs found
</TableCell>
</TableRow>
) : (
logs.map((log) => (
<TableRow key={log.id}>
<TableCell className="text-muted-foreground whitespace-nowrap">
{new Date(log.timestamp).toLocaleString()}
</TableCell>
<TableCell>
{log.user_name || log.user_email || "System"}
</TableCell>
<TableCell>
<Badge className={actionColors[log.action] || "bg-gray-100 text-gray-800"}>
{formatAction(log.action)}
</Badge>
</TableCell>
<TableCell className="capitalize">{log.resource}</TableCell>
<TableCell className="max-w-[200px] truncate">
{log.details ? (
<Popover>
<PopoverTrigger asChild>
<Button variant="ghost" size="sm" className="h-auto p-1">
<code className="text-xs">
{JSON.stringify(log.details).slice(0, 50)}...
</code>
</Button>
</PopoverTrigger>
<PopoverContent className="w-80">
<pre className="text-xs overflow-auto max-h-60">
{JSON.stringify(log.details, null, 2)}
</pre>
</PopoverContent>
</Popover>
) : (
"-"
)}
</TableCell>
<TableCell className="text-muted-foreground">
{log.ip_address || "-"}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
{/* Pagination */}
{pagination && pagination.totalPages > 1 && (
<div className="flex items-center justify-between">
<p className="text-sm text-muted-foreground">
Showing {(pagination.page - 1) * pagination.limit + 1} to{" "}
{Math.min(pagination.page * pagination.limit, pagination.total)} of{" "}
{pagination.total} entries
</p>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
disabled={pagination.page === 1}
onClick={() => {
const params = new URLSearchParams(searchParams.toString());
params.set("page", String(pagination.page - 1));
router.push(`/admin/audit-log?${params.toString()}`);
}}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
disabled={pagination.page === pagination.totalPages}
onClick={() => {
const params = new URLSearchParams(searchParams.toString());
params.set("page", String(pagination.page + 1));
router.push(`/admin/audit-log?${params.toString()}`);
}}
>
Next
</Button>
</div>
</div>
)}
</div>
);
}