/** * ConsentManager - Hauptklasse fuer das Consent Management * * DSGVO/TTDSG-konformes Consent Management fuer Web, PWA und Mobile. */ import type { ConsentConfig, ConsentState, ConsentCategory, ConsentCategories, ConsentInput, ConsentEventType, ConsentEventCallback, ConsentEventData, } from '../types'; import { ConsentStorage } from './ConsentStorage'; import { ScriptBlocker } from './ScriptBlocker'; import { ConsentAPI } from './ConsentAPI'; import { EventEmitter } from '../utils/EventEmitter'; import { generateFingerprint } from '../utils/fingerprint'; import { SDK_VERSION } from '../version'; import { DEFAULT_CONSENT, mergeConsentConfig, } from './consent-manager-config'; import { updateGoogleConsentMode as applyGoogleConsent } from './consent-manager-google'; /** * ConsentManager - Zentrale Klasse fuer Consent-Verwaltung */ export class ConsentManager { private config: ConsentConfig; private storage: ConsentStorage; private scriptBlocker: ScriptBlocker; private api: ConsentAPI; private events: EventEmitter; private currentConsent: ConsentState | null = null; private initialized = false; private bannerVisible = false; private deviceFingerprint: string = ''; constructor(config: ConsentConfig) { 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(): Promise { if (this.initialized) { this.log('Already initialized, skipping'); return; } try { this.log('Initializing ConsentManager...'); // Device Fingerprint generieren this.deviceFingerprint = await generateFingerprint(); // Consent aus Storage laden this.currentConsent = this.storage.get(); if (this.currentConsent) { this.log('Loaded consent from storage:', this.currentConsent); // Pruefen ob Consent abgelaufen if (this.isConsentExpired()) { this.log('Consent expired, clearing'); this.storage.clear(); this.currentConsent = null; } else { // Consent anwenden this.applyConsent(); } } // Script-Blocker initialisieren this.scriptBlocker.init(); this.initialized = true; this.emit('init', this.currentConsent); // Banner anzeigen falls noetig if (this.needsConsent()) { this.showBanner(); } this.log('ConsentManager initialized successfully'); } catch (error) { this.handleError(error as Error); throw error; } } // =========================================================================== // Public API // =========================================================================== /** * Pruefen ob Consent fuer Kategorie vorhanden */ hasConsent(category: ConsentCategory): boolean { if (!this.currentConsent) { return category === 'essential'; } return this.currentConsent.categories[category] ?? false; } /** * Pruefen ob Consent fuer Vendor vorhanden */ hasVendorConsent(vendorId: string): boolean { if (!this.currentConsent) { return false; } return this.currentConsent.vendors[vendorId] ?? false; } /** * Aktuellen Consent-State abrufen */ getConsent(): ConsentState | null { return this.currentConsent ? { ...this.currentConsent } : null; } /** * Consent setzen */ async setConsent(input: ConsentInput): Promise { const categories = this.normalizeConsentInput(input); // Essential ist immer aktiv categories.essential = true; const newConsent: ConsentState = { categories, vendors: 'vendors' in input && input.vendors ? input.vendors : {}, timestamp: new Date().toISOString(), version: SDK_VERSION, }; try { // An Backend senden const response = await this.api.saveConsent({ siteId: this.config.siteId, deviceFingerprint: this.deviceFingerprint, consent: newConsent, }); newConsent.consentId = response.consentId; newConsent.expiresAt = response.expiresAt; // Lokal speichern this.storage.set(newConsent); this.currentConsent = newConsent; // Consent anwenden this.applyConsent(); // Event emittieren this.emit('change', newConsent); this.config.onConsentChange?.(newConsent); this.log('Consent saved:', newConsent); } catch (error) { // Bei Netzwerkfehler trotzdem lokal speichern 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(): Promise { const allCategories: ConsentCategories = { 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(): Promise { const minimalCategories: ConsentCategories = { 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(): Promise { 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(): Promise { const exportData = { currentConsent: this.currentConsent, exportedAt: new Date().toISOString(), siteId: this.config.siteId, deviceFingerprint: this.deviceFingerprint, }; return JSON.stringify(exportData, null, 2); } // =========================================================================== // Banner Control // =========================================================================== /** * Pruefen ob Consent-Abfrage noetig */ needsConsent(): boolean { if (!this.currentConsent) { return true; } if (this.isConsentExpired()) { return true; } // Recheck nach X Tagen 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 (new Date() > recheckDate) { return true; } } return false; } /** * Banner anzeigen */ showBanner(): void { if (this.bannerVisible) { return; } this.bannerVisible = true; this.emit('banner_show', undefined); this.config.onBannerShow?.(); // Banner wird von UI-Komponente gerendert // Hier nur Status setzen this.log('Banner shown'); } /** * Banner verstecken */ hideBanner(): void { if (!this.bannerVisible) { return; } this.bannerVisible = false; this.emit('banner_hide', undefined); this.config.onBannerHide?.(); this.log('Banner hidden'); } /** * Einstellungs-Modal oeffnen */ showSettings(): void { this.emit('settings_open', undefined); this.log('Settings opened'); } /** * Pruefen ob Banner sichtbar */ isBannerVisible(): boolean { return this.bannerVisible; } // =========================================================================== // Event Handling // =========================================================================== /** * Event-Listener registrieren */ on( event: T, callback: ConsentEventCallback ): () => void { return this.events.on(event, callback); } /** * Event-Listener entfernen */ off( event: T, callback: ConsentEventCallback ): void { this.events.off(event, callback); } // =========================================================================== // Internal Methods // =========================================================================== /** * Konfiguration zusammenfuehren — delegates to the extracted helper. */ private mergeConfig(config: ConsentConfig): ConsentConfig { return mergeConsentConfig(config); } /** * Consent-Input normalisieren */ private normalizeConsentInput(input: ConsentInput): ConsentCategories { if ('categories' in input && input.categories) { return { ...DEFAULT_CONSENT, ...input.categories }; } return { ...DEFAULT_CONSENT, ...(input as Partial) }; } /** * Consent anwenden (Skripte aktivieren/blockieren) */ private applyConsent(): void { if (!this.currentConsent) { return; } for (const [category, allowed] of Object.entries( this.currentConsent.categories )) { if (allowed) { this.scriptBlocker.enableCategory(category as ConsentCategory); } else { this.scriptBlocker.disableCategory(category as ConsentCategory); } } // Google Consent Mode aktualisieren this.updateGoogleConsentMode(); } /** * Google Consent Mode v2 aktualisieren — delegates to the extracted helper. */ private updateGoogleConsentMode(): void { if (applyGoogleConsent(this.currentConsent)) { this.log('Google Consent Mode updated'); } } /** * Pruefen ob Consent abgelaufen */ private isConsentExpired(): boolean { if (!this.currentConsent?.expiresAt) { // Fallback: Nach rememberDays ablaufen 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 new Date() > expiryDate; } return false; } return new Date() > new Date(this.currentConsent.expiresAt); } /** * Event emittieren */ private emit( event: T, data: ConsentEventData[T] ): void { this.events.emit(event, data); } /** * Fehler behandeln */ private handleError(error: Error): void { this.log('Error:', error); this.emit('error', error); this.config.onError?.(error); } /** * Debug-Logging */ private log(...args: unknown[]): void { if (this.config.debug) { console.log('[ConsentSDK]', ...args); } } // =========================================================================== // Static Methods // =========================================================================== /** * SDK-Version abrufen */ static getVersion(): string { return SDK_VERSION; } } // Default-Export export default ConsentManager;