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>
204 lines
4.6 KiB
TypeScript
204 lines
4.6 KiB
TypeScript
/**
|
|
* ConsentStorage - Lokale Speicherung des Consent-Status
|
|
*
|
|
* Speichert Consent-Daten im localStorage mit HMAC-Signatur
|
|
* zur Manipulationserkennung.
|
|
*/
|
|
|
|
import type { ConsentConfig, ConsentState } from '../types';
|
|
|
|
const STORAGE_KEY = 'bp_consent';
|
|
const STORAGE_VERSION = '1';
|
|
|
|
/**
|
|
* Gespeichertes Format
|
|
*/
|
|
interface StoredConsent {
|
|
version: string;
|
|
consent: ConsentState;
|
|
signature: string;
|
|
}
|
|
|
|
/**
|
|
* ConsentStorage - Persistente Speicherung
|
|
*/
|
|
export class ConsentStorage {
|
|
private config: ConsentConfig;
|
|
private storageKey: string;
|
|
|
|
constructor(config: ConsentConfig) {
|
|
this.config = config;
|
|
// Pro Site ein separater Key
|
|
this.storageKey = `${STORAGE_KEY}_${config.siteId}`;
|
|
}
|
|
|
|
/**
|
|
* Consent laden
|
|
*/
|
|
get(): ConsentState | null {
|
|
if (typeof window === 'undefined') {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const raw = localStorage.getItem(this.storageKey);
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
|
|
const stored: StoredConsent = JSON.parse(raw);
|
|
|
|
// Version pruefen
|
|
if (stored.version !== STORAGE_VERSION) {
|
|
this.log('Storage version mismatch, clearing');
|
|
this.clear();
|
|
return null;
|
|
}
|
|
|
|
// Signatur pruefen
|
|
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: ConsentState): void {
|
|
if (typeof window === 'undefined') {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const signature = this.generateSignature(consent);
|
|
|
|
const stored: StoredConsent = {
|
|
version: STORAGE_VERSION,
|
|
consent,
|
|
signature,
|
|
};
|
|
|
|
localStorage.setItem(this.storageKey, JSON.stringify(stored));
|
|
|
|
// Auch als Cookie setzen (fuer Server-Side Rendering)
|
|
this.setCookie(consent);
|
|
|
|
this.log('Consent saved to storage');
|
|
} catch (error) {
|
|
this.log('Failed to save consent:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Consent loeschen
|
|
*/
|
|
clear(): void {
|
|
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(): boolean {
|
|
return this.get() !== null;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Cookie Management
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* Consent als Cookie setzen
|
|
*/
|
|
private setCookie(consent: ConsentState): void {
|
|
const days = this.config.consent?.rememberDays ?? 365;
|
|
const expires = new Date();
|
|
expires.setDate(expires.getDate() + days);
|
|
|
|
// Nur Kategorien als Cookie (fuer SSR)
|
|
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
|
|
*/
|
|
private clearCookie(): void {
|
|
document.cookie = `${this.storageKey}=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;`;
|
|
}
|
|
|
|
// ===========================================================================
|
|
// Signature (Simple HMAC-like)
|
|
// ===========================================================================
|
|
|
|
/**
|
|
* Signatur generieren
|
|
*/
|
|
private generateSignature(consent: ConsentState): string {
|
|
const data = JSON.stringify(consent);
|
|
const key = this.config.siteId;
|
|
|
|
// Einfache Hash-Funktion (fuer Client-Side)
|
|
// In Produktion wuerde man SubtleCrypto verwenden
|
|
return this.simpleHash(data + key);
|
|
}
|
|
|
|
/**
|
|
* Signatur verifizieren
|
|
*/
|
|
private verifySignature(consent: ConsentState, signature: string): boolean {
|
|
const expected = this.generateSignature(consent);
|
|
return expected === signature;
|
|
}
|
|
|
|
/**
|
|
* Einfache Hash-Funktion (djb2)
|
|
*/
|
|
private simpleHash(str: string): string {
|
|
let hash = 5381;
|
|
for (let i = 0; i < str.length; i++) {
|
|
hash = (hash * 33) ^ str.charCodeAt(i);
|
|
}
|
|
return (hash >>> 0).toString(16);
|
|
}
|
|
|
|
/**
|
|
* Debug-Logging
|
|
*/
|
|
private log(...args: unknown[]): void {
|
|
if (this.config.debug) {
|
|
console.log('[ConsentStorage]', ...args);
|
|
}
|
|
}
|
|
}
|
|
|
|
export default ConsentStorage;
|