Phase 4: extract config defaults, Google Consent Mode helper, and framework adapter internals into sibling files so every source file is under the hard cap. Public API surface preserved; all 135 tests green, tsup build + tsc typecheck clean. - core/ConsentManager 525 -> 467 LOC (extract config + google helpers) - react/index 511 LOC -> 199 LOC barrel + components/hooks/context - vue/index 511 LOC -> 32 LOC barrel + components/composables/context/plugin - angular/index 509 LOC -> 45 LOC barrel + interface/service/module/templates Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
468 lines
11 KiB
TypeScript
468 lines
11 KiB
TypeScript
/**
|
|
* 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<ConsentEventData>;
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<void> {
|
|
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<string> {
|
|
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<T extends ConsentEventType>(
|
|
event: T,
|
|
callback: ConsentEventCallback<ConsentEventData[T]>
|
|
): () => void {
|
|
return this.events.on(event, callback);
|
|
}
|
|
|
|
/**
|
|
* Event-Listener entfernen
|
|
*/
|
|
off<T extends ConsentEventType>(
|
|
event: T,
|
|
callback: ConsentEventCallback<ConsentEventData[T]>
|
|
): 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<ConsentCategories>) };
|
|
}
|
|
|
|
/**
|
|
* 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<T extends ConsentEventType>(
|
|
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;
|