feat(cmp): active script blocking + DSE Interessenabwaegung
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-consent (push) Successful in 30s
CI / test-python-voice (push) Successful in 31s
CI / test-bqas (push) Successful in 31s

ScriptManager: two blocking mechanisms — injection of CONSENT_SCRIPTS
after consent + activation of type="text/plain" data-consent scripts.
Standard CMP blocking pattern ready for third-party analytics/marketing.

DSE: add Interessenabwaegung (balancing test) for Art. 6(1)(f) DSGVO
processing: Hosting and Server-Logfiles sections now document why
legitimate interest outweighs data subject rights.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-12 16:55:24 +02:00
parent 9e3604fe31
commit f5d4e3bd95
2 changed files with 75 additions and 24 deletions
+24 -4
View File
@@ -31,7 +31,17 @@ export default function DatenschutzPage() {
<p>
Diese Website wird auf Servern der Hetzner Online GmbH in Deutschland gehostet.
Es findet kein Drittlandtransfer fuer das Hosting statt.
Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an zuverlaessigem Betrieb).
</p>
<p className="mt-2">
<span className="text-white font-medium">Rechtsgrundlage:</span> Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse).
</p>
<p className="mt-1">
<span className="text-white font-medium">Interessenabwaegung:</span> Unser berechtigtes Interesse liegt im
zuverlaessigen und sicheren Betrieb der Website. Ohne Hosting-Infrastruktur koennen wir unser Angebot
nicht bereitstellen. Die Verarbeitung beschraenkt sich auf technisch notwendige Verbindungsdaten
(IP-Adresse, Zeitstempel). Entgegenstehende Interessen der Betroffenen ueberwiegen nicht, da die
Daten nur kurzzeitig (7 Tage) gespeichert, nicht mit anderen Datenquellen zusammengefuehrt und
ausschliesslich zur Sicherstellung des Betriebs und der IT-Sicherheit verwendet werden.
</p>
</div>
@@ -80,9 +90,19 @@ export default function DatenschutzPage() {
<div>
<h2 className="text-lg font-semibold text-white mb-2">5. Server-Logfiles</h2>
<p>
Der Hosting-Provider erhebt technisch notwendige Logfiles (IP-Adresse, Browsertyp, Zeitstempel).
Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an der Sicherheit).
Logfiles werden nach 7 Tagen automatisch geloescht.
Der Hosting-Provider erhebt technisch notwendige Logfiles (IP-Adresse, Browsertyp, Zeitstempel,
aufgerufene Seite, HTTP-Statuscode).
</p>
<p className="mt-2">
<span className="text-white font-medium">Rechtsgrundlage:</span> Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse).
</p>
<p className="mt-1">
<span className="text-white font-medium">Interessenabwaegung:</span> Die Erhebung von Server-Logfiles ist
fuer die Erkennung und Abwehr von Cyberangriffen, die Fehlerbehebung und die Gewaehrleistung der
Systemstabilitaet unerlasslich. Die Daten werden automatisiert nach 7 Tagen geloescht und nicht
zur Profilbildung oder Identifizierung einzelner Nutzer verwendet. Eine Zusammenfuehrung mit anderen
Datenquellen findet nicht statt. Das Interesse der Betroffenen am Schutz ihrer Daten wird durch die
kurze Speicherdauer und die rein technische Nutzung angemessen gewahrt.
</p>
</div>
@@ -1,31 +1,43 @@
'use client'
import { useEffect, useRef } from 'react'
import { useEffect, useRef, useCallback } from 'react'
/**
* ScriptManager — consent-aware script injection.
* ScriptManager — active consent-aware script blocking + injection.
*
* Listens to the `consent-change` CustomEvent dispatched by ConsentBanner
* and injects/skips third-party scripts based on accepted categories.
* Two mechanisms:
* 1. INJECTION: Scripts in CONSENT_SCRIPTS are only injected AFTER consent.
* 2. BLOCKING: Existing <script data-consent="category" type="text/plain">
* elements in the page are activated after consent by changing type to
* "text/javascript". This is the standard CMP blocking pattern.
*
* Add scripts to CONSENT_SCRIPTS when the site starts using analytics,
* marketing, or functional third-party tools.
* Usage for inline scripts in pages:
* <script type="text/plain" data-consent="analytics">
* // This won't execute until analytics consent is given
* gtag('config', 'G-XXXXXX');
* </script>
*
* Usage for adding new third-party scripts:
* Add to CONSENT_SCRIPTS below. They'll be injected only after consent.
*/
interface ConsentScript {
src: string
async?: boolean
id?: string
}
const CONSENT_SCRIPTS: Record<string, ConsentScript[]> = {
analytics: [
// Example: { src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXX', async: true },
// { src: 'https://www.googletagmanager.com/gtag/js?id=G-XXXXXX', async: true, id: 'gtag' },
// { src: 'https://plausible.io/js/script.js', async: true, id: 'plausible' },
],
marketing: [
// Example: { src: 'https://connect.facebook.net/en_US/fbevents.js', async: true },
// { src: 'https://connect.facebook.net/en_US/fbevents.js', async: true, id: 'fb-pixel' },
// { src: 'https://snap.licdn.com/li.lms-analytics/insight.min.js', async: true, id: 'li-insight' },
],
functional: [
// Example: { src: 'https://widget.example.com/chat.js', async: true },
// { src: 'https://widget.example.com/chat.js', async: true, id: 'chat-widget' },
],
}
@@ -46,39 +58,58 @@ function getStoredConsent(): ConsentState | null {
export default function ScriptManager() {
const injected = useRef(new Set<string>())
function injectScripts(consent: ConsentState) {
const categories: (keyof typeof CONSENT_SCRIPTS)[] = []
if (consent.analytics) categories.push('analytics')
if (consent.functional) categories.push('functional')
// marketing would need its own toggle in ConsentState
const applyConsent = useCallback((consent: ConsentState) => {
const accepted = new Set<string>()
accepted.add('essential') // always allowed
if (consent.functional) accepted.add('functional')
if (consent.analytics) accepted.add('analytics')
for (const cat of categories) {
// 1. INJECT: Add scripts from CONSENT_SCRIPTS for accepted categories
for (const cat of accepted) {
for (const script of CONSENT_SCRIPTS[cat] ?? []) {
if (injected.current.has(script.src)) continue
const el = document.createElement('script')
el.src = script.src
if (script.async) el.async = true
if (script.id) el.id = script.id
el.dataset.consent = cat
document.head.appendChild(el)
injected.current.add(script.src)
}
}
}
// 2. ACTIVATE: Unblock <script type="text/plain" data-consent="...">
const blocked = document.querySelectorAll('script[type="text/plain"][data-consent]')
for (const el of blocked) {
const cat = el.getAttribute('data-consent') || ''
if (accepted.has(cat)) {
const clone = document.createElement('script')
// Copy attributes
for (const attr of el.attributes) {
if (attr.name === 'type') continue // skip type="text/plain"
clone.setAttribute(attr.name, attr.value)
}
clone.type = 'text/javascript'
clone.textContent = el.textContent
el.parentNode?.replaceChild(clone, el)
}
}
}, [])
useEffect(() => {
// On mount: apply saved consent (return visitors)
const saved = getStoredConsent()
if (saved) injectScripts(saved)
if (saved) applyConsent(saved)
// Listen for new consent decisions
function onConsentChange(e: Event) {
const detail = (e as CustomEvent<ConsentState>).detail
if (detail) injectScripts(detail)
if (detail) applyConsent(detail)
}
window.addEventListener('consent-change', onConsentChange)
return () => window.removeEventListener('consent-change', onConsentChange)
}, [])
}, [applyConsent])
return null // Invisible component
return null
}