Docker Compose with 24+ services: - PostgreSQL (PostGIS), Valkey, MinIO, Qdrant - Vault (PKI/TLS), Nginx (Reverse Proxy) - Backend Core API, Consent Service, Billing Service - RAG Service, Embedding Service - Gitea, Woodpecker CI/CD - Night Scheduler, Health Aggregator - Jitsi (Web/XMPP/JVB/Jicofo), Mailpit Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1337 lines
36 KiB
JavaScript
1337 lines
36 KiB
JavaScript
// src/react/index.tsx
|
|
import {
|
|
createContext,
|
|
useContext,
|
|
useEffect,
|
|
useState,
|
|
useCallback,
|
|
useMemo
|
|
} from "react";
|
|
|
|
// src/core/ConsentStorage.ts
|
|
var STORAGE_KEY = "bp_consent";
|
|
var STORAGE_VERSION = "1";
|
|
var ConsentStorage = class {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this.storageKey = `${STORAGE_KEY}_${config.siteId}`;
|
|
}
|
|
/**
|
|
* Consent laden
|
|
*/
|
|
get() {
|
|
if (typeof window === "undefined") {
|
|
return null;
|
|
}
|
|
try {
|
|
const raw = localStorage.getItem(this.storageKey);
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
const stored = JSON.parse(raw);
|
|
if (stored.version !== STORAGE_VERSION) {
|
|
this.log("Storage version mismatch, clearing");
|
|
this.clear();
|
|
return null;
|
|
}
|
|
if (!this.verifySignature(stored.consent, stored.signature)) {
|
|
this.log("Invalid signature, clearing");
|
|
this.clear();
|
|
return null;
|
|
}
|
|
return stored.consent;
|
|
} catch (error) {
|
|
this.log("Failed to load consent:", error);
|
|
return null;
|
|
}
|
|
}
|
|
/**
|
|
* Consent speichern
|
|
*/
|
|
set(consent) {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
try {
|
|
const signature = this.generateSignature(consent);
|
|
const stored = {
|
|
version: STORAGE_VERSION,
|
|
consent,
|
|
signature
|
|
};
|
|
localStorage.setItem(this.storageKey, JSON.stringify(stored));
|
|
this.setCookie(consent);
|
|
this.log("Consent saved to storage");
|
|
} catch (error) {
|
|
this.log("Failed to save consent:", error);
|
|
}
|
|
}
|
|
/**
|
|
* Consent loeschen
|
|
*/
|
|
clear() {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
try {
|
|
localStorage.removeItem(this.storageKey);
|
|
this.clearCookie();
|
|
this.log("Consent cleared from storage");
|
|
} catch (error) {
|
|
this.log("Failed to clear consent:", error);
|
|
}
|
|
}
|
|
/**
|
|
* Pruefen ob Consent existiert
|
|
*/
|
|
exists() {
|
|
return this.get() !== null;
|
|
}
|
|
// ===========================================================================
|
|
// Cookie Management
|
|
// ===========================================================================
|
|
/**
|
|
* Consent als Cookie setzen
|
|
*/
|
|
setCookie(consent) {
|
|
const days = this.config.consent?.rememberDays ?? 365;
|
|
const expires = /* @__PURE__ */ new Date();
|
|
expires.setDate(expires.getDate() + days);
|
|
const cookieValue = JSON.stringify(consent.categories);
|
|
const encoded = encodeURIComponent(cookieValue);
|
|
document.cookie = [
|
|
`${this.storageKey}=${encoded}`,
|
|
`expires=${expires.toUTCString()}`,
|
|
"path=/",
|
|
"SameSite=Lax",
|
|
location.protocol === "https:" ? "Secure" : ""
|
|
].filter(Boolean).join("; ");
|
|
}
|
|
/**
|
|
* Cookie loeschen
|
|
*/
|
|
clearCookie() {
|
|
document.cookie = `${this.storageKey}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
}
|
|
// ===========================================================================
|
|
// Signature (Simple HMAC-like)
|
|
// ===========================================================================
|
|
/**
|
|
* Signatur generieren
|
|
*/
|
|
generateSignature(consent) {
|
|
const data = JSON.stringify(consent);
|
|
const key = this.config.siteId;
|
|
return this.simpleHash(data + key);
|
|
}
|
|
/**
|
|
* Signatur verifizieren
|
|
*/
|
|
verifySignature(consent, signature) {
|
|
const expected = this.generateSignature(consent);
|
|
return expected === signature;
|
|
}
|
|
/**
|
|
* Einfache Hash-Funktion (djb2)
|
|
*/
|
|
simpleHash(str) {
|
|
let hash = 5381;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = hash * 33 ^ str.charCodeAt(i);
|
|
}
|
|
return (hash >>> 0).toString(16);
|
|
}
|
|
/**
|
|
* Debug-Logging
|
|
*/
|
|
log(...args) {
|
|
if (this.config.debug) {
|
|
console.log("[ConsentStorage]", ...args);
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/core/ScriptBlocker.ts
|
|
var ScriptBlocker = class {
|
|
constructor(config) {
|
|
this.observer = null;
|
|
this.enabledCategories = /* @__PURE__ */ new Set(["essential"]);
|
|
this.processedElements = /* @__PURE__ */ new WeakSet();
|
|
this.config = config;
|
|
}
|
|
/**
|
|
* Initialisieren und Observer starten
|
|
*/
|
|
init() {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
this.processExistingElements();
|
|
this.observer = new MutationObserver((mutations) => {
|
|
for (const mutation of mutations) {
|
|
for (const node of mutation.addedNodes) {
|
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
this.processElement(node);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
this.observer.observe(document.documentElement, {
|
|
childList: true,
|
|
subtree: true
|
|
});
|
|
this.log("ScriptBlocker initialized");
|
|
}
|
|
/**
|
|
* Kategorie aktivieren
|
|
*/
|
|
enableCategory(category) {
|
|
if (this.enabledCategories.has(category)) {
|
|
return;
|
|
}
|
|
this.enabledCategories.add(category);
|
|
this.log("Category enabled:", category);
|
|
this.activateCategory(category);
|
|
}
|
|
/**
|
|
* Kategorie deaktivieren
|
|
*/
|
|
disableCategory(category) {
|
|
if (category === "essential") {
|
|
return;
|
|
}
|
|
this.enabledCategories.delete(category);
|
|
this.log("Category disabled:", category);
|
|
}
|
|
/**
|
|
* Alle Kategorien blockieren (ausser Essential)
|
|
*/
|
|
blockAll() {
|
|
this.enabledCategories.clear();
|
|
this.enabledCategories.add("essential");
|
|
this.log("All categories blocked");
|
|
}
|
|
/**
|
|
* Pruefen ob Kategorie aktiviert
|
|
*/
|
|
isCategoryEnabled(category) {
|
|
return this.enabledCategories.has(category);
|
|
}
|
|
/**
|
|
* Observer stoppen
|
|
*/
|
|
destroy() {
|
|
this.observer?.disconnect();
|
|
this.observer = null;
|
|
this.log("ScriptBlocker destroyed");
|
|
}
|
|
// ===========================================================================
|
|
// Internal Methods
|
|
// ===========================================================================
|
|
/**
|
|
* Bestehende Elemente verarbeiten
|
|
*/
|
|
processExistingElements() {
|
|
const scripts = document.querySelectorAll(
|
|
"script[data-consent]"
|
|
);
|
|
scripts.forEach((script) => this.processScript(script));
|
|
const iframes = document.querySelectorAll(
|
|
"iframe[data-consent]"
|
|
);
|
|
iframes.forEach((iframe) => this.processIframe(iframe));
|
|
this.log(`Processed ${scripts.length} scripts, ${iframes.length} iframes`);
|
|
}
|
|
/**
|
|
* Element verarbeiten
|
|
*/
|
|
processElement(element) {
|
|
if (element.tagName === "SCRIPT") {
|
|
this.processScript(element);
|
|
} else if (element.tagName === "IFRAME") {
|
|
this.processIframe(element);
|
|
}
|
|
element.querySelectorAll("script[data-consent]").forEach((script) => this.processScript(script));
|
|
element.querySelectorAll("iframe[data-consent]").forEach((iframe) => this.processIframe(iframe));
|
|
}
|
|
/**
|
|
* Script-Element verarbeiten
|
|
*/
|
|
processScript(script) {
|
|
if (this.processedElements.has(script)) {
|
|
return;
|
|
}
|
|
const category = script.dataset.consent;
|
|
if (!category) {
|
|
return;
|
|
}
|
|
this.processedElements.add(script);
|
|
if (this.enabledCategories.has(category)) {
|
|
this.activateScript(script);
|
|
} else {
|
|
this.log(`Script blocked (${category}):`, script.dataset.src || "inline");
|
|
}
|
|
}
|
|
/**
|
|
* iFrame-Element verarbeiten
|
|
*/
|
|
processIframe(iframe) {
|
|
if (this.processedElements.has(iframe)) {
|
|
return;
|
|
}
|
|
const category = iframe.dataset.consent;
|
|
if (!category) {
|
|
return;
|
|
}
|
|
this.processedElements.add(iframe);
|
|
if (this.enabledCategories.has(category)) {
|
|
this.activateIframe(iframe);
|
|
} else {
|
|
this.log(`iFrame blocked (${category}):`, iframe.dataset.src);
|
|
this.showPlaceholder(iframe, category);
|
|
}
|
|
}
|
|
/**
|
|
* Script aktivieren
|
|
*/
|
|
activateScript(script) {
|
|
const src = script.dataset.src;
|
|
if (src) {
|
|
const newScript = document.createElement("script");
|
|
for (const attr of script.attributes) {
|
|
if (attr.name !== "type" && attr.name !== "data-src") {
|
|
newScript.setAttribute(attr.name, attr.value);
|
|
}
|
|
}
|
|
newScript.src = src;
|
|
newScript.removeAttribute("data-consent");
|
|
script.parentNode?.replaceChild(newScript, script);
|
|
this.log("External script activated:", src);
|
|
} else {
|
|
const newScript = document.createElement("script");
|
|
for (const attr of script.attributes) {
|
|
if (attr.name !== "type") {
|
|
newScript.setAttribute(attr.name, attr.value);
|
|
}
|
|
}
|
|
newScript.textContent = script.textContent;
|
|
newScript.removeAttribute("data-consent");
|
|
script.parentNode?.replaceChild(newScript, script);
|
|
this.log("Inline script activated");
|
|
}
|
|
}
|
|
/**
|
|
* iFrame aktivieren
|
|
*/
|
|
activateIframe(iframe) {
|
|
const src = iframe.dataset.src;
|
|
if (!src) {
|
|
return;
|
|
}
|
|
const placeholder = iframe.parentElement?.querySelector(
|
|
".bp-consent-placeholder"
|
|
);
|
|
placeholder?.remove();
|
|
iframe.src = src;
|
|
iframe.removeAttribute("data-src");
|
|
iframe.removeAttribute("data-consent");
|
|
iframe.style.display = "";
|
|
this.log("iFrame activated:", src);
|
|
}
|
|
/**
|
|
* Placeholder fuer blockierten iFrame anzeigen
|
|
*/
|
|
showPlaceholder(iframe, category) {
|
|
iframe.style.display = "none";
|
|
const placeholder = document.createElement("div");
|
|
placeholder.className = "bp-consent-placeholder";
|
|
placeholder.setAttribute("data-category", category);
|
|
placeholder.innerHTML = `
|
|
<div class="bp-consent-placeholder-content">
|
|
<p>Dieser Inhalt erfordert Ihre Zustimmung.</p>
|
|
<button type="button" class="bp-consent-placeholder-btn">
|
|
${this.getCategoryName(category)} aktivieren
|
|
</button>
|
|
</div>
|
|
`;
|
|
const btn = placeholder.querySelector("button");
|
|
btn?.addEventListener("click", () => {
|
|
window.dispatchEvent(
|
|
new CustomEvent("bp-consent-request", {
|
|
detail: { category }
|
|
})
|
|
);
|
|
});
|
|
iframe.parentNode?.insertBefore(placeholder, iframe.nextSibling);
|
|
}
|
|
/**
|
|
* Alle Elemente einer Kategorie aktivieren
|
|
*/
|
|
activateCategory(category) {
|
|
const scripts = document.querySelectorAll(
|
|
`script[data-consent="${category}"]`
|
|
);
|
|
scripts.forEach((script) => this.activateScript(script));
|
|
const iframes = document.querySelectorAll(
|
|
`iframe[data-consent="${category}"]`
|
|
);
|
|
iframes.forEach((iframe) => this.activateIframe(iframe));
|
|
this.log(
|
|
`Activated ${scripts.length} scripts, ${iframes.length} iframes for ${category}`
|
|
);
|
|
}
|
|
/**
|
|
* Kategorie-Name fuer UI
|
|
*/
|
|
getCategoryName(category) {
|
|
const names = {
|
|
essential: "Essentielle Cookies",
|
|
functional: "Funktionale Cookies",
|
|
analytics: "Statistik-Cookies",
|
|
marketing: "Marketing-Cookies",
|
|
social: "Social Media-Cookies"
|
|
};
|
|
return names[category] ?? category;
|
|
}
|
|
/**
|
|
* Debug-Logging
|
|
*/
|
|
log(...args) {
|
|
if (this.config.debug) {
|
|
console.log("[ScriptBlocker]", ...args);
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/core/ConsentAPI.ts
|
|
var ConsentAPI = class {
|
|
constructor(config) {
|
|
this.config = config;
|
|
this.baseUrl = config.apiEndpoint.replace(/\/$/, "");
|
|
}
|
|
/**
|
|
* Consent speichern
|
|
*/
|
|
async saveConsent(request) {
|
|
const payload = {
|
|
...request,
|
|
metadata: {
|
|
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : "",
|
|
language: typeof navigator !== "undefined" ? navigator.language : "",
|
|
screenResolution: typeof window !== "undefined" ? `${window.screen.width}x${window.screen.height}` : "",
|
|
platform: "web",
|
|
...request.metadata
|
|
}
|
|
};
|
|
const response = await this.fetch("/consent", {
|
|
method: "POST",
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to save consent: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
/**
|
|
* Consent abrufen
|
|
*/
|
|
async getConsent(siteId, deviceFingerprint) {
|
|
const params = new URLSearchParams({
|
|
siteId,
|
|
deviceFingerprint
|
|
});
|
|
const response = await this.fetch(`/consent?${params}`);
|
|
if (response.status === 404) {
|
|
return null;
|
|
}
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to get consent: ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
return data.consent;
|
|
}
|
|
/**
|
|
* Consent widerrufen
|
|
*/
|
|
async revokeConsent(consentId) {
|
|
const response = await this.fetch(`/consent/${consentId}`, {
|
|
method: "DELETE"
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to revoke consent: ${response.status}`);
|
|
}
|
|
}
|
|
/**
|
|
* Site-Konfiguration abrufen
|
|
*/
|
|
async getSiteConfig(siteId) {
|
|
const response = await this.fetch(`/config/${siteId}`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to get site config: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
/**
|
|
* Consent-Historie exportieren (DSGVO Art. 20)
|
|
*/
|
|
async exportConsent(userId) {
|
|
const params = new URLSearchParams({ userId });
|
|
const response = await this.fetch(`/consent/export?${params}`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to export consent: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
// ===========================================================================
|
|
// Internal Methods
|
|
// ===========================================================================
|
|
/**
|
|
* Fetch mit Standard-Headers
|
|
*/
|
|
async fetch(path, options = {}) {
|
|
const url = `${this.baseUrl}${path}`;
|
|
const headers = {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
...this.getSignatureHeaders(),
|
|
...options.headers || {}
|
|
};
|
|
try {
|
|
const response = await fetch(url, {
|
|
...options,
|
|
headers,
|
|
credentials: "include"
|
|
});
|
|
this.log(`${options.method || "GET"} ${path}:`, response.status);
|
|
return response;
|
|
} catch (error) {
|
|
this.log("Fetch error:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
/**
|
|
* Signatur-Headers generieren (HMAC)
|
|
*/
|
|
getSignatureHeaders() {
|
|
const timestamp = Math.floor(Date.now() / 1e3).toString();
|
|
const signature = this.simpleHash(`${this.config.siteId}:${timestamp}`);
|
|
return {
|
|
"X-Consent-Timestamp": timestamp,
|
|
"X-Consent-Signature": `sha256=${signature}`
|
|
};
|
|
}
|
|
/**
|
|
* Einfache Hash-Funktion (djb2)
|
|
*/
|
|
simpleHash(str) {
|
|
let hash = 5381;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = hash * 33 ^ str.charCodeAt(i);
|
|
}
|
|
return (hash >>> 0).toString(16);
|
|
}
|
|
/**
|
|
* Debug-Logging
|
|
*/
|
|
log(...args) {
|
|
if (this.config.debug) {
|
|
console.log("[ConsentAPI]", ...args);
|
|
}
|
|
}
|
|
};
|
|
|
|
// src/utils/EventEmitter.ts
|
|
var EventEmitter = class {
|
|
constructor() {
|
|
this.listeners = /* @__PURE__ */ new Map();
|
|
}
|
|
/**
|
|
* Event-Listener registrieren
|
|
* @returns Unsubscribe-Funktion
|
|
*/
|
|
on(event, callback) {
|
|
if (!this.listeners.has(event)) {
|
|
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
}
|
|
this.listeners.get(event).add(callback);
|
|
return () => this.off(event, callback);
|
|
}
|
|
/**
|
|
* Event-Listener entfernen
|
|
*/
|
|
off(event, callback) {
|
|
this.listeners.get(event)?.delete(callback);
|
|
}
|
|
/**
|
|
* Event emittieren
|
|
*/
|
|
emit(event, data) {
|
|
this.listeners.get(event)?.forEach((callback) => {
|
|
try {
|
|
callback(data);
|
|
} catch (error) {
|
|
console.error(`Error in event handler for ${String(event)}:`, error);
|
|
}
|
|
});
|
|
}
|
|
/**
|
|
* Einmaligen Listener registrieren
|
|
*/
|
|
once(event, callback) {
|
|
const wrapper = (data) => {
|
|
this.off(event, wrapper);
|
|
callback(data);
|
|
};
|
|
return this.on(event, wrapper);
|
|
}
|
|
/**
|
|
* Alle Listener entfernen
|
|
*/
|
|
clear() {
|
|
this.listeners.clear();
|
|
}
|
|
/**
|
|
* Alle Listener fuer ein Event entfernen
|
|
*/
|
|
clearEvent(event) {
|
|
this.listeners.delete(event);
|
|
}
|
|
/**
|
|
* Anzahl Listener fuer ein Event
|
|
*/
|
|
listenerCount(event) {
|
|
return this.listeners.get(event)?.size ?? 0;
|
|
}
|
|
};
|
|
|
|
// src/utils/fingerprint.ts
|
|
function getComponents() {
|
|
if (typeof window === "undefined") {
|
|
return ["server"];
|
|
}
|
|
const components = [];
|
|
try {
|
|
const ua = navigator.userAgent;
|
|
if (ua.includes("Chrome")) components.push("chrome");
|
|
else if (ua.includes("Firefox")) components.push("firefox");
|
|
else if (ua.includes("Safari")) components.push("safari");
|
|
else if (ua.includes("Edge")) components.push("edge");
|
|
else components.push("other");
|
|
} catch {
|
|
components.push("unknown-browser");
|
|
}
|
|
try {
|
|
components.push(navigator.language || "unknown-lang");
|
|
} catch {
|
|
components.push("unknown-lang");
|
|
}
|
|
try {
|
|
const width = window.screen.width;
|
|
if (width >= 2560) components.push("4k");
|
|
else if (width >= 1920) components.push("fhd");
|
|
else if (width >= 1366) components.push("hd");
|
|
else if (width >= 768) components.push("tablet");
|
|
else components.push("mobile");
|
|
} catch {
|
|
components.push("unknown-screen");
|
|
}
|
|
try {
|
|
const depth = window.screen.colorDepth;
|
|
if (depth >= 24) components.push("deep-color");
|
|
else components.push("standard-color");
|
|
} catch {
|
|
components.push("unknown-color");
|
|
}
|
|
try {
|
|
const offset = (/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
const hours = Math.floor(Math.abs(offset) / 60);
|
|
const sign = offset <= 0 ? "+" : "-";
|
|
components.push(`tz${sign}${hours}`);
|
|
} catch {
|
|
components.push("unknown-tz");
|
|
}
|
|
try {
|
|
const platform = navigator.platform?.toLowerCase() || "";
|
|
if (platform.includes("mac")) components.push("mac");
|
|
else if (platform.includes("win")) components.push("win");
|
|
else if (platform.includes("linux")) components.push("linux");
|
|
else if (platform.includes("iphone") || platform.includes("ipad"))
|
|
components.push("ios");
|
|
else if (platform.includes("android")) components.push("android");
|
|
else components.push("other-platform");
|
|
} catch {
|
|
components.push("unknown-platform");
|
|
}
|
|
try {
|
|
if ("ontouchstart" in window || navigator.maxTouchPoints > 0) {
|
|
components.push("touch");
|
|
} else {
|
|
components.push("no-touch");
|
|
}
|
|
} catch {
|
|
components.push("unknown-touch");
|
|
}
|
|
try {
|
|
if (navigator.doNotTrack === "1") {
|
|
components.push("dnt");
|
|
}
|
|
} catch {
|
|
}
|
|
return components;
|
|
}
|
|
async function sha256(message) {
|
|
if (typeof window === "undefined" || !window.crypto?.subtle) {
|
|
return simpleHash(message);
|
|
}
|
|
try {
|
|
const encoder = new TextEncoder();
|
|
const data = encoder.encode(message);
|
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
|
return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
} catch {
|
|
return simpleHash(message);
|
|
}
|
|
}
|
|
function simpleHash(str) {
|
|
let hash = 5381;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = hash * 33 ^ str.charCodeAt(i);
|
|
}
|
|
return (hash >>> 0).toString(16).padStart(8, "0");
|
|
}
|
|
async function generateFingerprint() {
|
|
const components = getComponents();
|
|
const combined = components.join("|");
|
|
const hash = await sha256(combined);
|
|
return `fp_${hash.substring(0, 32)}`;
|
|
}
|
|
|
|
// src/version.ts
|
|
var SDK_VERSION = "1.0.0";
|
|
|
|
// src/core/ConsentManager.ts
|
|
var DEFAULT_CONFIG = {
|
|
language: "de",
|
|
fallbackLanguage: "en",
|
|
ui: {
|
|
position: "bottom",
|
|
layout: "modal",
|
|
theme: "auto",
|
|
zIndex: 999999,
|
|
blockScrollOnModal: true
|
|
},
|
|
consent: {
|
|
required: true,
|
|
rejectAllVisible: true,
|
|
acceptAllVisible: true,
|
|
granularControl: true,
|
|
vendorControl: false,
|
|
rememberChoice: true,
|
|
rememberDays: 365,
|
|
geoTargeting: false,
|
|
recheckAfterDays: 180
|
|
},
|
|
categories: ["essential", "functional", "analytics", "marketing", "social"],
|
|
debug: false
|
|
};
|
|
var DEFAULT_CONSENT = {
|
|
essential: true,
|
|
functional: false,
|
|
analytics: false,
|
|
marketing: false,
|
|
social: false
|
|
};
|
|
var ConsentManager = class {
|
|
constructor(config) {
|
|
this.currentConsent = null;
|
|
this.initialized = false;
|
|
this.bannerVisible = false;
|
|
this.deviceFingerprint = "";
|
|
this.config = this.mergeConfig(config);
|
|
this.storage = new ConsentStorage(this.config);
|
|
this.scriptBlocker = new ScriptBlocker(this.config);
|
|
this.api = new ConsentAPI(this.config);
|
|
this.events = new EventEmitter();
|
|
this.log("ConsentManager created with config:", this.config);
|
|
}
|
|
/**
|
|
* SDK initialisieren
|
|
*/
|
|
async init() {
|
|
if (this.initialized) {
|
|
this.log("Already initialized, skipping");
|
|
return;
|
|
}
|
|
try {
|
|
this.log("Initializing ConsentManager...");
|
|
this.deviceFingerprint = await generateFingerprint();
|
|
this.currentConsent = this.storage.get();
|
|
if (this.currentConsent) {
|
|
this.log("Loaded consent from storage:", this.currentConsent);
|
|
if (this.isConsentExpired()) {
|
|
this.log("Consent expired, clearing");
|
|
this.storage.clear();
|
|
this.currentConsent = null;
|
|
} else {
|
|
this.applyConsent();
|
|
}
|
|
}
|
|
this.scriptBlocker.init();
|
|
this.initialized = true;
|
|
this.emit("init", this.currentConsent);
|
|
if (this.needsConsent()) {
|
|
this.showBanner();
|
|
}
|
|
this.log("ConsentManager initialized successfully");
|
|
} catch (error) {
|
|
this.handleError(error);
|
|
throw error;
|
|
}
|
|
}
|
|
// ===========================================================================
|
|
// Public API
|
|
// ===========================================================================
|
|
/**
|
|
* Pruefen ob Consent fuer Kategorie vorhanden
|
|
*/
|
|
hasConsent(category) {
|
|
if (!this.currentConsent) {
|
|
return category === "essential";
|
|
}
|
|
return this.currentConsent.categories[category] ?? false;
|
|
}
|
|
/**
|
|
* Pruefen ob Consent fuer Vendor vorhanden
|
|
*/
|
|
hasVendorConsent(vendorId) {
|
|
if (!this.currentConsent) {
|
|
return false;
|
|
}
|
|
return this.currentConsent.vendors[vendorId] ?? false;
|
|
}
|
|
/**
|
|
* Aktuellen Consent-State abrufen
|
|
*/
|
|
getConsent() {
|
|
return this.currentConsent ? { ...this.currentConsent } : null;
|
|
}
|
|
/**
|
|
* Consent setzen
|
|
*/
|
|
async setConsent(input) {
|
|
const categories = this.normalizeConsentInput(input);
|
|
categories.essential = true;
|
|
const newConsent = {
|
|
categories,
|
|
vendors: "vendors" in input && input.vendors ? input.vendors : {},
|
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
version: SDK_VERSION
|
|
};
|
|
try {
|
|
const response = await this.api.saveConsent({
|
|
siteId: this.config.siteId,
|
|
deviceFingerprint: this.deviceFingerprint,
|
|
consent: newConsent
|
|
});
|
|
newConsent.consentId = response.consentId;
|
|
newConsent.expiresAt = response.expiresAt;
|
|
this.storage.set(newConsent);
|
|
this.currentConsent = newConsent;
|
|
this.applyConsent();
|
|
this.emit("change", newConsent);
|
|
this.config.onConsentChange?.(newConsent);
|
|
this.log("Consent saved:", newConsent);
|
|
} catch (error) {
|
|
this.log("API error, saving locally:", error);
|
|
this.storage.set(newConsent);
|
|
this.currentConsent = newConsent;
|
|
this.applyConsent();
|
|
this.emit("change", newConsent);
|
|
}
|
|
}
|
|
/**
|
|
* Alle Kategorien akzeptieren
|
|
*/
|
|
async acceptAll() {
|
|
const allCategories = {
|
|
essential: true,
|
|
functional: true,
|
|
analytics: true,
|
|
marketing: true,
|
|
social: true
|
|
};
|
|
await this.setConsent(allCategories);
|
|
this.emit("accept_all", this.currentConsent);
|
|
this.hideBanner();
|
|
}
|
|
/**
|
|
* Alle nicht-essentiellen Kategorien ablehnen
|
|
*/
|
|
async rejectAll() {
|
|
const minimalCategories = {
|
|
essential: true,
|
|
functional: false,
|
|
analytics: false,
|
|
marketing: false,
|
|
social: false
|
|
};
|
|
await this.setConsent(minimalCategories);
|
|
this.emit("reject_all", this.currentConsent);
|
|
this.hideBanner();
|
|
}
|
|
/**
|
|
* Alle Einwilligungen widerrufen
|
|
*/
|
|
async revokeAll() {
|
|
if (this.currentConsent?.consentId) {
|
|
try {
|
|
await this.api.revokeConsent(this.currentConsent.consentId);
|
|
} catch (error) {
|
|
this.log("Failed to revoke on server:", error);
|
|
}
|
|
}
|
|
this.storage.clear();
|
|
this.currentConsent = null;
|
|
this.scriptBlocker.blockAll();
|
|
this.log("All consents revoked");
|
|
}
|
|
/**
|
|
* Consent-Daten exportieren (DSGVO Art. 20)
|
|
*/
|
|
async exportConsent() {
|
|
const exportData = {
|
|
currentConsent: this.currentConsent,
|
|
exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
siteId: this.config.siteId,
|
|
deviceFingerprint: this.deviceFingerprint
|
|
};
|
|
return JSON.stringify(exportData, null, 2);
|
|
}
|
|
// ===========================================================================
|
|
// Banner Control
|
|
// ===========================================================================
|
|
/**
|
|
* Pruefen ob Consent-Abfrage noetig
|
|
*/
|
|
needsConsent() {
|
|
if (!this.currentConsent) {
|
|
return true;
|
|
}
|
|
if (this.isConsentExpired()) {
|
|
return true;
|
|
}
|
|
if (this.config.consent?.recheckAfterDays) {
|
|
const consentDate = new Date(this.currentConsent.timestamp);
|
|
const recheckDate = new Date(consentDate);
|
|
recheckDate.setDate(
|
|
recheckDate.getDate() + this.config.consent.recheckAfterDays
|
|
);
|
|
if (/* @__PURE__ */ new Date() > recheckDate) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
/**
|
|
* Banner anzeigen
|
|
*/
|
|
showBanner() {
|
|
if (this.bannerVisible) {
|
|
return;
|
|
}
|
|
this.bannerVisible = true;
|
|
this.emit("banner_show", void 0);
|
|
this.config.onBannerShow?.();
|
|
this.log("Banner shown");
|
|
}
|
|
/**
|
|
* Banner verstecken
|
|
*/
|
|
hideBanner() {
|
|
if (!this.bannerVisible) {
|
|
return;
|
|
}
|
|
this.bannerVisible = false;
|
|
this.emit("banner_hide", void 0);
|
|
this.config.onBannerHide?.();
|
|
this.log("Banner hidden");
|
|
}
|
|
/**
|
|
* Einstellungs-Modal oeffnen
|
|
*/
|
|
showSettings() {
|
|
this.emit("settings_open", void 0);
|
|
this.log("Settings opened");
|
|
}
|
|
/**
|
|
* Pruefen ob Banner sichtbar
|
|
*/
|
|
isBannerVisible() {
|
|
return this.bannerVisible;
|
|
}
|
|
// ===========================================================================
|
|
// Event Handling
|
|
// ===========================================================================
|
|
/**
|
|
* Event-Listener registrieren
|
|
*/
|
|
on(event, callback) {
|
|
return this.events.on(event, callback);
|
|
}
|
|
/**
|
|
* Event-Listener entfernen
|
|
*/
|
|
off(event, callback) {
|
|
this.events.off(event, callback);
|
|
}
|
|
// ===========================================================================
|
|
// Internal Methods
|
|
// ===========================================================================
|
|
/**
|
|
* Konfiguration zusammenfuehren
|
|
*/
|
|
mergeConfig(config) {
|
|
return {
|
|
...DEFAULT_CONFIG,
|
|
...config,
|
|
ui: { ...DEFAULT_CONFIG.ui, ...config.ui },
|
|
consent: { ...DEFAULT_CONFIG.consent, ...config.consent }
|
|
};
|
|
}
|
|
/**
|
|
* Consent-Input normalisieren
|
|
*/
|
|
normalizeConsentInput(input) {
|
|
if ("categories" in input && input.categories) {
|
|
return { ...DEFAULT_CONSENT, ...input.categories };
|
|
}
|
|
return { ...DEFAULT_CONSENT, ...input };
|
|
}
|
|
/**
|
|
* Consent anwenden (Skripte aktivieren/blockieren)
|
|
*/
|
|
applyConsent() {
|
|
if (!this.currentConsent) {
|
|
return;
|
|
}
|
|
for (const [category, allowed] of Object.entries(
|
|
this.currentConsent.categories
|
|
)) {
|
|
if (allowed) {
|
|
this.scriptBlocker.enableCategory(category);
|
|
} else {
|
|
this.scriptBlocker.disableCategory(category);
|
|
}
|
|
}
|
|
this.updateGoogleConsentMode();
|
|
}
|
|
/**
|
|
* Google Consent Mode v2 aktualisieren
|
|
*/
|
|
updateGoogleConsentMode() {
|
|
if (typeof window === "undefined" || !this.currentConsent) {
|
|
return;
|
|
}
|
|
const gtag = window.gtag;
|
|
if (typeof gtag !== "function") {
|
|
return;
|
|
}
|
|
const { categories } = this.currentConsent;
|
|
gtag("consent", "update", {
|
|
ad_storage: categories.marketing ? "granted" : "denied",
|
|
ad_user_data: categories.marketing ? "granted" : "denied",
|
|
ad_personalization: categories.marketing ? "granted" : "denied",
|
|
analytics_storage: categories.analytics ? "granted" : "denied",
|
|
functionality_storage: categories.functional ? "granted" : "denied",
|
|
personalization_storage: categories.functional ? "granted" : "denied",
|
|
security_storage: "granted"
|
|
});
|
|
this.log("Google Consent Mode updated");
|
|
}
|
|
/**
|
|
* Pruefen ob Consent abgelaufen
|
|
*/
|
|
isConsentExpired() {
|
|
if (!this.currentConsent?.expiresAt) {
|
|
if (this.currentConsent?.timestamp && this.config.consent?.rememberDays) {
|
|
const consentDate = new Date(this.currentConsent.timestamp);
|
|
const expiryDate = new Date(consentDate);
|
|
expiryDate.setDate(
|
|
expiryDate.getDate() + this.config.consent.rememberDays
|
|
);
|
|
return /* @__PURE__ */ new Date() > expiryDate;
|
|
}
|
|
return false;
|
|
}
|
|
return /* @__PURE__ */ new Date() > new Date(this.currentConsent.expiresAt);
|
|
}
|
|
/**
|
|
* Event emittieren
|
|
*/
|
|
emit(event, data) {
|
|
this.events.emit(event, data);
|
|
}
|
|
/**
|
|
* Fehler behandeln
|
|
*/
|
|
handleError(error) {
|
|
this.log("Error:", error);
|
|
this.emit("error", error);
|
|
this.config.onError?.(error);
|
|
}
|
|
/**
|
|
* Debug-Logging
|
|
*/
|
|
log(...args) {
|
|
if (this.config.debug) {
|
|
console.log("[ConsentSDK]", ...args);
|
|
}
|
|
}
|
|
// ===========================================================================
|
|
// Static Methods
|
|
// ===========================================================================
|
|
/**
|
|
* SDK-Version abrufen
|
|
*/
|
|
static getVersion() {
|
|
return SDK_VERSION;
|
|
}
|
|
};
|
|
|
|
// src/react/index.tsx
|
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
var ConsentContext = createContext(null);
|
|
var ConsentProvider = ({
|
|
config,
|
|
children
|
|
}) => {
|
|
const [manager, setManager] = useState(null);
|
|
const [consent, setConsent] = useState(null);
|
|
const [isInitialized, setIsInitialized] = useState(false);
|
|
const [isLoading, setIsLoading] = useState(true);
|
|
const [isBannerVisible, setIsBannerVisible] = useState(false);
|
|
useEffect(() => {
|
|
const consentManager = new ConsentManager(config);
|
|
setManager(consentManager);
|
|
const unsubChange = consentManager.on("change", (newConsent) => {
|
|
setConsent(newConsent);
|
|
});
|
|
const unsubBannerShow = consentManager.on("banner_show", () => {
|
|
setIsBannerVisible(true);
|
|
});
|
|
const unsubBannerHide = consentManager.on("banner_hide", () => {
|
|
setIsBannerVisible(false);
|
|
});
|
|
consentManager.init().then(() => {
|
|
setConsent(consentManager.getConsent());
|
|
setIsInitialized(true);
|
|
setIsLoading(false);
|
|
setIsBannerVisible(consentManager.isBannerVisible());
|
|
}).catch((error) => {
|
|
console.error("Failed to initialize ConsentManager:", error);
|
|
setIsLoading(false);
|
|
});
|
|
return () => {
|
|
unsubChange();
|
|
unsubBannerShow();
|
|
unsubBannerHide();
|
|
};
|
|
}, [config]);
|
|
const hasConsent = useCallback(
|
|
(category) => {
|
|
return manager?.hasConsent(category) ?? category === "essential";
|
|
},
|
|
[manager]
|
|
);
|
|
const acceptAll = useCallback(async () => {
|
|
await manager?.acceptAll();
|
|
}, [manager]);
|
|
const rejectAll = useCallback(async () => {
|
|
await manager?.rejectAll();
|
|
}, [manager]);
|
|
const saveSelection = useCallback(
|
|
async (categories) => {
|
|
await manager?.setConsent(categories);
|
|
manager?.hideBanner();
|
|
},
|
|
[manager]
|
|
);
|
|
const showBanner = useCallback(() => {
|
|
manager?.showBanner();
|
|
}, [manager]);
|
|
const hideBanner = useCallback(() => {
|
|
manager?.hideBanner();
|
|
}, [manager]);
|
|
const showSettings = useCallback(() => {
|
|
manager?.showSettings();
|
|
}, [manager]);
|
|
const needsConsent = useMemo(() => {
|
|
return manager?.needsConsent() ?? true;
|
|
}, [manager, consent]);
|
|
const contextValue = useMemo(
|
|
() => ({
|
|
manager,
|
|
consent,
|
|
isInitialized,
|
|
isLoading,
|
|
isBannerVisible,
|
|
needsConsent,
|
|
hasConsent,
|
|
acceptAll,
|
|
rejectAll,
|
|
saveSelection,
|
|
showBanner,
|
|
hideBanner,
|
|
showSettings
|
|
}),
|
|
[
|
|
manager,
|
|
consent,
|
|
isInitialized,
|
|
isLoading,
|
|
isBannerVisible,
|
|
needsConsent,
|
|
hasConsent,
|
|
acceptAll,
|
|
rejectAll,
|
|
saveSelection,
|
|
showBanner,
|
|
hideBanner,
|
|
showSettings
|
|
]
|
|
);
|
|
return /* @__PURE__ */ jsx(ConsentContext.Provider, { value: contextValue, children });
|
|
};
|
|
function useConsent(category) {
|
|
const context = useContext(ConsentContext);
|
|
if (!context) {
|
|
throw new Error("useConsent must be used within a ConsentProvider");
|
|
}
|
|
if (category) {
|
|
return {
|
|
...context,
|
|
allowed: context.hasConsent(category)
|
|
};
|
|
}
|
|
return context;
|
|
}
|
|
function useConsentManager() {
|
|
const context = useContext(ConsentContext);
|
|
return context?.manager ?? null;
|
|
}
|
|
var ConsentGate = ({
|
|
category,
|
|
children,
|
|
placeholder = null,
|
|
fallback = null
|
|
}) => {
|
|
const { hasConsent, isLoading } = useConsent();
|
|
if (isLoading) {
|
|
return /* @__PURE__ */ jsx(Fragment, { children: fallback });
|
|
}
|
|
if (!hasConsent(category)) {
|
|
return /* @__PURE__ */ jsx(Fragment, { children: placeholder });
|
|
}
|
|
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
};
|
|
var ConsentPlaceholder = ({
|
|
category,
|
|
message,
|
|
buttonText,
|
|
className = ""
|
|
}) => {
|
|
const { showSettings } = useConsent();
|
|
const categoryNames = {
|
|
essential: "Essentielle Cookies",
|
|
functional: "Funktionale Cookies",
|
|
analytics: "Statistik-Cookies",
|
|
marketing: "Marketing-Cookies",
|
|
social: "Social Media-Cookies"
|
|
};
|
|
const defaultMessage = `Dieser Inhalt erfordert ${categoryNames[category]}.`;
|
|
return /* @__PURE__ */ jsxs("div", { className: `bp-consent-placeholder ${className}`, children: [
|
|
/* @__PURE__ */ jsx("p", { children: message || defaultMessage }),
|
|
/* @__PURE__ */ jsx("button", { type: "button", onClick: showSettings, children: buttonText || "Cookie-Einstellungen oeffnen" })
|
|
] });
|
|
};
|
|
var ConsentBanner = ({ render, className }) => {
|
|
const {
|
|
consent,
|
|
isBannerVisible,
|
|
needsConsent,
|
|
acceptAll,
|
|
rejectAll,
|
|
saveSelection,
|
|
showSettings,
|
|
hideBanner
|
|
} = useConsent();
|
|
const renderProps = {
|
|
isVisible: isBannerVisible,
|
|
consent,
|
|
needsConsent,
|
|
onAcceptAll: acceptAll,
|
|
onRejectAll: rejectAll,
|
|
onSaveSelection: saveSelection,
|
|
onShowSettings: showSettings,
|
|
onClose: hideBanner
|
|
};
|
|
if (render) {
|
|
return /* @__PURE__ */ jsx(Fragment, { children: render(renderProps) });
|
|
}
|
|
if (!isBannerVisible) {
|
|
return null;
|
|
}
|
|
return /* @__PURE__ */ jsx(
|
|
"div",
|
|
{
|
|
className: `bp-consent-banner ${className || ""}`,
|
|
role: "dialog",
|
|
"aria-modal": "true",
|
|
"aria-label": "Cookie-Einstellungen",
|
|
children: /* @__PURE__ */ jsxs("div", { className: "bp-consent-banner-content", children: [
|
|
/* @__PURE__ */ jsx("h2", { children: "Datenschutzeinstellungen" }),
|
|
/* @__PURE__ */ jsx("p", { children: "Wir nutzen Cookies und aehnliche Technologien, um Ihnen ein optimales Nutzererlebnis zu bieten." }),
|
|
/* @__PURE__ */ jsxs("div", { className: "bp-consent-banner-actions", children: [
|
|
/* @__PURE__ */ jsx(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bp-consent-btn bp-consent-btn-reject",
|
|
onClick: rejectAll,
|
|
children: "Alle ablehnen"
|
|
}
|
|
),
|
|
/* @__PURE__ */ jsx(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bp-consent-btn bp-consent-btn-settings",
|
|
onClick: showSettings,
|
|
children: "Einstellungen"
|
|
}
|
|
),
|
|
/* @__PURE__ */ jsx(
|
|
"button",
|
|
{
|
|
type: "button",
|
|
className: "bp-consent-btn bp-consent-btn-accept",
|
|
onClick: acceptAll,
|
|
children: "Alle akzeptieren"
|
|
}
|
|
)
|
|
] })
|
|
] })
|
|
}
|
|
);
|
|
};
|
|
export {
|
|
ConsentBanner,
|
|
ConsentContext,
|
|
ConsentGate,
|
|
ConsentPlaceholder,
|
|
ConsentProvider,
|
|
useConsent,
|
|
useConsentManager
|
|
};
|
|
//# sourceMappingURL=index.mjs.map
|