Initial commit: breakpilot-compliance - Compliance SDK Platform
Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
525
consent-sdk/src/core/ConsentManager.ts
Normal file
525
consent-sdk/src/core/ConsentManager.ts
Normal file
@@ -0,0 +1,525 @@
|
||||
/**
|
||||
* 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';
|
||||
|
||||
/**
|
||||
* Default-Konfiguration
|
||||
*/
|
||||
const DEFAULT_CONFIG: Partial<ConsentConfig> = {
|
||||
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,
|
||||
};
|
||||
|
||||
/**
|
||||
* Default Consent-State (nur Essential aktiv)
|
||||
*/
|
||||
const DEFAULT_CONSENT: ConsentCategories = {
|
||||
essential: true,
|
||||
functional: false,
|
||||
analytics: false,
|
||||
marketing: false,
|
||||
social: false,
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private mergeConfig(config: ConsentConfig): ConsentConfig {
|
||||
return {
|
||||
...DEFAULT_CONFIG,
|
||||
...config,
|
||||
ui: { ...DEFAULT_CONFIG.ui, ...config.ui },
|
||||
consent: { ...DEFAULT_CONFIG.consent, ...config.consent },
|
||||
} as ConsentConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
private updateGoogleConsentMode(): void {
|
||||
if (typeof window === 'undefined' || !this.currentConsent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gtag = (window as unknown as { gtag?: (...args: unknown[]) => void }).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
|
||||
*/
|
||||
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;
|
||||
Reference in New Issue
Block a user