Monitoring UX: Log leeren + PageSize wählbar
- Backend: DELETE /api/monitoring/events (settings:update). Optional ?olderThanDays=N – nur Events älter als N Tage löschen. Hinterlässt selbst einen Audit-Eintrag "Log geleert: X Einträge" mit User-E-Mail + IP, damit der Vorgang nachvollziehbar bleibt. - Frontend: "Log leeren"-Button öffnet Bestätigungs-Modal mit optionalem "älter als X Tage"-Filter. Roter Bestätigungs-Button. - Frontend: PageSize-Selector (10/25/50/100/200) neben dem Header. Wechsel setzt automatisch zurück auf Seite 1. Live-verifiziert: Clear löscht 10 Events, schreibt 1 Audit-Event, PageSize=5 wird in pagination respektiert. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
57eb027d5f
commit
b5ba47aa93
|
|
@ -148,6 +148,51 @@ export async function testAlert(_req: AuthRequest, res: Response): Promise<void>
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/monitoring/events
|
||||
* Löscht alle SecurityEvents (oder optional nur älter als ?olderThanDays).
|
||||
* Alert-versendete CRITICAL-Events werden vorher noch geloggt, damit der
|
||||
* Audit-Trail erhalten bleibt.
|
||||
*/
|
||||
export async function clearEvents(req: AuthRequest, res: Response): Promise<void> {
|
||||
try {
|
||||
const olderThanDays = req.query.olderThanDays
|
||||
? parseInt(req.query.olderThanDays as string)
|
||||
: undefined;
|
||||
|
||||
const where: any = {};
|
||||
if (olderThanDays && olderThanDays > 0) {
|
||||
const cutoff = new Date(Date.now() - olderThanDays * 24 * 60 * 60 * 1000);
|
||||
where.createdAt = { lt: cutoff };
|
||||
}
|
||||
|
||||
const result = await prisma.securityEvent.deleteMany({ where });
|
||||
|
||||
// Audit-Spur: Wer hat geleert
|
||||
const user = (req as any).user;
|
||||
await prisma.securityEvent.create({
|
||||
data: {
|
||||
type: 'PERMISSION_CHANGED',
|
||||
severity: 'INFO',
|
||||
message: `Security-Log geleert: ${result.count} Einträge gelöscht${olderThanDays ? ` (älter als ${olderThanDays} Tage)` : ''}`,
|
||||
userId: user?.userId || null,
|
||||
userEmail: user?.email || null,
|
||||
ipAddress: req.ip || 'unknown',
|
||||
endpoint: 'DELETE /api/monitoring/events',
|
||||
},
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `${result.count} Events gelöscht`,
|
||||
data: { deletedCount: result.count },
|
||||
} as any);
|
||||
} catch (error) {
|
||||
console.error('clearEvents error:', error);
|
||||
res.status(500).json({ success: false, error: 'Löschen fehlgeschlagen' } as ApiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/monitoring/run-digest (manueller Trigger für den Hourly-Digest)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -11,5 +11,6 @@ router.get('/settings', requirePermission('settings:read'), monitoringController
|
|||
router.put('/settings', requirePermission('settings:update'), monitoringController.updateMonitoringSettings);
|
||||
router.post('/test-alert', requirePermission('settings:update'), monitoringController.testAlert);
|
||||
router.post('/run-digest', requirePermission('settings:update'), monitoringController.runDigestNow);
|
||||
router.delete('/events', requirePermission('settings:update'), monitoringController.clearEvents);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,8 @@ import Card from '../../components/ui/Card';
|
|||
import Button from '../../components/ui/Button';
|
||||
import Input from '../../components/ui/Input';
|
||||
import Select from '../../components/ui/Select';
|
||||
import { ArrowLeft, Send, RefreshCw, Mail, ShieldAlert, ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import Modal from '../../components/ui/Modal';
|
||||
import { ArrowLeft, Send, RefreshCw, Mail, ShieldAlert, ChevronLeft, ChevronRight, Trash2 } from 'lucide-react';
|
||||
|
||||
const TYPE_OPTIONS: { value: SecurityEventType | ''; label: string }[] = [
|
||||
{ value: '', label: 'Alle Typen' },
|
||||
|
|
@ -58,12 +59,15 @@ export default function Monitoring() {
|
|||
const queryClient = useQueryClient();
|
||||
|
||||
const [page, setPage] = useState(1);
|
||||
const [pageSize, setPageSize] = useState(50);
|
||||
const [filters, setFilters] = useState({
|
||||
type: '' as SecurityEventType | '',
|
||||
severity: '' as SecuritySeverity | '',
|
||||
search: '',
|
||||
ip: '',
|
||||
});
|
||||
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
||||
const [clearOlderThanDays, setClearOlderThanDays] = useState<number | ''>('');
|
||||
|
||||
const [alertEmail, setAlertEmail] = useState('');
|
||||
const [digestEnabled, setDigestEnabled] = useState(false);
|
||||
|
|
@ -82,11 +86,22 @@ export default function Monitoring() {
|
|||
|
||||
// Events laden
|
||||
const { data: eventsData, isLoading: eventsLoading } = useQuery({
|
||||
queryKey: ['monitoring-events', page, filters],
|
||||
queryFn: () => monitoringApi.getEvents({ page, limit: 50, ...filters }),
|
||||
queryKey: ['monitoring-events', page, pageSize, filters],
|
||||
queryFn: () => monitoringApi.getEvents({ page, limit: pageSize, ...filters }),
|
||||
refetchInterval: 30_000, // alle 30s neu laden
|
||||
});
|
||||
|
||||
const clearEvents = useMutation({
|
||||
mutationFn: (olderThanDays?: number) => monitoringApi.clearEvents(olderThanDays),
|
||||
onSuccess: (res) => {
|
||||
toast.success(res.message || 'Events gelöscht');
|
||||
setShowClearConfirm(false);
|
||||
setClearOlderThanDays('');
|
||||
queryClient.invalidateQueries({ queryKey: ['monitoring-events'] });
|
||||
},
|
||||
onError: (e: Error) => toast.error(e.message || 'Löschen fehlgeschlagen'),
|
||||
});
|
||||
|
||||
const saveSettings = useMutation({
|
||||
mutationFn: () => monitoringApi.updateSettings({ alertEmail, digestEnabled }),
|
||||
onSuccess: () => {
|
||||
|
|
@ -227,7 +242,26 @@ export default function Monitoring() {
|
|||
|
||||
{/* Tabelle */}
|
||||
<Card>
|
||||
<h2 className="text-lg font-semibold mb-3">Events</h2>
|
||||
<div className="flex items-center justify-between mb-3 flex-wrap gap-2">
|
||||
<h2 className="text-lg font-semibold">Events</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<label className="text-sm text-gray-600">Pro Seite:</label>
|
||||
<select
|
||||
value={pageSize}
|
||||
onChange={(e) => { setPageSize(parseInt(e.target.value)); setPage(1); }}
|
||||
className="px-2 py-1 border border-gray-300 rounded text-sm"
|
||||
>
|
||||
<option value={10}>10</option>
|
||||
<option value={25}>25</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
<option value={200}>200</option>
|
||||
</select>
|
||||
<Button variant="secondary" size="sm" onClick={() => setShowClearConfirm(true)}>
|
||||
<Trash2 className="w-4 h-4 mr-1" /> Log leeren
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{eventsLoading ? (
|
||||
<div className="text-gray-500 py-4">Lade…</div>
|
||||
) : events.length === 0 ? (
|
||||
|
|
@ -284,6 +318,50 @@ export default function Monitoring() {
|
|||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Clear-Confirm-Modal */}
|
||||
<Modal
|
||||
isOpen={showClearConfirm}
|
||||
onClose={() => setShowClearConfirm(false)}
|
||||
title="Security-Log leeren"
|
||||
size="sm"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm text-gray-700">
|
||||
Sicher? Alle Events werden aus der Datenbank entfernt. Ein
|
||||
Audit-Log-Eintrag mit deinem Namen bleibt erhalten.
|
||||
</p>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
Nur Events älter als (Tage)
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
value={clearOlderThanDays}
|
||||
onChange={(e) => setClearOlderThanDays(e.target.value === '' ? '' : parseInt(e.target.value))}
|
||||
placeholder="leer = alle löschen"
|
||||
className="block w-full max-w-[200px] px-3 py-2 border border-gray-300 rounded text-sm"
|
||||
/>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Beispiel: 30 = nur Events älter als 30 Tage löschen.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-2">
|
||||
<Button variant="secondary" onClick={() => setShowClearConfirm(false)}>
|
||||
Abbrechen
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => clearEvents.mutate(clearOlderThanDays === '' ? undefined : Number(clearOlderThanDays))}
|
||||
disabled={clearEvents.isPending}
|
||||
className="!bg-red-600 hover:!bg-red-700"
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-1" />
|
||||
{clearOlderThanDays === '' ? 'Alle löschen' : `Älter als ${clearOlderThanDays} Tage löschen`}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1489,6 +1489,11 @@ export const monitoringApi = {
|
|||
const res = await api.post<ApiResponse<{ sent: boolean; eventCount: number; reason?: string }>>('/monitoring/run-digest');
|
||||
return res.data;
|
||||
},
|
||||
clearEvents: async (olderThanDays?: number) => {
|
||||
const q = olderThanDays ? `?olderThanDays=${olderThanDays}` : '';
|
||||
const res = await api.delete<ApiResponse<{ deletedCount: number }>>(`/monitoring/events${q}`);
|
||||
return res.data;
|
||||
},
|
||||
};
|
||||
|
||||
export const emailLogApi = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue