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)
|
* 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.put('/settings', requirePermission('settings:update'), monitoringController.updateMonitoringSettings);
|
||||||
router.post('/test-alert', requirePermission('settings:update'), monitoringController.testAlert);
|
router.post('/test-alert', requirePermission('settings:update'), monitoringController.testAlert);
|
||||||
router.post('/run-digest', requirePermission('settings:update'), monitoringController.runDigestNow);
|
router.post('/run-digest', requirePermission('settings:update'), monitoringController.runDigestNow);
|
||||||
|
router.delete('/events', requirePermission('settings:update'), monitoringController.clearEvents);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@ import Card from '../../components/ui/Card';
|
||||||
import Button from '../../components/ui/Button';
|
import Button from '../../components/ui/Button';
|
||||||
import Input from '../../components/ui/Input';
|
import Input from '../../components/ui/Input';
|
||||||
import Select from '../../components/ui/Select';
|
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 }[] = [
|
const TYPE_OPTIONS: { value: SecurityEventType | ''; label: string }[] = [
|
||||||
{ value: '', label: 'Alle Typen' },
|
{ value: '', label: 'Alle Typen' },
|
||||||
|
|
@ -58,12 +59,15 @@ export default function Monitoring() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
const [pageSize, setPageSize] = useState(50);
|
||||||
const [filters, setFilters] = useState({
|
const [filters, setFilters] = useState({
|
||||||
type: '' as SecurityEventType | '',
|
type: '' as SecurityEventType | '',
|
||||||
severity: '' as SecuritySeverity | '',
|
severity: '' as SecuritySeverity | '',
|
||||||
search: '',
|
search: '',
|
||||||
ip: '',
|
ip: '',
|
||||||
});
|
});
|
||||||
|
const [showClearConfirm, setShowClearConfirm] = useState(false);
|
||||||
|
const [clearOlderThanDays, setClearOlderThanDays] = useState<number | ''>('');
|
||||||
|
|
||||||
const [alertEmail, setAlertEmail] = useState('');
|
const [alertEmail, setAlertEmail] = useState('');
|
||||||
const [digestEnabled, setDigestEnabled] = useState(false);
|
const [digestEnabled, setDigestEnabled] = useState(false);
|
||||||
|
|
@ -82,11 +86,22 @@ export default function Monitoring() {
|
||||||
|
|
||||||
// Events laden
|
// Events laden
|
||||||
const { data: eventsData, isLoading: eventsLoading } = useQuery({
|
const { data: eventsData, isLoading: eventsLoading } = useQuery({
|
||||||
queryKey: ['monitoring-events', page, filters],
|
queryKey: ['monitoring-events', page, pageSize, filters],
|
||||||
queryFn: () => monitoringApi.getEvents({ page, limit: 50, ...filters }),
|
queryFn: () => monitoringApi.getEvents({ page, limit: pageSize, ...filters }),
|
||||||
refetchInterval: 30_000, // alle 30s neu laden
|
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({
|
const saveSettings = useMutation({
|
||||||
mutationFn: () => monitoringApi.updateSettings({ alertEmail, digestEnabled }),
|
mutationFn: () => monitoringApi.updateSettings({ alertEmail, digestEnabled }),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
|
|
@ -227,7 +242,26 @@ export default function Monitoring() {
|
||||||
|
|
||||||
{/* Tabelle */}
|
{/* Tabelle */}
|
||||||
<Card>
|
<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 ? (
|
{eventsLoading ? (
|
||||||
<div className="text-gray-500 py-4">Lade…</div>
|
<div className="text-gray-500 py-4">Lade…</div>
|
||||||
) : events.length === 0 ? (
|
) : events.length === 0 ? (
|
||||||
|
|
@ -284,6 +318,50 @@ export default function Monitoring() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1489,6 +1489,11 @@ export const monitoringApi = {
|
||||||
const res = await api.post<ApiResponse<{ sent: boolean; eventCount: number; reason?: string }>>('/monitoring/run-digest');
|
const res = await api.post<ApiResponse<{ sent: boolean; eventCount: number; reason?: string }>>('/monitoring/run-digest');
|
||||||
return res.data;
|
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 = {
|
export const emailLogApi = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue