Files
breakpilot-compliance/admin-compliance/lib/sdk/incidents/api-incidents.ts
Sharang Parnerkar 58e95d5e8e refactor(admin): split 9 more oversized lib/ files into focused modules
Files split by agents before rate limit:
  - dsr/api.ts (669 → barrel + helpers)
  - einwilligungen/context.tsx (669 → barrel + hooks/reducer)
  - export.ts (753 → barrel + domain exporters)
  - incidents/api.ts (845 → barrel + api-helpers)
  - tom-generator/context.tsx (720 → barrel + hooks/reducer)
  - vendor-compliance/context.tsx (1010 → 234 provider + hooks/reducer)
  - api-docs/endpoints.ts — partially split (3 domain files created)
  - academy/api.ts — partially split (helpers extracted)
  - whistleblower/api.ts — partially split (helpers extracted)

next build passes. api-client.ts (885) deferred to next session.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 19:12:09 +02:00

373 lines
12 KiB
TypeScript

/**
* Incident CRUD, Risk Assessment, Notifications, Measures, Timeline, Statistics
*/
import {
Incident,
IncidentListResponse,
IncidentFilters,
IncidentCreateRequest,
IncidentUpdateRequest,
IncidentStatistics,
IncidentMeasure,
TimelineEntry,
RiskAssessmentRequest,
AuthorityNotification,
DataSubjectNotification,
IncidentSeverity,
IncidentStatus,
IncidentCategory,
} from './types'
import { INCIDENTS_API_BASE, fetchWithTimeout, getAuthHeaders } from './api-helpers'
// =============================================================================
// INCIDENT LIST & CRUD
// =============================================================================
/**
* Alle Vorfaelle abrufen mit optionalen Filtern
*/
export async function fetchIncidents(filters?: IncidentFilters): Promise<IncidentListResponse> {
const params = new URLSearchParams()
if (filters) {
if (filters.status) {
const statuses = Array.isArray(filters.status) ? filters.status : [filters.status]
statuses.forEach(s => params.append('status', s))
}
if (filters.severity) {
const severities = Array.isArray(filters.severity) ? filters.severity : [filters.severity]
severities.forEach(s => params.append('severity', s))
}
if (filters.category) {
const categories = Array.isArray(filters.category) ? filters.category : [filters.category]
categories.forEach(c => params.append('category', c))
}
if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
if (filters.overdue !== undefined) params.set('overdue', String(filters.overdue))
if (filters.search) params.set('search', filters.search)
if (filters.dateFrom) params.set('dateFrom', filters.dateFrom)
if (filters.dateTo) params.set('dateTo', filters.dateTo)
}
const queryString = params.toString()
const url = `${INCIDENTS_API_BASE}/api/v1/incidents${queryString ? `?${queryString}` : ''}`
return fetchWithTimeout<IncidentListResponse>(url)
}
/**
* Einzelnen Vorfall per ID abrufen
*/
export async function fetchIncident(id: string): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`)
}
/**
* Neuen Vorfall erstellen
*/
export async function createIncident(request: IncidentCreateRequest): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents`, {
method: 'POST',
body: JSON.stringify(request)
})
}
/**
* Vorfall aktualisieren
*/
export async function updateIncident(id: string, update: IncidentUpdateRequest): Promise<Incident> {
return fetchWithTimeout<Incident>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
method: 'PUT',
body: JSON.stringify(update)
})
}
/**
* Vorfall loeschen (Soft Delete)
*/
export async function deleteIncident(id: string): Promise<void> {
await fetchWithTimeout<void>(`${INCIDENTS_API_BASE}/api/v1/incidents/${id}`, {
method: 'DELETE'
})
}
// =============================================================================
// RISK ASSESSMENT
// =============================================================================
/**
* Risikobewertung fuer einen Vorfall durchfuehren (Art. 33 DSGVO)
*/
export async function submitRiskAssessment(
incidentId: string,
assessment: RiskAssessmentRequest
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/risk-assessment`,
{
method: 'POST',
body: JSON.stringify(assessment)
}
)
}
// =============================================================================
// AUTHORITY NOTIFICATION (Art. 33 DSGVO)
// =============================================================================
/**
* Meldeformular fuer die Aufsichtsbehoerde generieren
*/
export async function generateAuthorityForm(incidentId: string): Promise<Blob> {
const response = await fetch(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-form/pdf`,
{
headers: getAuthHeaders()
}
)
if (!response.ok) {
throw new Error(`PDF-Generierung fehlgeschlagen: ${response.statusText}`)
}
return response.blob()
}
/**
* Meldung an die Aufsichtsbehoerde einreichen (Art. 33 DSGVO)
*/
export async function submitAuthorityNotification(
incidentId: string,
data: Partial<AuthorityNotification>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/authority-notification`,
{
method: 'POST',
body: JSON.stringify(data)
}
)
}
// =============================================================================
// DATA SUBJECT NOTIFICATION (Art. 34 DSGVO)
// =============================================================================
/**
* Betroffene Personen benachrichtigen (Art. 34 DSGVO)
*/
export async function sendDataSubjectNotification(
incidentId: string,
data: Partial<DataSubjectNotification>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/data-subject-notification`,
{
method: 'POST',
body: JSON.stringify(data)
}
)
}
// =============================================================================
// MEASURES (Massnahmen)
// =============================================================================
/**
* Massnahme hinzufuegen (Sofort-, Korrektur- oder Praeventionsmassnahme)
*/
export async function addMeasure(
incidentId: string,
measure: Omit<IncidentMeasure, 'id' | 'incidentId'>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/measures`,
{
method: 'POST',
body: JSON.stringify(measure)
}
)
}
/**
* Massnahme aktualisieren
*/
export async function updateMeasure(
measureId: string,
update: Partial<IncidentMeasure>
): Promise<IncidentMeasure> {
return fetchWithTimeout<IncidentMeasure>(
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}`,
{
method: 'PUT',
body: JSON.stringify(update)
}
)
}
/**
* Massnahme als abgeschlossen markieren
*/
export async function completeMeasure(measureId: string): Promise<IncidentMeasure> {
return fetchWithTimeout<IncidentMeasure>(
`${INCIDENTS_API_BASE}/api/v1/measures/${measureId}/complete`,
{
method: 'POST'
}
)
}
// =============================================================================
// TIMELINE
// =============================================================================
/**
* Zeitleisteneintrag hinzufuegen
*/
export async function addTimelineEntry(
incidentId: string,
entry: Omit<TimelineEntry, 'id' | 'incidentId'>
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/timeline`,
{
method: 'POST',
body: JSON.stringify(entry)
}
)
}
// =============================================================================
// CLOSE INCIDENT
// =============================================================================
/**
* Vorfall abschliessen mit Lessons Learned
*/
export async function closeIncident(
incidentId: string,
lessonsLearned: string
): Promise<Incident> {
return fetchWithTimeout<Incident>(
`${INCIDENTS_API_BASE}/api/v1/incidents/${incidentId}/close`,
{
method: 'POST',
body: JSON.stringify({ lessonsLearned })
}
)
}
// =============================================================================
// STATISTICS
// =============================================================================
/**
* Vorfall-Statistiken abrufen
*/
export async function fetchIncidentStatistics(): Promise<IncidentStatistics> {
return fetchWithTimeout<IncidentStatistics>(
`${INCIDENTS_API_BASE}/api/v1/incidents/statistics`
)
}
// =============================================================================
// SDK PROXY FUNCTION (mit Fallback auf Mock-Daten)
// =============================================================================
/**
* Fetch Incident-Liste via SDK-Proxy mit Fallback auf Mock-Daten
*/
export async function fetchSDKIncidentList(): Promise<{ incidents: Incident[]; statistics: IncidentStatistics }> {
try {
const res = await fetch('/api/sdk/v1/incidents', {
headers: getAuthHeaders()
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const data = await res.json()
const incidents: Incident[] = data.incidents || []
const statistics = computeStatistics(incidents)
return { incidents, statistics }
} catch (error) {
console.warn('SDK-Backend nicht erreichbar, verwende Mock-Daten:', error)
// Import mock data lazily to keep this file lean
const { createMockIncidents, createMockStatistics } = await import('./api-mock')
const incidents = createMockIncidents()
const statistics = createMockStatistics()
return { incidents, statistics }
}
}
/**
* Statistiken lokal aus Incident-Liste berechnen
*/
function computeStatistics(incidents: Incident[]): IncidentStatistics {
const countBy = <K extends string>(items: { [key: string]: unknown }[], field: string): Record<K, number> => {
const result: Record<string, number> = {}
items.forEach(item => {
const key = String(item[field])
result[key] = (result[key] || 0) + 1
})
return result as Record<K, number>
}
const statusCounts = countBy<IncidentStatus>(incidents as unknown as { [key: string]: unknown }[], 'status')
const severityCounts = countBy<IncidentSeverity>(incidents as unknown as { [key: string]: unknown }[], 'severity')
const categoryCounts = countBy<IncidentCategory>(incidents as unknown as { [key: string]: unknown }[], 'category')
const openIncidents = incidents.filter(i => i.status !== 'closed').length
const notificationsPending = incidents.filter(i =>
i.authorityNotification !== null &&
i.authorityNotification.status === 'pending' &&
i.status !== 'closed'
).length
let totalResponseHours = 0
let respondedCount = 0
incidents.forEach(i => {
if (i.riskAssessment && i.riskAssessment.assessedAt) {
const detected = new Date(i.detectedAt).getTime()
const assessed = new Date(i.riskAssessment.assessedAt).getTime()
totalResponseHours += (assessed - detected) / (1000 * 60 * 60)
respondedCount++
}
})
return {
totalIncidents: incidents.length,
openIncidents,
notificationsPending,
averageResponseTimeHours: respondedCount > 0 ? Math.round(totalResponseHours / respondedCount * 10) / 10 : 0,
bySeverity: {
low: severityCounts['low'] || 0,
medium: severityCounts['medium'] || 0,
high: severityCounts['high'] || 0,
critical: severityCounts['critical'] || 0
},
byCategory: {
data_breach: categoryCounts['data_breach'] || 0,
unauthorized_access: categoryCounts['unauthorized_access'] || 0,
data_loss: categoryCounts['data_loss'] || 0,
system_compromise: categoryCounts['system_compromise'] || 0,
phishing: categoryCounts['phishing'] || 0,
ransomware: categoryCounts['ransomware'] || 0,
insider_threat: categoryCounts['insider_threat'] || 0,
physical_breach: categoryCounts['physical_breach'] || 0,
other: categoryCounts['other'] || 0
},
byStatus: {
detected: statusCounts['detected'] || 0,
assessment: statusCounts['assessment'] || 0,
containment: statusCounts['containment'] || 0,
notification_required: statusCounts['notification_required'] || 0,
notification_sent: statusCounts['notification_sent'] || 0,
remediation: statusCounts['remediation'] || 0,
closed: statusCounts['closed'] || 0
}
}
}