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
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user