Add audit log functionality to track user activity and data changes.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: 705f2157-ef97-4fbd-89e4-8c7f2ecaea90
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/7ed01c5f-a82d-405a-b728-b2e3d127c60c/3d6885b0-d066-41b0-8042-fa2b629d3709.jpg
This commit is contained in:
alphaeusmote
2025-04-09 01:56:37 +00:00
parent 067cdacde8
commit c52c024ccc
6 changed files with 896 additions and 13 deletions
+2
View File
@@ -14,6 +14,7 @@ import LdapConnectionsPage from "@/pages/ldap-connections-page";
import LdapQueryBuilderPage from "@/pages/ldap-query-builder-page";
import SettingsPage from "@/pages/settings-page";
import UserManagementPage from "@/pages/user-management-page";
import AuditLogsPage from "@/pages/audit-logs-page";
import { ProtectedRoute } from "./lib/protected-route";
import { Loader2 } from "lucide-react";
@@ -32,6 +33,7 @@ function Router() {
<ProtectedRoute path="/api-tokens" component={ApiTokensPage} />
<ProtectedRoute path="/ldap-connections" component={LdapConnectionsPage} />
<ProtectedRoute path="/ldap-query-builder" component={LdapQueryBuilderPage} />
<ProtectedRoute path="/audit-logs" component={AuditLogsPage} />
<ProtectedRoute path="/settings" component={SettingsPage} />
<ProtectedRoute path="/user-management" component={UserManagementPage} />
<Route component={NotFound} />
+2
View File
@@ -29,6 +29,7 @@ import {
Server,
ShieldAlert,
Filter,
ClipboardList,
} from "lucide-react";
import { useMobile } from "@/hooks/use-mobile";
import { useToast } from "@/hooks/use-toast";
@@ -70,6 +71,7 @@ const menuSections: MenuSection[] = [
{ title: "API Tokens", path: "/api-tokens", icon: <Key className="h-4 w-4" /> },
{ title: "LDAP Connections", path: "/ldap-connections", icon: <Server className="h-4 w-4" /> },
{ title: "LDAP Query Builder", path: "/ldap-query-builder", icon: <Filter className="h-4 w-4" /> },
{ title: "Audit Logs", path: "/audit-logs", icon: <ClipboardList className="h-4 w-4" /> },
{ title: "Settings", path: "/settings", icon: <Settings className="h-4 w-4" /> },
{ title: "User Management", path: "/user-management", icon: <ShieldAlert className="h-4 w-4" /> },
],
+189
View File
@@ -0,0 +1,189 @@
import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { useToast } from "@/hooks/use-toast";
import { Button } from "@/components/ui/button";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Loader2, RefreshCw } from "lucide-react";
import DashboardLayout from "@/layouts/dashboard-layout";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
// Define the audit log type based on schema
interface AuditLog {
id: number;
timestamp: string;
userId: number | null;
action: string;
targetId: string;
details: Record<string, any>;
connectionId: number;
}
// Define the connection type for filtering
interface Connection {
id: number;
name: string;
}
export default function AuditLogsPage() {
const { toast } = useToast();
const [selectedConnection, setSelectedConnection] = useState<string>("all");
// Fetch LDAP connections for the filter dropdown
const { data: connections = [], isLoading: isLoadingConnections } = useQuery<Connection[]>({
queryKey: ["/api/connections"],
staleTime: 60000,
});
// Fetch audit logs with connection filter if needed
const {
data: auditLogs = [],
isLoading: isLoadingLogs,
refetch,
isRefetching
} = useQuery<AuditLog[]>({
queryKey: [
selectedConnection === "all"
? "/api/audit-logs"
: `/api/connections/${selectedConnection}/audit-logs`
],
staleTime: 30000,
});
// Format date for display
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "short",
day: "numeric",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
}).format(date);
};
// Format the action for display
const formatAction = (action: string) => {
return action.replace(/_/g, " ").toLowerCase().replace(/\b\w/g, (c) => c.toUpperCase());
};
// Get the connection name from ID
const getConnectionName = (connectionId: number) => {
const connection = connections.find(c => c.id === connectionId);
return connection ? connection.name : `Connection ${connectionId}`;
};
// Get the appropriate badge color based on action
const getActionColor = (action: string) => {
if (action.includes("ADD") || action.includes("CREATE")) return "bg-green-600";
if (action.includes("REMOVE") || action.includes("DELETE")) return "bg-red-600";
if (action.includes("MOVE") || action.includes("UPDATE")) return "bg-blue-600";
return "bg-gray-600";
};
return (
<DashboardLayout title="Audit Logs">
<div className="container mx-auto py-6">
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">Audit Logs</h1>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Label htmlFor="connection-filter">Connection:</Label>
<Select
value={selectedConnection}
onValueChange={setSelectedConnection}
disabled={isLoadingConnections}
>
<SelectTrigger id="connection-filter" className="w-[200px]">
<SelectValue placeholder="All Connections" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Connections</SelectItem>
{connections.map(connection => (
<SelectItem key={connection.id} value={connection.id.toString()}>
{connection.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
variant="outline"
size="icon"
onClick={() => refetch()}
disabled={isRefetching}
>
{isRefetching ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<RefreshCw className="h-4 w-4" />
)}
</Button>
</div>
</div>
<Card>
<CardHeader>
<CardTitle>Activity Log</CardTitle>
<CardDescription>
A detailed record of all actions performed in the system
</CardDescription>
</CardHeader>
<CardContent>
{(isLoadingLogs || isRefetching) ? (
<div className="flex justify-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-primary" />
</div>
) : auditLogs.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No audit logs found
</div>
) : (
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Timestamp</TableHead>
<TableHead>Action</TableHead>
<TableHead>Target</TableHead>
<TableHead>Connection</TableHead>
<TableHead>Details</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{auditLogs.map((log) => (
<TableRow key={log.id}>
<TableCell className="font-mono text-xs">
{formatDate(log.timestamp)}
</TableCell>
<TableCell>
<Badge className={getActionColor(log.action)}>
{formatAction(log.action)}
</Badge>
</TableCell>
<TableCell className="font-mono text-xs max-w-[200px] truncate">
{log.targetId}
</TableCell>
<TableCell>
{getConnectionName(log.connectionId)}
</TableCell>
<TableCell className="max-w-[300px]">
<pre className="text-xs overflow-x-auto p-2 bg-muted rounded">
{JSON.stringify(log.details, null, 2)}
</pre>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
</DashboardLayout>
);
}