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> <p>
Diese Website wird auf Servern der Hetzner Online GmbH in Deutschland gehostet. Diese Website wird auf Servern der Hetzner Online GmbH in Deutschland gehostet.
Es findet kein Drittlandtransfer fuer das Hosting statt. 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> </p>
</div> </div>
@@ -80,9 +90,19 @@ export default function DatenschutzPage() {
<div> <div>
<h2 className="text-lg font-semibold text-white mb-2">5. Server-Logfiles</h2> <h2 className="text-lg font-semibold text-white mb-2">5. Server-Logfiles</h2>
<p> <p>
Der Hosting-Provider erhebt technisch notwendige Logfiles (IP-Adresse, Browsertyp, Zeitstempel). Der Hosting-Provider erhebt technisch notwendige Logfiles (IP-Adresse, Browsertyp, Zeitstempel,
Rechtsgrundlage: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an der Sicherheit). aufgerufene Seite, HTTP-Statuscode).
Logfiles werden nach 7 Tagen automatisch geloescht. </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> </p>
</div> </div>
@@ -1,31 +1,43 @@
'use client' '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 * Two mechanisms:
* and injects/skips third-party scripts based on accepted categories. * 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, * Usage for inline scripts in pages:
* marketing, or functional third-party tools. * <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 { interface ConsentScript {
src: string src: string
async?: boolean async?: boolean
id?: string
} }
const CONSENT_SCRIPTS: Record<string, ConsentScript[]> = { const CONSENT_SCRIPTS: Record<string, ConsentScript[]> = {
analytics: [ 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: [ 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: [ 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() { export default function ScriptManager() {
const injected = useRef(new Set<string>()) const injected = useRef(new Set<string>())
function injectScripts(consent: ConsentState) { const applyConsent = useCallback((consent: ConsentState) => {
const categories: (keyof typeof CONSENT_SCRIPTS)[] = [] const accepted = new Set<string>()
if (consent.analytics) categories.push('analytics') accepted.add('essential') // always allowed
if (consent.functional) categories.push('functional') if (consent.functional) accepted.add('functional')
// marketing would need its own toggle in ConsentState 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] ?? []) { for (const script of CONSENT_SCRIPTS[cat] ?? []) {
if (injected.current.has(script.src)) continue if (injected.current.has(script.src)) continue
const el = document.createElement('script') const el = document.createElement('script')
el.src = script.src el.src = script.src
if (script.async) el.async = true if (script.async) el.async = true
if (script.id) el.id = script.id
el.dataset.consent = cat el.dataset.consent = cat
document.head.appendChild(el) document.head.appendChild(el)
injected.current.add(script.src) 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(() => { useEffect(() => {
// On mount: apply saved consent (return visitors) // On mount: apply saved consent (return visitors)
const saved = getStoredConsent() const saved = getStoredConsent()
if (saved) injectScripts(saved) if (saved) applyConsent(saved)
// Listen for new consent decisions // Listen for new consent decisions
function onConsentChange(e: Event) { function onConsentChange(e: Event) {
const detail = (e as CustomEvent<ConsentState>).detail const detail = (e as CustomEvent<ConsentState>).detail
if (detail) injectScripts(detail) if (detail) applyConsent(detail)
} }
window.addEventListener('consent-change', onConsentChange) window.addEventListener('consent-change', onConsentChange)
return () => window.removeEventListener('consent-change', onConsentChange) return () => window.removeEventListener('consent-change', onConsentChange)
}, []) }, [applyConsent])
return null // Invisible component return null
} }