feat: Whistleblower backend + Scanner banner-check (last 2 gaps)

Whistleblower (HinSchG):
- Migration 118: 3 tables (reports, messages, measures) with
  HinSchG deadlines (7d acknowledgment, 3mo feedback)
- whistleblower_routes.py: 14 endpoints (CRUD, acknowledge, close,
  messages, measures, public submit, anonymous status check)
- Frontend api-operations.ts rewired from Go SDK to compliance proxy
- Access key format XXXX-XXXX-XXXX for anonymous reporters

Scanner banner-check (TTDSG § 25):
- CMP Dashboard: green "Kein Cookie-Banner erforderlich" when no
  trackers detected + no banner configured
- Red warning "Cookie-Banner fehlt!" when trackers found but no banner
- Mandatory note: Impressum (DDG § 5) + DSE (DSGVO Art. 13) still required

[migration-approved]

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-04 00:22:18 +02:00
parent eb4ea8bc42
commit c89a68e59e
5 changed files with 424 additions and 16 deletions
@@ -21,7 +21,8 @@ import {
// CONFIGURATION
// =============================================================================
const WB_API_BASE = process.env.NEXT_PUBLIC_SDK_API_URL || 'http://localhost:8093'
// Use compliance backend proxy (Python) instead of Go SDK
const WB_API_BASE = '/api/sdk/v1/compliance'
const API_TIMEOUT = 30000
// =============================================================================
@@ -121,27 +122,27 @@ export async function fetchReports(filters?: ReportFilters): Promise<ReportListR
}
const queryString = params.toString()
const url = `${WB_API_BASE}/api/v1/admin/whistleblower/reports${queryString ? `?${queryString}` : ''}`
const url = `${WB_API_BASE}/whistleblower/reports${queryString ? `?${queryString}` : ''}`
return fetchWithTimeout<ReportListResponse>(url)
}
export async function fetchReport(id: string): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`
`${WB_API_BASE}/whistleblower/reports/${id}`
)
}
export async function updateReport(id: string, update: ReportUpdateRequest): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`,
`${WB_API_BASE}/whistleblower/reports/${id}`,
{ method: 'PUT', body: JSON.stringify(update) }
)
}
export async function deleteReport(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}`,
`${WB_API_BASE}/whistleblower/reports/${id}`,
{ method: 'DELETE' }
)
}
@@ -154,7 +155,7 @@ export async function submitPublicReport(
data: PublicReportSubmission
): Promise<{ report: WhistleblowerReport; accessKey: string }> {
const response = await fetch(
`${WB_API_BASE}/api/v1/public/whistleblower/submit`,
`${WB_API_BASE}/whistleblower/submit`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
@@ -173,7 +174,7 @@ export async function fetchReportByAccessKey(
accessKey: string
): Promise<WhistleblowerReport> {
const response = await fetch(
`${WB_API_BASE}/api/v1/public/whistleblower/report/${accessKey}`,
`${WB_API_BASE}/whistleblower/check/${accessKey}`,
{ method: 'GET', headers: { 'Content-Type': 'application/json' } }
)
@@ -190,14 +191,14 @@ export async function fetchReportByAccessKey(
export async function acknowledgeReport(id: string): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/acknowledge`,
`${WB_API_BASE}/whistleblower/reports/${id}/acknowledge`,
{ method: 'POST' }
)
}
export async function startInvestigation(id: string): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/investigate`,
`${WB_API_BASE}/whistleblower/reports/${id}/investigate`,
{ method: 'POST' }
)
}
@@ -207,7 +208,7 @@ export async function addMeasure(
measure: Omit<WhistleblowerMeasure, 'id' | 'reportId' | 'completedAt'>
): Promise<WhistleblowerMeasure> {
return fetchWithTimeout<WhistleblowerMeasure>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/measures`,
`${WB_API_BASE}/whistleblower/reports/${id}/measures`,
{ method: 'POST', body: JSON.stringify(measure) }
)
}
@@ -217,7 +218,7 @@ export async function closeReport(
resolution: { reason: string; notes: string }
): Promise<WhistleblowerReport> {
return fetchWithTimeout<WhistleblowerReport>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${id}/close`,
`${WB_API_BASE}/whistleblower/reports/${id}/close`,
{ method: 'POST', body: JSON.stringify(resolution) }
)
}
@@ -232,14 +233,14 @@ export async function sendMessage(
role: 'reporter' | 'ombudsperson'
): Promise<AnonymousMessage> {
return fetchWithTimeout<AnonymousMessage>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`,
`${WB_API_BASE}/whistleblower/reports/${reportId}/messages`,
{ method: 'POST', body: JSON.stringify({ senderRole: role, message }) }
)
}
export async function fetchMessages(reportId: string): Promise<AnonymousMessage[]> {
return fetchWithTimeout<AnonymousMessage[]>(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/messages`
`${WB_API_BASE}/whistleblower/reports/${reportId}/messages`
)
}
@@ -269,7 +270,7 @@ export async function uploadAttachment(
}
const response = await fetch(
`${WB_API_BASE}/api/v1/admin/whistleblower/reports/${reportId}/attachments`,
`${WB_API_BASE}/whistleblower/reports/${reportId}/attachments`,
{
method: 'POST',
headers,
@@ -290,7 +291,7 @@ export async function uploadAttachment(
export async function deleteAttachment(id: string): Promise<void> {
await fetchWithTimeout<void>(
`${WB_API_BASE}/api/v1/admin/whistleblower/attachments/${id}`,
`${WB_API_BASE}/whistleblower/attachments/${id}`,
{ method: 'DELETE' }
)
}
@@ -301,6 +302,6 @@ export async function deleteAttachment(id: string): Promise<void> {
export async function fetchWhistleblowerStatistics(): Promise<WhistleblowerStatistics> {
return fetchWithTimeout<WhistleblowerStatistics>(
`${WB_API_BASE}/api/v1/admin/whistleblower/statistics`
`${WB_API_BASE}/whistleblower/reports/stats`
)
}