Files
breakpilot-compliance/admin-compliance/app/api/sdk/v1/agent/consent-test/route.ts
T
Benjamin Admin 16c40ddae4 feat: consent-test email with phase-structured findings
Email shows 3 phases (Before/After Reject/After Accept) with:
- Violation cards per phase (CRITICAL/HIGH badges)
- Undocumented services in Phase C
- Summary table (critical/high/undocumented counts)
- Dark Pattern warning if tracking persists after rejection

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-29 13:14:01 +02:00

143 lines
7.1 KiB
TypeScript

/**
* Consent Test API Proxy
* POST /api/sdk/v1/agent/consent-test → consent-tester:8094/scan → email via backend
*/
import { NextRequest, NextResponse } from 'next/server'
const CONSENT_TESTER_URL = process.env.CONSENT_TESTER_URL || 'http://bp-compliance-consent-tester:8094'
const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:8002'
interface Violation { service: string; severity: string; text: string; legal_ref: string }
function buildEmailHtml(data: any): string {
const url = data.url || ''
const banner = data.banner_detected ? data.banner_provider : 'Nicht erkannt'
const phases = data.phases || {}
const summary = data.summary || {}
const sev = (s: string) => s === 'CRITICAL'
? '<span style="background:#991b1b;color:white;padding:2px 6px;border-radius:3px;font-size:11px;">KRITISCH</span>'
: '<span style="background:#ea580c;color:white;padding:2px 6px;border-radius:3px;font-size:11px;">HOCH</span>'
const violationRows = (violations: Violation[]) => violations.length === 0
? '<tr><td colspan="3" style="padding:6px;color:#16a34a;">✓ Keine Verstoesse</td></tr>'
: violations.map(v =>
`<tr><td style="padding:6px;">${sev(v.severity)}</td><td style="padding:6px;font-weight:600;">${v.service}</td><td style="padding:6px;">${v.text}<br><span style="color:#6b7280;font-size:11px;">${v.legal_ref}</span></td></tr>`
).join('')
const undocRows = (items: string[]) => items.length === 0
? ''
: items.map(s => `<tr><td style="padding:6px;">⚠</td><td style="padding:6px;font-weight:600;">${s}</td><td style="padding:6px;">Nicht in Cookie-Policy dokumentiert</td></tr>`).join('')
return `
<div style="font-family:-apple-system,sans-serif;max-width:700px;margin:0 auto;">
<div style="background:linear-gradient(135deg,#1e1b4b,#312e81);color:white;padding:20px 24px;border-radius:12px 12px 0 0;">
<h2 style="margin:0;font-size:18px;">Cookie-Consent-Test</h2>
<p style="margin:4px 0 0;opacity:0.8;font-size:13px;">${url}</p>
</div>
<div style="padding:20px 24px;border:1px solid #e2e8f0;border-top:none;">
<table style="width:100%;border-collapse:collapse;margin-bottom:20px;">
<tr><td style="padding:6px 0;color:#64748b;width:160px;">Cookie-Banner</td><td style="padding:6px 0;font-weight:600;">${data.banner_detected ? '✓ ' + banner : '✗ Nicht erkannt'}</td></tr>
<tr><td style="padding:6px 0;color:#64748b;">Kritische Verstoesse</td><td style="padding:6px 0;"><strong style="color:${summary.critical > 0 ? '#dc2626' : '#16a34a'}">${summary.critical || 0}</strong></td></tr>
<tr><td style="padding:6px 0;color:#64748b;">Hohe Verstoesse</td><td style="padding:6px 0;"><strong style="color:${summary.high > 0 ? '#ea580c' : '#16a34a'}">${summary.high || 0}</strong></td></tr>
<tr><td style="padding:6px 0;color:#64748b;">Undokumentiert</td><td style="padding:6px 0;">${summary.undocumented || 0}</td></tr>
</table>
<h3 style="color:#1e293b;font-size:14px;margin:20px 0 8px;border-bottom:2px solid #e2e8f0;padding-bottom:6px;">
🔍 Phase A: Vor Einwilligung
</h3>
<p style="color:#64748b;font-size:12px;margin:0 0 8px;">Was laedt OHNE dass der Nutzer etwas geklickt hat?</p>
<table style="width:100%;border-collapse:collapse;">${violationRows(phases.before_consent?.violations || [])}</table>
${data.banner_detected ? `
<h3 style="color:#1e293b;font-size:14px;margin:20px 0 8px;border-bottom:2px solid #e2e8f0;padding-bottom:6px;">
🚫 Phase B: Nach Ablehnung
</h3>
<p style="color:#64748b;font-size:12px;margin:0 0 8px;">Was laedt NACHDEM der Nutzer "Nur notwendige" geklickt hat?</p>
<table style="width:100%;border-collapse:collapse;">${violationRows(phases.after_reject?.violations || [])}</table>
<h3 style="color:#1e293b;font-size:14px;margin:20px 0 8px;border-bottom:2px solid #e2e8f0;padding-bottom:6px;">
✅ Phase C: Nach Zustimmung
</h3>
<p style="color:#64748b;font-size:12px;margin:0 0 8px;">Was laedt NACHDEM der Nutzer "Alle akzeptieren" geklickt hat?</p>
<table style="width:100%;border-collapse:collapse;">${undocRows(phases.after_accept?.undocumented || [])}</table>
${(phases.after_accept?.undocumented?.length || 0) === 0 ? '<p style="color:#16a34a;font-size:13px;">✓ Alle Dienste dokumentiert</p>' : ''}
` : `
<div style="background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:12px;margin:12px 0;">
<strong style="color:#dc2626;">Kein Cookie-Banner erkannt.</strong>
Alle Tracking-Dienste laden ohne Einwilligung — Verstoss gegen §25 TDDDG.
</div>
`}
${(summary.critical || 0) > 0 ? `
<div style="background:#fef2f2;border-left:4px solid #dc2626;padding:12px 16px;margin-top:20px;">
<strong style="color:#991b1b;">⚠ KRITISCH:</strong> Tracking-Dienste laden trotz Ablehnung.
Dies ist ein schwerer Verstoss gegen §25 TDDDG und kann als Dark Pattern gewertet werden.
Sofortige Korrektur der Cookie-Banner-Konfiguration empfohlen.
</div>
` : ''}
</div>
<div style="background:#f8fafc;padding:12px 24px;border:1px solid #e2e8f0;border-top:none;border-radius:0 0 12px 12px;">
<p style="color:#94a3b8;font-size:11px;margin:0;">
Automatisch erstellt vom BreakPilot Compliance Agent (Playwright + Chromium)
</p>
</div>
</div>
`
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const url = body.url
// Step 1: Run consent test
const response = await fetch(`${CONSENT_TESTER_URL}/scan`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(180000),
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Consent-Tester: ${response.status}`, detail: errorText },
{ status: response.status }
)
}
const data = await response.json()
// Step 2: Send email with phase-structured findings
try {
const total = (data.summary?.total_violations || 0)
const severity = (data.summary?.critical || 0) > 0 ? 'KRITISCH' : total > 0 ? 'FINDINGS' : 'OK'
await fetch(`${BACKEND_URL}/api/compliance/agent/notify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient: body.recipient || 'dsb@breakpilot.local',
subject: `[COOKIE-TEST] [${severity}] ${url}${total} Verstoesse`,
body_html: buildEmailHtml({ ...data, url }),
role: total > 0 ? 'Datenschutzbeauftragter' : 'Kein Handlungsbedarf',
}),
signal: AbortSignal.timeout(10000),
})
} catch (emailErr) {
console.warn('Email send failed (non-blocking):', emailErr)
}
return NextResponse.json(data)
} catch (error) {
console.error('Consent test proxy error:', error)
return NextResponse.json(
{ error: 'Cookie-Test fehlgeschlagen oder Timeout' },
{ status: 503 }
)
}
}