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>
1330 lines
36 KiB
JavaScript
1330 lines
36 KiB
JavaScript
"use strict";
|
|
var __defProp = Object.defineProperty;
|
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
var __export = (target, all) => {
|
|
for (var name in all)
|
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
};
|
|
var __copyProps = (to, from, except, desc) => {
|
|
if (from && typeof from === "object" || typeof from === "function") {
|
|
for (let key of __getOwnPropNames(from))
|
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
}
|
|
return to;
|
|
};
|
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
|
|
// src/angular/index.ts
|
|
var index_exports = {};
|
|
__export(index_exports, {
|
|
CONSENT_BANNER_TEMPLATE: () => CONSENT_BANNER_TEMPLATE,
|
|
CONSENT_CONFIG: () => CONSENT_CONFIG,
|
|
CONSENT_GATE_USAGE: () => CONSENT_GATE_USAGE,
|
|
CONSENT_SERVICE: () => CONSENT_SERVICE,
|
|
ConsentModuleDefinition: () => ConsentModuleDefinition,
|
|
ConsentServiceBase: () => ConsentServiceBase,
|
|
consentServiceFactory: () => consentServiceFactory
|
|
});
|
|
module.exports = __toCommonJS(index_exports);
|
|
|
|
// 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/angular/index.ts
|
|
var ConsentServiceBase = class {
|
|
constructor(config) {
|
|
this._consent = null;
|
|
this._isInitialized = false;
|
|
this._isLoading = true;
|
|
this._isBannerVisible = false;
|
|
// Callbacks fuer Angular Change Detection
|
|
this.changeCallbacks = [];
|
|
this.bannerShowCallbacks = [];
|
|
this.bannerHideCallbacks = [];
|
|
this.manager = new ConsentManager(config);
|
|
this.setupEventListeners();
|
|
this.initialize();
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Getters
|
|
// ---------------------------------------------------------------------------
|
|
get isInitialized() {
|
|
return this._isInitialized;
|
|
}
|
|
get isLoading() {
|
|
return this._isLoading;
|
|
}
|
|
get isBannerVisible() {
|
|
return this._isBannerVisible;
|
|
}
|
|
get consent() {
|
|
return this._consent;
|
|
}
|
|
get needsConsent() {
|
|
return this.manager.needsConsent();
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Methods
|
|
// ---------------------------------------------------------------------------
|
|
hasConsent(category) {
|
|
return this.manager.hasConsent(category);
|
|
}
|
|
async acceptAll() {
|
|
await this.manager.acceptAll();
|
|
}
|
|
async rejectAll() {
|
|
await this.manager.rejectAll();
|
|
}
|
|
async saveSelection(categories) {
|
|
await this.manager.setConsent(categories);
|
|
this.manager.hideBanner();
|
|
}
|
|
showBanner() {
|
|
this.manager.showBanner();
|
|
}
|
|
hideBanner() {
|
|
this.manager.hideBanner();
|
|
}
|
|
showSettings() {
|
|
this.manager.showSettings();
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Change Detection Support
|
|
// ---------------------------------------------------------------------------
|
|
/**
|
|
* Registriert Callback fuer Consent-Aenderungen
|
|
* (fuer Angular Change Detection)
|
|
*/
|
|
onConsentChange(callback) {
|
|
this.changeCallbacks.push(callback);
|
|
return () => {
|
|
const index = this.changeCallbacks.indexOf(callback);
|
|
if (index > -1) {
|
|
this.changeCallbacks.splice(index, 1);
|
|
}
|
|
};
|
|
}
|
|
/**
|
|
* Registriert Callback wenn Banner angezeigt wird
|
|
*/
|
|
onBannerShow(callback) {
|
|
this.bannerShowCallbacks.push(callback);
|
|
return () => {
|
|
const index = this.bannerShowCallbacks.indexOf(callback);
|
|
if (index > -1) {
|
|
this.bannerShowCallbacks.splice(index, 1);
|
|
}
|
|
};
|
|
}
|
|
/**
|
|
* Registriert Callback wenn Banner ausgeblendet wird
|
|
*/
|
|
onBannerHide(callback) {
|
|
this.bannerHideCallbacks.push(callback);
|
|
return () => {
|
|
const index = this.bannerHideCallbacks.indexOf(callback);
|
|
if (index > -1) {
|
|
this.bannerHideCallbacks.splice(index, 1);
|
|
}
|
|
};
|
|
}
|
|
// ---------------------------------------------------------------------------
|
|
// Internal
|
|
// ---------------------------------------------------------------------------
|
|
setupEventListeners() {
|
|
this.manager.on("change", (consent) => {
|
|
this._consent = consent;
|
|
this.changeCallbacks.forEach((cb) => cb(consent));
|
|
});
|
|
this.manager.on("banner_show", () => {
|
|
this._isBannerVisible = true;
|
|
this.bannerShowCallbacks.forEach((cb) => cb());
|
|
});
|
|
this.manager.on("banner_hide", () => {
|
|
this._isBannerVisible = false;
|
|
this.bannerHideCallbacks.forEach((cb) => cb());
|
|
});
|
|
}
|
|
async initialize() {
|
|
try {
|
|
await this.manager.init();
|
|
this._consent = this.manager.getConsent();
|
|
this._isInitialized = true;
|
|
this._isBannerVisible = this.manager.isBannerVisible();
|
|
} catch (error) {
|
|
console.error("Failed to initialize ConsentManager:", error);
|
|
} finally {
|
|
this._isLoading = false;
|
|
}
|
|
}
|
|
};
|
|
var CONSENT_CONFIG = "CONSENT_CONFIG";
|
|
var CONSENT_SERVICE = "CONSENT_SERVICE";
|
|
function consentServiceFactory(config) {
|
|
return new ConsentServiceBase(config);
|
|
}
|
|
var ConsentModuleDefinition = {
|
|
/**
|
|
* Providers fuer Root-Module
|
|
*/
|
|
forRoot: (config) => ({
|
|
provide: CONSENT_CONFIG,
|
|
useValue: config
|
|
})
|
|
};
|
|
var CONSENT_BANNER_TEMPLATE = `
|
|
<div
|
|
*ngIf="consent.isBannerVisible"
|
|
class="bp-consent-banner"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
aria-label="Cookie-Einstellungen"
|
|
>
|
|
<div class="bp-consent-banner-content">
|
|
<h2>Datenschutzeinstellungen</h2>
|
|
<p>
|
|
Wir nutzen Cookies und \xE4hnliche Technologien, um Ihnen ein optimales
|
|
Nutzererlebnis zu bieten.
|
|
</p>
|
|
<div class="bp-consent-banner-actions">
|
|
<button
|
|
type="button"
|
|
class="bp-consent-btn bp-consent-btn-reject"
|
|
(click)="consent.rejectAll()"
|
|
>
|
|
Alle ablehnen
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="bp-consent-btn bp-consent-btn-settings"
|
|
(click)="consent.showSettings()"
|
|
>
|
|
Einstellungen
|
|
</button>
|
|
<button
|
|
type="button"
|
|
class="bp-consent-btn bp-consent-btn-accept"
|
|
(click)="consent.acceptAll()"
|
|
>
|
|
Alle akzeptieren
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
var CONSENT_GATE_USAGE = `
|
|
<!-- Verwendung in Templates -->
|
|
<div *bpConsentGate="'analytics'">
|
|
<analytics-component></analytics-component>
|
|
</div>
|
|
|
|
<!-- Mit else Template -->
|
|
<ng-container *bpConsentGate="'marketing'; else placeholder">
|
|
<marketing-component></marketing-component>
|
|
</ng-container>
|
|
<ng-template #placeholder>
|
|
<p>Bitte akzeptieren Sie Marketing-Cookies.</p>
|
|
</ng-template>
|
|
`;
|
|
// Annotate the CommonJS export names for ESM import in node:
|
|
0 && (module.exports = {
|
|
CONSENT_BANNER_TEMPLATE,
|
|
CONSENT_CONFIG,
|
|
CONSENT_GATE_USAGE,
|
|
CONSENT_SERVICE,
|
|
ConsentModuleDefinition,
|
|
ConsentServiceBase,
|
|
consentServiceFactory
|
|
});
|
|
//# sourceMappingURL=index.js.map
|