From 4f9718dce428c8d1383a5458c5745eb3fa4ecf76 Mon Sep 17 00:00:00 2001 From: admin-valentin Date: Sun, 2 Aug 2026 12:50:20 +0000 Subject: [PATCH] feat(CC-077): add Security Events page (filtered audit log with severity classification) --- src/app/dashboard/privacy/security/page.tsx | 120 ++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 src/app/dashboard/privacy/security/page.tsx diff --git a/src/app/dashboard/privacy/security/page.tsx b/src/app/dashboard/privacy/security/page.tsx new file mode 100644 index 0000000..6681380 --- /dev/null +++ b/src/app/dashboard/privacy/security/page.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { useState } from 'react'; +import { useQuery } from '@tanstack/react-query'; +import Link from 'next/link'; +import { apiFetch } from '../../../../lib/api'; +import { useSession } from '../../../../components/session-provider'; + +interface AuditEntry { + id: string; action: string; entityType: string | null; entityId: string | null; + userId: string | null; userEmail: string | null; ipAddress: string | null; + details: Record | null; severity: string | null; + createdAt: string; +} + +const SEVERITY_META: Record = { + info: { label: 'INFO', cls: 'bg-muted text-ink-faint' }, + warning: { label: 'WARNING', cls: 'bg-warn/10 text-warn' }, + critical: { label: 'CRITICAL', cls: 'bg-signal-danger/10 text-signal-danger' }, +}; + +const SECURITY_ACTIONS = ['login', 'logout', 'login_failed', 'password_change', 'api_key', 'permission', 'role', 'delete', 'export', 'access', 'auth', 'session', 'token']; + +function isSecurityEvent(entry: AuditEntry) { + const action = entry.action.toLowerCase(); + return SECURITY_ACTIONS.some((s) => action.includes(s)) || entry.severity === 'critical'; +} + +export default function SecurityEventsPage() { + const { activeTenant } = useSession(); + const tenantId = activeTenant?.tenantId ?? ''; + const [filter, setFilter] = useState<'all' | 'security' | 'critical'>('all'); + + const { data: entries = [], isLoading } = useQuery({ + queryKey: ['audit-security', tenantId], + queryFn: () => apiFetch('/v1/audit-log?limit=200', { tenantId }), + enabled: Boolean(tenantId), staleTime: 30_000, refetchInterval: 60_000, + }); + + const filtered = entries.filter((e) => { + if (filter === 'security') return isSecurityEvent(e); + if (filter === 'critical') return e.severity === 'critical' || e.severity === 'warning'; + return true; + }); + + const critical = entries.filter((e) => e.severity === 'critical'); + const security = entries.filter(isSecurityEvent); + + return ( +
+
+
+

Evenimente Securitate

+

+ {entries.length} intrări audit · {security.length} securitate + {critical.length > 0 && {critical.length} critice} +

+
+ + Audit complet → + +
+ + {critical.length > 0 && ( +
+

⚠ {critical.length} evenimente critice detectate

+ {critical.slice(0, 3).map((e) => ( +

+ {e.action} · {e.userEmail ?? e.userId ?? 'sistem'} · {new Date(e.createdAt).toLocaleString('ro-RO')} +

+ ))} +
+ )} + + {/* Filter */} +
+ {([['all', `Toate (${entries.length})`], ['security', `Securitate (${security.length})`], ['critical', `Critice (${critical.length})`]] as const).map(([key, label]) => ( + + ))} +
+ + {isLoading ? ( +
Se încarcă…
+ ) : filtered.length === 0 ? ( +
+

🔒

+

Niciun eveniment în această categorie.

+
+ ) : ( +
+ {filtered.slice(0, 50).map((e) => { + const sm = SEVERITY_META[e.severity ?? 'info'] ?? SEVERITY_META.info; + return ( +
+ {sm.label} +
+

{e.action}

+
+ {e.userEmail && {e.userEmail}} + {e.entityType && {e.entityType}{e.entityId ? `:${e.entityId.slice(0, 8)}…` : ''}} + {e.ipAddress && IP: {e.ipAddress}} +
+
+ + {new Date(e.createdAt).toLocaleString('ro-RO', { hour: '2-digit', minute: '2-digit', day: '2-digit', month: 'short' })} + +
+ ); + })} + {filtered.length > 50 && ( +

și alte {filtered.length - 50} intrări — vezi Audit complet

+ )} +
+ )} +
+ ); +}