# ET-AUDIT-FRONTEND: Componentes React para Auditoria ## Identificacion | Campo | Valor | |-------|-------| | **ID** | ET-AUDIT-FRONTEND | | **Modulo** | MGN-007 Audit | | **Version** | 1.0 | | **Estado** | En Diseno | | **Framework** | React 18 + TypeScript | | **UI Library** | shadcn/ui | | **State** | Zustand 4+ | | **Autor** | Requirements-Analyst | | **Fecha** | 2025-12-05 | --- ## Estructura de Archivos ``` apps/frontend/src/modules/audit/ ├── index.ts ├── types/ │ ├── audit-log.types.ts │ ├── access-log.types.ts │ ├── security-event.types.ts │ └── scheduled-report.types.ts ├── stores/ │ ├── audit-logs.store.ts │ ├── access-logs.store.ts │ ├── security-events.store.ts │ └── scheduled-reports.store.ts ├── hooks/ │ ├── useAuditLogs.ts │ ├── useAccessLogs.ts │ ├── useSecurityEvents.ts │ ├── useEntityHistory.ts │ └── useAuditExport.ts ├── components/ │ ├── AuditLogViewer/ │ │ ├── AuditLogViewer.tsx │ │ ├── AuditLogFilters.tsx │ │ ├── AuditLogRow.tsx │ │ ├── AuditLogDetail.tsx │ │ └── AuditDiffViewer.tsx │ ├── AccessLogViewer/ │ │ ├── AccessLogViewer.tsx │ │ ├── AccessLogFilters.tsx │ │ └── AccessLogStats.tsx │ ├── SecurityDashboard/ │ │ ├── SecurityDashboard.tsx │ │ ├── SecurityEventCard.tsx │ │ ├── SecurityEventDetail.tsx │ │ ├── SeverityBadge.tsx │ │ └── SecurityTimeline.tsx │ ├── EntityHistory/ │ │ ├── EntityHistoryPanel.tsx │ │ ├── EntityHistoryTimeline.tsx │ │ └── ChangeCard.tsx │ ├── ScheduledReports/ │ │ ├── ScheduledReportsList.tsx │ │ ├── ScheduledReportForm.tsx │ │ └── CronScheduleBuilder.tsx │ └── shared/ │ ├── DateRangePicker.tsx │ ├── ExportButton.tsx │ ├── JsonViewer.tsx │ └── UserAvatar.tsx ├── pages/ │ ├── AuditLogsPage.tsx │ ├── AccessLogsPage.tsx │ ├── SecurityEventsPage.tsx │ └── ScheduledReportsPage.tsx └── routes.tsx ``` --- ## Tipos TypeScript ### Audit Log Types ```typescript // types/audit-log.types.ts export type AuditAction = 'create' | 'update' | 'delete' | 'restore'; export interface AuditLog { id: string; tenantId: string; userId: string | null; action: AuditAction; entityType: string; entityId: string; oldValues: Record | null; newValues: Record | null; changedFields: string[] | null; ipAddress: string | null; userAgent: string | null; requestId: string | null; createdAt: string; // Joined data user?: { id: string; fullName: string; email: string; avatarUrl?: string; }; } export interface AuditLogFilters { entityType?: string; entityId?: string; userId?: string; action?: AuditAction; dateFrom?: string; dateTo?: string; search?: string; } export interface AuditDiff { field: string; oldValue: any; newValue: any; type: 'added' | 'removed' | 'changed'; } export interface EntityStats { entityType: string; count: number; } export interface ActionStats { action: AuditAction; count: number; } ``` ### Security Event Types ```typescript // types/security-event.types.ts export type SecuritySeverity = 'low' | 'medium' | 'high' | 'critical'; export type SecurityEventType = | 'login_failed' | 'login_blocked' | 'brute_force_detected' | 'password_changed' | 'password_reset_requested' | 'session_hijack_attempt' | 'concurrent_session' | 'session_from_new_location' | 'session_from_new_device' | 'permission_escalation' | 'role_changed' | 'admin_created' | 'mass_delete_attempt' | 'data_export' | 'sensitive_data_access' | 'api_key_created' | 'webhook_modified' | 'settings_changed'; export interface SecurityEvent { id: string; tenantId: string | null; userId: string | null; eventType: SecurityEventType; severity: SecuritySeverity; title: string; description: string | null; metadata: Record; ipAddress: string | null; countryCode: string | null; isResolved: boolean; resolvedBy: string | null; resolvedAt: string | null; resolutionNotes: string | null; createdAt: string; // Joined data user?: { id: string; fullName: string; email: string; }; resolvedByUser?: { id: string; fullName: string; }; } export interface SecurityEventFilters { severity?: SecuritySeverity; eventType?: SecurityEventType; isResolved?: boolean; dateFrom?: string; userId?: string; } export interface SeverityCounts { low: number; medium: number; high: number; critical: number; total: number; } ``` ### Access Log Types ```typescript // types/access-log.types.ts export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; export interface AccessLog { id: string; tenantId: string; userId: string | null; sessionId: string | null; requestId: string; method: HttpMethod; path: string; queryParams: Record | null; statusCode: number; responseTimeMs: number; ipAddress: string; userAgent: string | null; referer: string | null; createdAt: string; // Joined data user?: { id: string; fullName: string; email: string; }; } export interface AccessLogFilters { userId?: string; path?: string; method?: HttpMethod; statusCode?: number; minResponseTime?: number; dateFrom?: string; dateTo?: string; } export interface AccessStats { period: string; totalRequests: number; avgResponseTime: number; errorCount: number; } export interface EndpointStats { method: HttpMethod; path: string; count: number; avgResponseTime: number; } ``` ### Scheduled Report Types ```typescript // types/scheduled-report.types.ts export type AuditReportType = 'activity' | 'security' | 'compliance' | 'change' | 'access'; export type ExportFormat = 'csv' | 'xlsx' | 'pdf' | 'json'; export interface ScheduledReport { id: string; tenantId: string; name: string; reportType: AuditReportType; filters: Record; format: ExportFormat; schedule: string; // cron expression recipients: string[]; isActive: boolean; lastRunAt: string | null; nextRunAt: string | null; createdBy: string; createdAt: string; } export interface CreateScheduledReportDto { name: string; reportType: AuditReportType; filters: Record; format: ExportFormat; schedule: string; recipients: string[]; } ``` --- ## Stores (Zustand) ### Audit Logs Store ```typescript // stores/audit-logs.store.ts import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { auditApi } from '../api/audit.api'; import type { AuditLog, AuditLogFilters, ActionStats, EntityStats } from '../types'; interface AuditLogsState { // Data logs: AuditLog[]; selectedLog: AuditLog | null; entityHistory: AuditLog[]; actionStats: ActionStats[]; entityStats: EntityStats[]; // UI State filters: AuditLogFilters; isLoading: boolean; isExporting: boolean; error: string | null; // Pagination page: number; limit: number; total: number; // Actions fetchLogs: () => Promise; fetchEntityHistory: (entityType: string, entityId: string) => Promise; fetchStats: (dateFrom: Date, dateTo: Date) => Promise; setFilters: (filters: Partial) => void; clearFilters: () => void; setPage: (page: number) => void; selectLog: (log: AuditLog | null) => void; exportLogs: (format: 'csv' | 'xlsx' | 'pdf') => Promise; } export const useAuditLogsStore = create()( devtools( (set, get) => ({ // Initial State logs: [], selectedLog: null, entityHistory: [], actionStats: [], entityStats: [], filters: {}, isLoading: false, isExporting: false, error: null, page: 1, limit: 50, total: 0, fetchLogs: async () => { const { filters, page, limit } = get(); set({ isLoading: true, error: null }); try { const response = await auditApi.getLogs({ ...filters, page, limit, }); set({ logs: response.items, total: response.total, isLoading: false, }); } catch (error) { set({ error: error instanceof Error ? error.message : 'Failed to fetch logs', isLoading: false, }); } }, fetchEntityHistory: async (entityType: string, entityId: string) => { set({ isLoading: true, error: null }); try { const history = await auditApi.getEntityHistory(entityType, entityId); set({ entityHistory: history, isLoading: false }); } catch (error) { set({ error: error instanceof Error ? error.message : 'Failed to fetch history', isLoading: false, }); } }, fetchStats: async (dateFrom: Date, dateTo: Date) => { try { const [actionStats, entityStats] = await Promise.all([ auditApi.getStatsByAction(dateFrom, dateTo), auditApi.getStatsByEntity(dateFrom, dateTo), ]); set({ actionStats, entityStats }); } catch (error) { console.error('Failed to fetch stats:', error); } }, setFilters: (newFilters) => { set((state) => ({ filters: { ...state.filters, ...newFilters }, page: 1, // Reset to first page })); get().fetchLogs(); }, clearFilters: () => { set({ filters: {}, page: 1 }); get().fetchLogs(); }, setPage: (page) => { set({ page }); get().fetchLogs(); }, selectLog: (log) => set({ selectedLog: log }), exportLogs: async (format) => { const { filters } = get(); set({ isExporting: true }); try { const blob = await auditApi.exportLogs({ ...filters, format }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `audit-logs-${new Date().toISOString().split('T')[0]}.${format}`; a.click(); URL.revokeObjectURL(url); } finally { set({ isExporting: false }); } }, }), { name: 'audit-logs-store' } ) ); ``` ### Security Events Store ```typescript // stores/security-events.store.ts import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { auditApi } from '../api/audit.api'; import type { SecurityEvent, SecurityEventFilters, SeverityCounts } from '../types'; interface SecurityEventsState { // Data events: SecurityEvent[]; selectedEvent: SecurityEvent | null; counts: SeverityCounts; // UI State filters: SecurityEventFilters; isLoading: boolean; isResolving: boolean; error: string | null; // Pagination page: number; limit: number; total: number; // Actions fetchEvents: () => Promise; fetchCounts: () => Promise; setFilters: (filters: Partial) => void; clearFilters: () => void; setPage: (page: number) => void; selectEvent: (event: SecurityEvent | null) => void; resolveEvent: (id: string, notes?: string) => Promise; } export const useSecurityEventsStore = create()( devtools( (set, get) => ({ // Initial State events: [], selectedEvent: null, counts: { low: 0, medium: 0, high: 0, critical: 0, total: 0 }, filters: { isResolved: false }, isLoading: false, isResolving: false, error: null, page: 1, limit: 25, total: 0, fetchEvents: async () => { const { filters, page, limit } = get(); set({ isLoading: true, error: null }); try { const response = await auditApi.getSecurityEvents({ ...filters, page, limit, }); set({ events: response.items, total: response.total, isLoading: false, }); } catch (error) { set({ error: error instanceof Error ? error.message : 'Failed to fetch events', isLoading: false, }); } }, fetchCounts: async () => { try { const counts = await auditApi.getSecurityCounts(); set({ counts: { ...counts, total: counts.low + counts.medium + counts.high + counts.critical, }, }); } catch (error) { console.error('Failed to fetch counts:', error); } }, setFilters: (newFilters) => { set((state) => ({ filters: { ...state.filters, ...newFilters }, page: 1, })); get().fetchEvents(); }, clearFilters: () => { set({ filters: { isResolved: false }, page: 1 }); get().fetchEvents(); }, setPage: (page) => { set({ page }); get().fetchEvents(); }, selectEvent: (event) => set({ selectedEvent: event }), resolveEvent: async (id: string, notes?: string) => { set({ isResolving: true }); try { await auditApi.resolveSecurityEvent(id, { notes }); // Update local state set((state) => ({ events: state.events.map((e) => e.id === id ? { ...e, isResolved: true, resolutionNotes: notes || null } : e ), selectedEvent: state.selectedEvent?.id === id ? { ...state.selectedEvent, isResolved: true, resolutionNotes: notes || null } : state.selectedEvent, })); // Refresh counts get().fetchCounts(); } finally { set({ isResolving: false }); } }, }), { name: 'security-events-store' } ) ); ``` ### Access Logs Store ```typescript // stores/access-logs.store.ts import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; import { auditApi } from '../api/audit.api'; import type { AccessLog, AccessLogFilters, AccessStats, EndpointStats } from '../types'; interface AccessLogsState { // Data logs: AccessLog[]; stats: AccessStats[]; topEndpoints: EndpointStats[]; // UI State filters: AccessLogFilters; isLoading: boolean; error: string | null; // Chart settings groupBy: 'hour' | 'day'; // Pagination page: number; limit: number; total: number; // Actions fetchLogs: () => Promise; fetchStats: (dateFrom: Date, dateTo: Date) => Promise; fetchTopEndpoints: (dateFrom: Date, limit?: number) => Promise; setFilters: (filters: Partial) => void; clearFilters: () => void; setGroupBy: (groupBy: 'hour' | 'day') => void; setPage: (page: number) => void; } export const useAccessLogsStore = create()( devtools( (set, get) => ({ // Initial State logs: [], stats: [], topEndpoints: [], filters: {}, isLoading: false, error: null, groupBy: 'hour', page: 1, limit: 100, total: 0, fetchLogs: async () => { const { filters, page, limit } = get(); set({ isLoading: true, error: null }); try { const response = await auditApi.getAccessLogs({ ...filters, page, limit, }); set({ logs: response.items, total: response.total, isLoading: false, }); } catch (error) { set({ error: error instanceof Error ? error.message : 'Failed to fetch logs', isLoading: false, }); } }, fetchStats: async (dateFrom: Date, dateTo: Date) => { const { groupBy } = get(); try { const stats = await auditApi.getAccessStats(dateFrom, dateTo, groupBy); set({ stats }); } catch (error) { console.error('Failed to fetch stats:', error); } }, fetchTopEndpoints: async (dateFrom: Date, limit = 10) => { try { const topEndpoints = await auditApi.getTopEndpoints(dateFrom, limit); set({ topEndpoints }); } catch (error) { console.error('Failed to fetch top endpoints:', error); } }, setFilters: (newFilters) => { set((state) => ({ filters: { ...state.filters, ...newFilters }, page: 1, })); get().fetchLogs(); }, clearFilters: () => { set({ filters: {}, page: 1 }); get().fetchLogs(); }, setGroupBy: (groupBy) => set({ groupBy }), setPage: (page) => { set({ page }); get().fetchLogs(); }, }), { name: 'access-logs-store' } ) ); ``` --- ## Hooks Personalizados ### useEntityHistory ```typescript // hooks/useEntityHistory.ts import { useEffect, useCallback } from 'react'; import { useAuditLogsStore } from '../stores/audit-logs.store'; interface UseEntityHistoryOptions { entityType: string; entityId: string; autoFetch?: boolean; } export function useEntityHistory(options: UseEntityHistoryOptions) { const { entityType, entityId, autoFetch = true } = options; const { entityHistory, isLoading, error, fetchEntityHistory, } = useAuditLogsStore(); const fetch = useCallback(() => { fetchEntityHistory(entityType, entityId); }, [entityType, entityId, fetchEntityHistory]); useEffect(() => { if (autoFetch && entityType && entityId) { fetch(); } }, [autoFetch, entityType, entityId, fetch]); return { history: entityHistory, isLoading, error, refetch: fetch, }; } ``` ### useAuditExport ```typescript // hooks/useAuditExport.ts import { useState, useCallback } from 'react'; import { auditApi } from '../api/audit.api'; import type { AuditLogFilters, ExportFormat } from '../types'; export function useAuditExport() { const [isExporting, setIsExporting] = useState(false); const [error, setError] = useState(null); const exportLogs = useCallback(async ( filters: AuditLogFilters, format: ExportFormat, filename?: string ) => { setIsExporting(true); setError(null); try { const blob = await auditApi.exportLogs({ ...filters, format }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename || `audit-export-${Date.now()}.${format}`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); return true; } catch (err) { setError(err instanceof Error ? err.message : 'Export failed'); return false; } finally { setIsExporting(false); } }, []); return { exportLogs, isExporting, error, }; } ``` ### useSecurityAlerts ```typescript // hooks/useSecurityAlerts.ts import { useEffect, useRef } from 'react'; import { useSecurityEventsStore } from '../stores/security-events.store'; import { useToast } from '@/components/ui/use-toast'; export function useSecurityAlerts() { const { counts, fetchCounts } = useSecurityEventsStore(); const { toast } = useToast(); const previousCritical = useRef(counts.critical); // Poll for new events useEffect(() => { const interval = setInterval(() => { fetchCounts(); }, 30000); // Every 30 seconds return () => clearInterval(interval); }, [fetchCounts]); // Show toast when critical count increases useEffect(() => { if (counts.critical > previousCritical.current) { const newEvents = counts.critical - previousCritical.current; toast({ variant: 'destructive', title: 'Critical Security Alert', description: `${newEvents} new critical security event${newEvents > 1 ? 's' : ''} detected`, }); } previousCritical.current = counts.critical; }, [counts.critical, toast]); return { hasUnresolvedCritical: counts.critical > 0, hasUnresolvedHigh: counts.high > 0, totalUnresolved: counts.total, counts, }; } ``` --- ## Componentes React ### AuditLogViewer ```tsx // components/AuditLogViewer/AuditLogViewer.tsx import { useEffect } from 'react'; import { useAuditLogsStore } from '../../stores/audit-logs.store'; import { AuditLogFilters } from './AuditLogFilters'; import { AuditLogRow } from './AuditLogRow'; import { AuditLogDetail } from './AuditLogDetail'; import { Table, TableBody, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { Pagination } from '@/components/ui/pagination'; import { Sheet, SheetContent } from '@/components/ui/sheet'; import { Skeleton } from '@/components/ui/skeleton'; export function AuditLogViewer() { const { logs, selectedLog, isLoading, error, page, limit, total, fetchLogs, setPage, selectLog, } = useAuditLogsStore(); useEffect(() => { fetchLogs(); }, [fetchLogs]); const totalPages = Math.ceil(total / limit); if (error) { return (
Error: {error}
); } return (
Timestamp Action Entity User IP Address {isLoading ? ( Array.from({ length: 10 }).map((_, i) => ( )) ) : logs.length === 0 ? ( ) : ( logs.map((log) => ( selectLog(log)} isSelected={selectedLog?.id === log.id} /> )) )}
No audit logs found
{totalPages > 1 && ( )} selectLog(null)}> {selectedLog && }
); } ``` ### AuditDiffViewer ```tsx // components/AuditLogViewer/AuditDiffViewer.tsx import { useMemo } from 'react'; import { cn } from '@/lib/utils'; import type { AuditDiff } from '../../types'; interface AuditDiffViewerProps { oldValues: Record | null; newValues: Record | null; changedFields?: string[] | null; } export function AuditDiffViewer({ oldValues, newValues, changedFields, }: AuditDiffViewerProps) { const diffs = useMemo(() => { const result: AuditDiff[] = []; const fields = changedFields || Object.keys({ ...oldValues, ...newValues }); for (const field of fields) { const oldVal = oldValues?.[field]; const newVal = newValues?.[field]; if (oldVal === undefined && newVal !== undefined) { result.push({ field, oldValue: null, newValue: newVal, type: 'added' }); } else if (oldVal !== undefined && newVal === undefined) { result.push({ field, oldValue: oldVal, newValue: null, type: 'removed' }); } else if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) { result.push({ field, oldValue: oldVal, newValue: newVal, type: 'changed' }); } } return result; }, [oldValues, newValues, changedFields]); if (diffs.length === 0) { return (
No changes detected
); } return (
{diffs.map((diff) => (
{diff.field} ({diff.type})
Before {diff.oldValue !== null ? formatValue(diff.oldValue) : '(empty)'}
After {diff.newValue !== null ? formatValue(diff.newValue) : '(empty)'}
))}
); } function formatValue(value: any): string { if (typeof value === 'object') { return JSON.stringify(value, null, 2); } return String(value); } ``` ### SecurityDashboard ```tsx // components/SecurityDashboard/SecurityDashboard.tsx import { useEffect } from 'react'; import { useSecurityEventsStore } from '../../stores/security-events.store'; import { SecurityEventCard } from './SecurityEventCard'; import { SecurityEventDetail } from './SecurityEventDetail'; import { SeverityBadge } from './SeverityBadge'; import { SecurityTimeline } from './SecurityTimeline'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Sheet, SheetContent } from '@/components/ui/sheet'; import { Badge } from '@/components/ui/badge'; import { AlertTriangle, Shield, ShieldCheck, ShieldX } from 'lucide-react'; export function SecurityDashboard() { const { events, selectedEvent, counts, filters, isLoading, fetchEvents, fetchCounts, setFilters, selectEvent, resolveEvent, } = useSecurityEventsStore(); useEffect(() => { fetchEvents(); fetchCounts(); }, [fetchEvents, fetchCounts]); return (
{/* Summary Cards */}
{/* Tabs for filtering */} setFilters({ isResolved: v === 'unresolved' ? false : undefined, })} > Unresolved {counts.total > 0 && ( {counts.total} )} All Events
{events.map((event) => ( selectEvent(event)} /> ))}
{/* Detail Sheet */} selectEvent(null)}> {selectedEvent && ( resolveEvent(selectedEvent.id, notes)} /> )}
); } interface SeverityCardProps { title: string; count: number; icon: React.ElementType; variant: 'critical' | 'high' | 'medium' | 'low'; } function SeverityCard({ title, count, icon: Icon, variant }: SeverityCardProps) { const colors = { critical: 'text-red-500 bg-red-50 dark:bg-red-950', high: 'text-orange-500 bg-orange-50 dark:bg-orange-950', medium: 'text-yellow-500 bg-yellow-50 dark:bg-yellow-950', low: 'text-blue-500 bg-blue-50 dark:bg-blue-950', }; return ( 0 ? colors[variant] : ''}> {title}
{count}

unresolved events

); } ``` ### EntityHistoryPanel ```tsx // components/EntityHistory/EntityHistoryPanel.tsx import { useEntityHistory } from '../../hooks/useEntityHistory'; import { EntityHistoryTimeline } from './EntityHistoryTimeline'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Skeleton } from '@/components/ui/skeleton'; import { History, RefreshCw } from 'lucide-react'; interface EntityHistoryPanelProps { entityType: string; entityId: string; title?: string; } export function EntityHistoryPanel({ entityType, entityId, title = 'Change History', }: EntityHistoryPanelProps) { const { history, isLoading, error, refetch } = useEntityHistory({ entityType, entityId, }); return ( {title} {isLoading ? (
{Array.from({ length: 3 }).map((_, i) => ( ))}
) : error ? (
{error}
) : history.length === 0 ? (
No changes recorded for this entity
) : ( )}
); } ``` ### AccessLogStats ```tsx // components/AccessLogViewer/AccessLogStats.tsx import { useMemo } from 'react'; import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, BarElement, Title, Tooltip, Legend, Filler, } from 'chart.js'; import { Line, Bar } from 'react-chartjs-2'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import type { AccessStats, EndpointStats } from '../../types'; ChartJS.register( CategoryScale, LinearScale, PointElement, LineElement, BarElement, Title, Tooltip, Legend, Filler ); interface AccessLogStatsProps { stats: AccessStats[]; topEndpoints: EndpointStats[]; } export function AccessLogStats({ stats, topEndpoints }: AccessLogStatsProps) { const requestsChartData = useMemo(() => ({ labels: stats.map((s) => s.period), datasets: [ { label: 'Total Requests', data: stats.map((s) => s.totalRequests), borderColor: 'hsl(var(--primary))', backgroundColor: 'hsl(var(--primary) / 0.1)', fill: true, tension: 0.4, }, { label: 'Errors', data: stats.map((s) => s.errorCount), borderColor: 'hsl(var(--destructive))', backgroundColor: 'hsl(var(--destructive) / 0.1)', fill: true, tension: 0.4, }, ], }), [stats]); const responseTimeChartData = useMemo(() => ({ labels: stats.map((s) => s.period), datasets: [ { label: 'Avg Response Time (ms)', data: stats.map((s) => Math.round(s.avgResponseTime)), borderColor: 'hsl(var(--chart-2))', backgroundColor: 'hsl(var(--chart-2) / 0.5)', tension: 0.4, }, ], }), [stats]); const endpointsChartData = useMemo(() => ({ labels: topEndpoints.map((e) => `${e.method} ${e.path}`), datasets: [ { label: 'Request Count', data: topEndpoints.map((e) => e.count), backgroundColor: 'hsl(var(--primary) / 0.8)', }, ], }), [topEndpoints]); return (
Request Volume
Response Time
Top Endpoints
); } ``` ### SeverityBadge ```tsx // components/SecurityDashboard/SeverityBadge.tsx import { Badge } from '@/components/ui/badge'; import { cn } from '@/lib/utils'; import type { SecuritySeverity } from '../../types'; interface SeverityBadgeProps { severity: SecuritySeverity; className?: string; } const severityConfig: Record = { critical: { label: 'Critical', className: 'bg-red-500 hover:bg-red-600 text-white', }, high: { label: 'High', className: 'bg-orange-500 hover:bg-orange-600 text-white', }, medium: { label: 'Medium', className: 'bg-yellow-500 hover:bg-yellow-600 text-black', }, low: { label: 'Low', className: 'bg-blue-500 hover:bg-blue-600 text-white', }, }; export function SeverityBadge({ severity, className }: SeverityBadgeProps) { const config = severityConfig[severity]; return ( {config.label} ); } ``` ### JsonViewer ```tsx // components/shared/JsonViewer.tsx import { useState } from 'react'; import { ChevronDown, ChevronRight, Copy, Check } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; interface JsonViewerProps { data: any; expanded?: boolean; maxHeight?: string; } export function JsonViewer({ data, expanded = false, maxHeight = '400px', }: JsonViewerProps) { const [isExpanded, setIsExpanded] = useState(expanded); const [copied, setCopied] = useState(false); const formatted = JSON.stringify(data, null, 2); const handleCopy = async () => { await navigator.clipboard.writeText(formatted); setCopied(true); setTimeout(() => setCopied(false), 2000); }; const preview = JSON.stringify(data).slice(0, 50) + (JSON.stringify(data).length > 50 ? '...' : ''); return (
setIsExpanded(!isExpanded)} > {isExpanded ? ( ) : ( )} {isExpanded ? 'JSON Data' : preview}
{isExpanded && (
            {formatted}
          
)}
); } ``` --- ## Paginas ### AuditLogsPage ```tsx // pages/AuditLogsPage.tsx import { AuditLogViewer } from '../components/AuditLogViewer/AuditLogViewer'; import { ExportButton } from '../components/shared/ExportButton'; import { useAuditLogsStore } from '../stores/audit-logs.store'; import { PageHeader } from '@/components/layout/PageHeader'; export function AuditLogsPage() { const { filters, isExporting, exportLogs } = useAuditLogsStore(); return (
exportLogs(format)} isExporting={isExporting} /> } />
); } ``` ### SecurityEventsPage ```tsx // pages/SecurityEventsPage.tsx import { SecurityDashboard } from '../components/SecurityDashboard/SecurityDashboard'; import { useSecurityAlerts } from '../hooks/useSecurityAlerts'; import { PageHeader } from '@/components/layout/PageHeader'; import { Badge } from '@/components/ui/badge'; import { AlertTriangle } from 'lucide-react'; export function SecurityEventsPage() { const { hasUnresolvedCritical, totalUnresolved } = useSecurityAlerts(); return (
Security Events {hasUnresolvedCritical && ( Critical )} } description={ totalUnresolved > 0 ? `${totalUnresolved} unresolved security events require attention` : 'All security events have been resolved' } />
); } ``` --- ## Rutas ```tsx // routes.tsx import { lazy } from 'react'; import type { RouteObject } from 'react-router-dom'; const AuditLogsPage = lazy(() => import('./pages/AuditLogsPage')); const AccessLogsPage = lazy(() => import('./pages/AccessLogsPage')); const SecurityEventsPage = lazy(() => import('./pages/SecurityEventsPage')); const ScheduledReportsPage = lazy(() => import('./pages/ScheduledReportsPage')); export const auditRoutes: RouteObject[] = [ { path: 'audit', children: [ { path: 'logs', element: , }, { path: 'access', element: , }, { path: 'security', element: , }, { path: 'reports', element: , }, ], }, ]; ``` --- ## Wireframes ### Audit Logs Viewer ``` +------------------------------------------------------------------+ | Audit Logs [Export v] | | View and search entity changes across the system | +------------------------------------------------------------------+ | Filters: | | [Entity Type v] [Action v] [User v] [Date From] [Date To] [Clear] | +------------------------------------------------------------------+ | Timestamp | Action | Entity | User | IP | |--------------------|---------|--------------------|---------|-----| | 2025-12-05 14:32 | create | Contact: John Doe | Admin | ... | | 2025-12-05 14:30 | update | User: jane@... | Jane | ... | | 2025-12-05 14:28 | delete | Product: Widget | Admin | ... | | 2025-12-05 14:25 | update | Setting: timezone | System | ... | +------------------------------------------------------------------+ | < 1 2 3 ... 10 > | +------------------------------------------------------------------+ +-- Detail Sheet (on row click) ---+ | Audit Log Detail | |----------------------------------| | Action: UPDATE | | Entity: Contact | | Entity ID: abc-123... | | User: admin@company.com | | Timestamp: 2025-12-05 14:30:22 | | IP: 192.168.1.100 | | | | Changes: | | +------------------------------+ | | | email (changed) | | | | Before: old@email.com | | | | After: new@email.com | | | +------------------------------+ | | | phone (added) | | | | Before: (empty) | | | | After: +1-555-1234 | | | +------------------------------+ | +----------------------------------+ ``` ### Security Events Dashboard ``` +------------------------------------------------------------------+ | Security Events [! Critical] | | 5 unresolved security events require attention | +------------------------------------------------------------------+ | +------------+ +------------+ +------------+ +------------+ | | | CRITICAL | | HIGH | | MEDIUM | | LOW | | | | 2 | | 1 | | 2 | | 0 | | | | unresolved | | unresolved | | unresolved | | unresolved | | | +------------+ +------------+ +------------+ +------------+ | +------------------------------------------------------------------+ | [Unresolved (5)] [All Events] | +------------------------------------------------------------------+ | | | +------------------------+ +------------------------+ | | | [!] CRITICAL | | [!] CRITICAL | | | | Brute Force Detected | | Session Hijack Attempt | | | | user@company.com | | admin@company.com | | | | 192.168.1.50 | | Unknown Location | | | | 2 minutes ago | | 5 minutes ago | | | +------------------------+ +------------------------+ | | | | +------------------------+ | | | [/\] HIGH | | | | New Admin Created | | | | By: admin@company.com | | | | 10 minutes ago | | | +------------------------+ | +------------------------------------------------------------------+ +-- Event Detail Sheet -------------+ | Security Event | |-----------------------------------| | [!] CRITICAL | | Brute Force Detected | | | | 5 failed login attempts in 60s | | Target: user@company.com | | Source IP: 192.168.1.50 | | Country: Unknown | | Time: 2025-12-05 14:30:22 | | | | Metadata: | | { | | "attempts": 5, | | "threshold": 5, | | "window": "60s" | | } | | | | [Resolve Event] | | +-------------------------------+ | | | Resolution Notes: | | | | ____________________________ | | | | IP blocked and user notified | | | | ____________________________ | | | +-------------------------------+ | +-----------------------------------+ ``` ### Access Logs Statistics ``` +------------------------------------------------------------------+ | Access Logs | | API request history and performance metrics | +------------------------------------------------------------------+ | [Stats] [Raw Logs] [Last 24h v] [Hourly v] | +------------------------------------------------------------------+ | | | Request Volume | | +--------------------------------------------------------------+ | | | /\ | | | | / \ /\ ___Requests | | | | / \ / \ / | | | | / \/ \__/ ___Errors | | | +--------------------------------------------------------------+ | | | 00:00 04:00 08:00 12:00 16:00 20:00 24:00 | | | | +------------------------------------------------------------------+ | Response Time | Top Endpoints | | +----------------------------+ | +----------------------------+ | | | ___ | | | GET /api/users 1.2k | | | | / \ | | | POST /api/auth 890 | | | | / \__/\ | | | GET /api/contacts 756 | | | | / \ | | | PUT /api/settings 432 | | | +----------------------------+ | | DELETE /api/items 210 | | | Avg: 45ms Max: 320ms | +----------------------------+ | +------------------------------------------------------------------+ ``` ### Entity History Panel (Reusable) ``` +----------------------------------------+ | Change History [R] | +----------------------------------------+ | | | +-- 2025-12-05 14:30 ----------------+ | | | UPDATE by admin@company.com | | | | Changed: email, phone | | | | +--------------------------------+ | | | | | email: old@... -> new@... | | | | | | phone: (empty) -> +1-555-1234 | | | | | +--------------------------------+ | | | +------------------------------------+ | | | | +-- 2025-12-05 10:15 ----------------+ | | | UPDATE by jane@company.com | | | | Changed: name | | | | +--------------------------------+ | | | | | name: John -> John Doe | | | | | +--------------------------------+ | | | +------------------------------------+ | | | | +-- 2025-12-04 09:00 ----------------+ | | | CREATE by admin@company.com | | | | Initial creation | | | +------------------------------------+ | | | +----------------------------------------+ ``` --- ## Dependencias Frontend ```json { "dependencies": { "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.20.0", "zustand": "^4.4.0", "@tanstack/react-query": "^5.0.0", "chart.js": "^4.4.0", "react-chartjs-2": "^5.2.0", "date-fns": "^2.30.0", "lucide-react": "^0.294.0", "clsx": "^2.0.0", "tailwind-merge": "^2.0.0" }, "devDependencies": { "@types/react": "^18.2.0", "typescript": "^5.3.0", "tailwindcss": "^3.3.0" } } ``` --- ## Permisos de UI | Componente | Permiso Requerido | |------------|-------------------| | AuditLogsPage | audit.logs.read | | ExportButton (audit) | audit.logs.export | | AccessLogsPage | audit.access.read | | SecurityEventsPage | audit.security.read | | SecurityEventDetail (resolve) | audit.security.manage | | ScheduledReportsPage | audit.reports.read | | ScheduledReportForm | audit.reports.manage | --- ## Historial | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-12-05 | Requirements-Analyst | Creacion inicial |