refactor(consent-sdk,dsms-gateway): split ConsentManager, types, and main.py
- consent-sdk/src/types/index.ts: extracted 438 LOC into core.ts, config.ts, vendor.ts, api.ts, events.ts, storage.ts, translations.ts; index.ts is now a 21-LOC barrel re-exporter - consent-sdk/src/core/ConsentManager.ts: extracted normalizeConsentInput, isConsentExpired, needsConsent, ALL_CATEGORIES, MINIMAL_CATEGORIES into consent-manager-helpers.ts; reduced from 467 to 345 LOC - dsms-gateway/main.py: extracted models → models.py, config → config.py, IPFS helpers + verify_token → dependencies.py, route handlers → routers/documents.py and routers/node.py; main.py is now a 41-LOC app factory; test mock paths updated accordingly (27/27 tests pass) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,14 +1,9 @@
|
|||||||
/**
|
/** ConsentManager - DSGVO/TTDSG-konformes Consent Management fuer Web, PWA und Mobile. */
|
||||||
* ConsentManager - Hauptklasse fuer das Consent Management
|
|
||||||
*
|
|
||||||
* DSGVO/TTDSG-konformes Consent Management fuer Web, PWA und Mobile.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ConsentConfig,
|
ConsentConfig,
|
||||||
ConsentState,
|
ConsentState,
|
||||||
ConsentCategory,
|
ConsentCategory,
|
||||||
ConsentCategories,
|
|
||||||
ConsentInput,
|
ConsentInput,
|
||||||
ConsentEventType,
|
ConsentEventType,
|
||||||
ConsentEventCallback,
|
ConsentEventCallback,
|
||||||
@@ -20,15 +15,16 @@ import { ConsentAPI } from './ConsentAPI';
|
|||||||
import { EventEmitter } from '../utils/EventEmitter';
|
import { EventEmitter } from '../utils/EventEmitter';
|
||||||
import { generateFingerprint } from '../utils/fingerprint';
|
import { generateFingerprint } from '../utils/fingerprint';
|
||||||
import { SDK_VERSION } from '../version';
|
import { SDK_VERSION } from '../version';
|
||||||
import {
|
import { mergeConsentConfig } from './consent-manager-config';
|
||||||
DEFAULT_CONSENT,
|
|
||||||
mergeConsentConfig,
|
|
||||||
} from './consent-manager-config';
|
|
||||||
import { updateGoogleConsentMode as applyGoogleConsent } from './consent-manager-google';
|
import { updateGoogleConsentMode as applyGoogleConsent } from './consent-manager-google';
|
||||||
|
import {
|
||||||
|
normalizeConsentInput,
|
||||||
|
isConsentExpired,
|
||||||
|
needsConsent,
|
||||||
|
ALL_CATEGORIES,
|
||||||
|
MINIMAL_CATEGORIES,
|
||||||
|
} from './consent-manager-helpers';
|
||||||
|
|
||||||
/**
|
|
||||||
* ConsentManager - Zentrale Klasse fuer Consent-Verwaltung
|
|
||||||
*/
|
|
||||||
export class ConsentManager {
|
export class ConsentManager {
|
||||||
private config: ConsentConfig;
|
private config: ConsentConfig;
|
||||||
private storage: ConsentStorage;
|
private storage: ConsentStorage;
|
||||||
@@ -41,7 +37,7 @@ export class ConsentManager {
|
|||||||
private deviceFingerprint: string = '';
|
private deviceFingerprint: string = '';
|
||||||
|
|
||||||
constructor(config: ConsentConfig) {
|
constructor(config: ConsentConfig) {
|
||||||
this.config = this.mergeConfig(config);
|
this.config = mergeConsentConfig(config);
|
||||||
this.storage = new ConsentStorage(this.config);
|
this.storage = new ConsentStorage(this.config);
|
||||||
this.scriptBlocker = new ScriptBlocker(this.config);
|
this.scriptBlocker = new ScriptBlocker(this.config);
|
||||||
this.api = new ConsentAPI(this.config);
|
this.api = new ConsentAPI(this.config);
|
||||||
@@ -72,7 +68,7 @@ export class ConsentManager {
|
|||||||
this.log('Loaded consent from storage:', this.currentConsent);
|
this.log('Loaded consent from storage:', this.currentConsent);
|
||||||
|
|
||||||
// Pruefen ob Consent abgelaufen
|
// Pruefen ob Consent abgelaufen
|
||||||
if (this.isConsentExpired()) {
|
if (isConsentExpired(this.currentConsent, this.config)) {
|
||||||
this.log('Consent expired, clearing');
|
this.log('Consent expired, clearing');
|
||||||
this.storage.clear();
|
this.storage.clear();
|
||||||
this.currentConsent = null;
|
this.currentConsent = null;
|
||||||
@@ -89,7 +85,7 @@ export class ConsentManager {
|
|||||||
this.emit('init', this.currentConsent);
|
this.emit('init', this.currentConsent);
|
||||||
|
|
||||||
// Banner anzeigen falls noetig
|
// Banner anzeigen falls noetig
|
||||||
if (this.needsConsent()) {
|
if (needsConsent(this.currentConsent, this.config)) {
|
||||||
this.showBanner();
|
this.showBanner();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,9 +96,7 @@ export class ConsentManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// --- Public API ---
|
||||||
// Public API
|
|
||||||
// ===========================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pruefen ob Consent fuer Kategorie vorhanden
|
* Pruefen ob Consent fuer Kategorie vorhanden
|
||||||
@@ -135,7 +129,7 @@ export class ConsentManager {
|
|||||||
* Consent setzen
|
* Consent setzen
|
||||||
*/
|
*/
|
||||||
async setConsent(input: ConsentInput): Promise<void> {
|
async setConsent(input: ConsentInput): Promise<void> {
|
||||||
const categories = this.normalizeConsentInput(input);
|
const categories = normalizeConsentInput(input);
|
||||||
|
|
||||||
// Essential ist immer aktiv
|
// Essential ist immer aktiv
|
||||||
categories.essential = true;
|
categories.essential = true;
|
||||||
@@ -184,15 +178,7 @@ export class ConsentManager {
|
|||||||
* Alle Kategorien akzeptieren
|
* Alle Kategorien akzeptieren
|
||||||
*/
|
*/
|
||||||
async acceptAll(): Promise<void> {
|
async acceptAll(): Promise<void> {
|
||||||
const allCategories: ConsentCategories = {
|
await this.setConsent(ALL_CATEGORIES);
|
||||||
essential: true,
|
|
||||||
functional: true,
|
|
||||||
analytics: true,
|
|
||||||
marketing: true,
|
|
||||||
social: true,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.setConsent(allCategories);
|
|
||||||
this.emit('accept_all', this.currentConsent!);
|
this.emit('accept_all', this.currentConsent!);
|
||||||
this.hideBanner();
|
this.hideBanner();
|
||||||
}
|
}
|
||||||
@@ -201,15 +187,7 @@ export class ConsentManager {
|
|||||||
* Alle nicht-essentiellen Kategorien ablehnen
|
* Alle nicht-essentiellen Kategorien ablehnen
|
||||||
*/
|
*/
|
||||||
async rejectAll(): Promise<void> {
|
async rejectAll(): Promise<void> {
|
||||||
const minimalCategories: ConsentCategories = {
|
await this.setConsent(MINIMAL_CATEGORIES);
|
||||||
essential: true,
|
|
||||||
functional: false,
|
|
||||||
analytics: false,
|
|
||||||
marketing: false,
|
|
||||||
social: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
await this.setConsent(minimalCategories);
|
|
||||||
this.emit('reject_all', this.currentConsent!);
|
this.emit('reject_all', this.currentConsent!);
|
||||||
this.hideBanner();
|
this.hideBanner();
|
||||||
}
|
}
|
||||||
@@ -247,52 +225,23 @@ export class ConsentManager {
|
|||||||
return JSON.stringify(exportData, null, 2);
|
return JSON.stringify(exportData, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// --- Banner Control ---
|
||||||
// Banner Control
|
|
||||||
// ===========================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pruefen ob Consent-Abfrage noetig
|
* Pruefen ob Consent-Abfrage noetig
|
||||||
*/
|
*/
|
||||||
needsConsent(): boolean {
|
needsConsent(): boolean {
|
||||||
if (!this.currentConsent) {
|
return needsConsent(this.currentConsent, this.config);
|
||||||
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
|
* Banner anzeigen
|
||||||
*/
|
*/
|
||||||
showBanner(): void {
|
showBanner(): void {
|
||||||
if (this.bannerVisible) {
|
if (this.bannerVisible) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bannerVisible = true;
|
this.bannerVisible = true;
|
||||||
this.emit('banner_show', undefined);
|
this.emit('banner_show', undefined);
|
||||||
this.config.onBannerShow?.();
|
this.config.onBannerShow?.();
|
||||||
|
|
||||||
// Banner wird von UI-Komponente gerendert
|
|
||||||
// Hier nur Status setzen
|
|
||||||
this.log('Banner shown');
|
this.log('Banner shown');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,14 +249,10 @@ export class ConsentManager {
|
|||||||
* Banner verstecken
|
* Banner verstecken
|
||||||
*/
|
*/
|
||||||
hideBanner(): void {
|
hideBanner(): void {
|
||||||
if (!this.bannerVisible) {
|
if (!this.bannerVisible) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.bannerVisible = false;
|
this.bannerVisible = false;
|
||||||
this.emit('banner_hide', undefined);
|
this.emit('banner_hide', undefined);
|
||||||
this.config.onBannerHide?.();
|
this.config.onBannerHide?.();
|
||||||
|
|
||||||
this.log('Banner hidden');
|
this.log('Banner hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,9 +271,7 @@ export class ConsentManager {
|
|||||||
return this.bannerVisible;
|
return this.bannerVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// --- Event Handling ---
|
||||||
// Event Handling
|
|
||||||
// ===========================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Event-Listener registrieren
|
* Event-Listener registrieren
|
||||||
@@ -350,35 +293,13 @@ export class ConsentManager {
|
|||||||
this.events.off(event, callback);
|
this.events.off(event, callback);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// --- Internal Methods ---
|
||||||
// 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)
|
* Consent anwenden (Skripte aktivieren/blockieren)
|
||||||
*/
|
*/
|
||||||
private applyConsent(): void {
|
private applyConsent(): void {
|
||||||
if (!this.currentConsent) {
|
if (!this.currentConsent) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [category, allowed] of Object.entries(
|
for (const [category, allowed] of Object.entries(
|
||||||
this.currentConsent.categories
|
this.currentConsent.categories
|
||||||
@@ -391,69 +312,26 @@ export class ConsentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Google Consent Mode aktualisieren
|
// Google Consent Mode aktualisieren
|
||||||
this.updateGoogleConsentMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Google Consent Mode v2 aktualisieren — delegates to the extracted helper.
|
|
||||||
*/
|
|
||||||
private updateGoogleConsentMode(): void {
|
|
||||||
if (applyGoogleConsent(this.currentConsent)) {
|
if (applyGoogleConsent(this.currentConsent)) {
|
||||||
this.log('Google Consent Mode updated');
|
this.log('Google Consent Mode updated');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private emit<T extends ConsentEventType>(event: T, data: ConsentEventData[T]): void {
|
||||||
* 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);
|
this.events.emit(event, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fehler behandeln
|
|
||||||
*/
|
|
||||||
private handleError(error: Error): void {
|
private handleError(error: Error): void {
|
||||||
this.log('Error:', error);
|
this.log('Error:', error);
|
||||||
this.emit('error', error);
|
this.emit('error', error);
|
||||||
this.config.onError?.(error);
|
this.config.onError?.(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Debug-Logging
|
|
||||||
*/
|
|
||||||
private log(...args: unknown[]): void {
|
private log(...args: unknown[]): void {
|
||||||
if (this.config.debug) {
|
if (this.config.debug) console.log('[ConsentSDK]', ...args);
|
||||||
console.log('[ConsentSDK]', ...args);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===========================================================================
|
// --- Static Methods ---
|
||||||
// Static Methods
|
|
||||||
// ===========================================================================
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SDK-Version abrufen
|
* SDK-Version abrufen
|
||||||
|
|||||||
88
consent-sdk/src/core/consent-manager-helpers.ts
Normal file
88
consent-sdk/src/core/consent-manager-helpers.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* consent-manager-helpers.ts
|
||||||
|
*
|
||||||
|
* Pure helper functions used by ConsentManager that have no dependency on
|
||||||
|
* class instance state. Extracted to keep ConsentManager.ts under the
|
||||||
|
* 350 LOC soft target.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ConsentState,
|
||||||
|
ConsentCategories,
|
||||||
|
ConsentInput,
|
||||||
|
ConsentConfig,
|
||||||
|
} from '../types';
|
||||||
|
import { DEFAULT_CONSENT } from './consent-manager-config';
|
||||||
|
|
||||||
|
/** All categories accepted (used by acceptAll). */
|
||||||
|
export const ALL_CATEGORIES: ConsentCategories = {
|
||||||
|
essential: true,
|
||||||
|
functional: true,
|
||||||
|
analytics: true,
|
||||||
|
marketing: true,
|
||||||
|
social: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Only essential consent (used by rejectAll). */
|
||||||
|
export const MINIMAL_CATEGORIES: ConsentCategories = {
|
||||||
|
essential: true,
|
||||||
|
functional: false,
|
||||||
|
analytics: false,
|
||||||
|
marketing: false,
|
||||||
|
social: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalise a ConsentInput into a full ConsentCategories map.
|
||||||
|
* Always returns a shallow copy — never mutates the input.
|
||||||
|
*/
|
||||||
|
export function normalizeConsentInput(input: ConsentInput): ConsentCategories {
|
||||||
|
if ('categories' in input && input.categories) {
|
||||||
|
return { ...DEFAULT_CONSENT, ...input.categories };
|
||||||
|
}
|
||||||
|
return { ...DEFAULT_CONSENT, ...(input as Partial<ConsentCategories>) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true when the stored consent record has passed its expiry date.
|
||||||
|
* Falls back to `rememberDays` from config when `expiresAt` is absent.
|
||||||
|
*/
|
||||||
|
export function isConsentExpired(
|
||||||
|
consent: ConsentState | null,
|
||||||
|
config: ConsentConfig
|
||||||
|
): boolean {
|
||||||
|
if (!consent) return false;
|
||||||
|
|
||||||
|
if (!consent.expiresAt) {
|
||||||
|
if (consent.timestamp && config.consent?.rememberDays) {
|
||||||
|
const consentDate = new Date(consent.timestamp);
|
||||||
|
const expiryDate = new Date(consentDate);
|
||||||
|
expiryDate.setDate(expiryDate.getDate() + config.consent.rememberDays);
|
||||||
|
return new Date() > expiryDate;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date() > new Date(consent.expiresAt);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true when the user needs to be shown the consent banner.
|
||||||
|
*/
|
||||||
|
export function needsConsent(
|
||||||
|
consent: ConsentState | null,
|
||||||
|
config: ConsentConfig
|
||||||
|
): boolean {
|
||||||
|
if (!consent) return true;
|
||||||
|
|
||||||
|
if (isConsentExpired(consent, config)) return true;
|
||||||
|
|
||||||
|
if (config.consent?.recheckAfterDays) {
|
||||||
|
const consentDate = new Date(consent.timestamp);
|
||||||
|
const recheckDate = new Date(consentDate);
|
||||||
|
recheckDate.setDate(recheckDate.getDate() + config.consent.recheckAfterDays);
|
||||||
|
if (new Date() > recheckDate) return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
56
consent-sdk/src/types/api.ts
Normal file
56
consent-sdk/src/types/api.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* Consent SDK Types — API request/response shapes
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ConsentCategory, ConsentState } from './core';
|
||||||
|
import type { ConsentUIConfig, TCFConfig } from './config';
|
||||||
|
import type { ConsentVendor } from './vendor';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// API Types
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API-Antwort fuer Consent-Erstellung
|
||||||
|
*/
|
||||||
|
export interface ConsentAPIResponse {
|
||||||
|
consentId: string;
|
||||||
|
timestamp: string;
|
||||||
|
expiresAt: string;
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* API-Antwort fuer Site-Konfiguration
|
||||||
|
*/
|
||||||
|
export interface SiteConfigResponse {
|
||||||
|
siteId: string;
|
||||||
|
siteName: string;
|
||||||
|
categories: CategoryConfig[];
|
||||||
|
ui: ConsentUIConfig;
|
||||||
|
legal: LegalConfig;
|
||||||
|
tcf?: TCFConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kategorie-Konfiguration vom Server
|
||||||
|
*/
|
||||||
|
export interface CategoryConfig {
|
||||||
|
id: ConsentCategory;
|
||||||
|
name: Record<string, string>;
|
||||||
|
description: Record<string, string>;
|
||||||
|
required: boolean;
|
||||||
|
vendors: ConsentVendor[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rechtliche Konfiguration
|
||||||
|
*/
|
||||||
|
export interface LegalConfig {
|
||||||
|
privacyPolicyUrl: string;
|
||||||
|
imprintUrl: string;
|
||||||
|
dpo?: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
166
consent-sdk/src/types/config.ts
Normal file
166
consent-sdk/src/types/config.ts
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
/**
|
||||||
|
* Consent SDK Types — Configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ConsentCategory, ConsentState } from './core';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Configuration
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI-Position des Banners
|
||||||
|
*/
|
||||||
|
export type BannerPosition = 'bottom' | 'top' | 'center';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Banner-Layout
|
||||||
|
*/
|
||||||
|
export type BannerLayout = 'bar' | 'modal' | 'floating';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Farbschema
|
||||||
|
*/
|
||||||
|
export type BannerTheme = 'light' | 'dark' | 'auto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UI-Konfiguration
|
||||||
|
*/
|
||||||
|
export interface ConsentUIConfig {
|
||||||
|
/** Position des Banners */
|
||||||
|
position?: BannerPosition;
|
||||||
|
|
||||||
|
/** Layout-Typ */
|
||||||
|
layout?: BannerLayout;
|
||||||
|
|
||||||
|
/** Farbschema */
|
||||||
|
theme?: BannerTheme;
|
||||||
|
|
||||||
|
/** Pfad zu Custom CSS */
|
||||||
|
customCss?: string;
|
||||||
|
|
||||||
|
/** z-index fuer Banner */
|
||||||
|
zIndex?: number;
|
||||||
|
|
||||||
|
/** Scroll blockieren bei Modal */
|
||||||
|
blockScrollOnModal?: boolean;
|
||||||
|
|
||||||
|
/** Custom Container-ID */
|
||||||
|
containerId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consent-Verhaltens-Konfiguration
|
||||||
|
*/
|
||||||
|
export interface ConsentBehaviorConfig {
|
||||||
|
/** Muss Nutzer interagieren? */
|
||||||
|
required?: boolean;
|
||||||
|
|
||||||
|
/** "Alle ablehnen" Button sichtbar */
|
||||||
|
rejectAllVisible?: boolean;
|
||||||
|
|
||||||
|
/** "Alle akzeptieren" Button sichtbar */
|
||||||
|
acceptAllVisible?: boolean;
|
||||||
|
|
||||||
|
/** Einzelne Kategorien waehlbar */
|
||||||
|
granularControl?: boolean;
|
||||||
|
|
||||||
|
/** Einzelne Vendors waehlbar */
|
||||||
|
vendorControl?: boolean;
|
||||||
|
|
||||||
|
/** Auswahl speichern */
|
||||||
|
rememberChoice?: boolean;
|
||||||
|
|
||||||
|
/** Speicherdauer in Tagen */
|
||||||
|
rememberDays?: number;
|
||||||
|
|
||||||
|
/** Nur in EU anzeigen (Geo-Targeting) */
|
||||||
|
geoTargeting?: boolean;
|
||||||
|
|
||||||
|
/** Erneut nachfragen nach X Tagen */
|
||||||
|
recheckAfterDays?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TCF 2.2 Konfiguration
|
||||||
|
*/
|
||||||
|
export interface TCFConfig {
|
||||||
|
/** TCF aktivieren */
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
/** CMP ID */
|
||||||
|
cmpId?: number;
|
||||||
|
|
||||||
|
/** CMP Version */
|
||||||
|
cmpVersion?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PWA-spezifische Konfiguration
|
||||||
|
*/
|
||||||
|
export interface PWAConfig {
|
||||||
|
/** Offline-Unterstuetzung aktivieren */
|
||||||
|
offlineSupport?: boolean;
|
||||||
|
|
||||||
|
/** Bei Reconnect synchronisieren */
|
||||||
|
syncOnReconnect?: boolean;
|
||||||
|
|
||||||
|
/** Cache-Strategie */
|
||||||
|
cacheStrategy?: 'stale-while-revalidate' | 'network-first' | 'cache-first';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Haupt-Konfiguration fuer ConsentManager
|
||||||
|
*/
|
||||||
|
export interface ConsentConfig {
|
||||||
|
// Pflichtfelder
|
||||||
|
/** API-Endpunkt fuer Consent-Backend */
|
||||||
|
apiEndpoint: string;
|
||||||
|
|
||||||
|
/** Site-ID */
|
||||||
|
siteId: string;
|
||||||
|
|
||||||
|
// Sprache
|
||||||
|
/** Sprache (ISO 639-1) */
|
||||||
|
language?: string;
|
||||||
|
|
||||||
|
/** Fallback-Sprache */
|
||||||
|
fallbackLanguage?: string;
|
||||||
|
|
||||||
|
// UI
|
||||||
|
/** UI-Konfiguration */
|
||||||
|
ui?: ConsentUIConfig;
|
||||||
|
|
||||||
|
// Verhalten
|
||||||
|
/** Consent-Verhaltens-Konfiguration */
|
||||||
|
consent?: ConsentBehaviorConfig;
|
||||||
|
|
||||||
|
// Kategorien
|
||||||
|
/** Aktive Kategorien */
|
||||||
|
categories?: ConsentCategory[];
|
||||||
|
|
||||||
|
// TCF
|
||||||
|
/** TCF 2.2 Konfiguration */
|
||||||
|
tcf?: TCFConfig;
|
||||||
|
|
||||||
|
// PWA
|
||||||
|
/** PWA-Konfiguration */
|
||||||
|
pwa?: PWAConfig;
|
||||||
|
|
||||||
|
// Callbacks
|
||||||
|
/** Callback bei Consent-Aenderung */
|
||||||
|
onConsentChange?: (consent: ConsentState) => void;
|
||||||
|
|
||||||
|
/** Callback wenn Banner angezeigt wird */
|
||||||
|
onBannerShow?: () => void;
|
||||||
|
|
||||||
|
/** Callback wenn Banner geschlossen wird */
|
||||||
|
onBannerHide?: () => void;
|
||||||
|
|
||||||
|
/** Callback bei Fehler */
|
||||||
|
onError?: (error: Error) => void;
|
||||||
|
|
||||||
|
// Debug
|
||||||
|
/** Debug-Modus aktivieren */
|
||||||
|
debug?: boolean;
|
||||||
|
}
|
||||||
65
consent-sdk/src/types/core.ts
Normal file
65
consent-sdk/src/types/core.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Consent SDK Types — Core: categories, state, input
|
||||||
|
*/
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Consent Categories
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Standard-Consent-Kategorien nach IAB TCF 2.2
|
||||||
|
*/
|
||||||
|
export type ConsentCategory =
|
||||||
|
| 'essential' // Technisch notwendig (TTDSG § 25 Abs. 2)
|
||||||
|
| 'functional' // Personalisierung, Komfortfunktionen
|
||||||
|
| 'analytics' // Anonyme Nutzungsanalyse
|
||||||
|
| 'marketing' // Werbung, Retargeting
|
||||||
|
| 'social'; // Social Media Plugins
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consent-Status pro Kategorie
|
||||||
|
*/
|
||||||
|
export type ConsentCategories = Record<ConsentCategory, boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Consent-Status pro Vendor
|
||||||
|
*/
|
||||||
|
export type ConsentVendors = Record<string, boolean>;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Consent State
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Aktueller Consent-Zustand
|
||||||
|
*/
|
||||||
|
export interface ConsentState {
|
||||||
|
/** Consent pro Kategorie */
|
||||||
|
categories: ConsentCategories;
|
||||||
|
|
||||||
|
/** Consent pro Vendor (optional, für granulare Kontrolle) */
|
||||||
|
vendors: ConsentVendors;
|
||||||
|
|
||||||
|
/** Zeitstempel der letzten Aenderung */
|
||||||
|
timestamp: string;
|
||||||
|
|
||||||
|
/** SDK-Version bei Erstellung */
|
||||||
|
version: string;
|
||||||
|
|
||||||
|
/** Eindeutige Consent-ID vom Backend */
|
||||||
|
consentId?: string;
|
||||||
|
|
||||||
|
/** Ablaufdatum */
|
||||||
|
expiresAt?: string;
|
||||||
|
|
||||||
|
/** IAB TCF String (falls aktiviert) */
|
||||||
|
tcfString?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Minimaler Consent-Input fuer setConsent()
|
||||||
|
*/
|
||||||
|
export type ConsentInput = Partial<ConsentCategories> | {
|
||||||
|
categories?: Partial<ConsentCategories>;
|
||||||
|
vendors?: ConsentVendors;
|
||||||
|
};
|
||||||
49
consent-sdk/src/types/events.ts
Normal file
49
consent-sdk/src/types/events.ts
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Consent SDK Types — Event system
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ConsentState } from './core';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Events
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event-Typen
|
||||||
|
*/
|
||||||
|
export type ConsentEventType =
|
||||||
|
| 'init'
|
||||||
|
| 'change'
|
||||||
|
| 'accept_all'
|
||||||
|
| 'reject_all'
|
||||||
|
| 'save_selection'
|
||||||
|
| 'banner_show'
|
||||||
|
| 'banner_hide'
|
||||||
|
| 'settings_open'
|
||||||
|
| 'settings_close'
|
||||||
|
| 'vendor_enable'
|
||||||
|
| 'vendor_disable'
|
||||||
|
| 'error';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event-Listener Callback
|
||||||
|
*/
|
||||||
|
export type ConsentEventCallback<T = unknown> = (data: T) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Event-Daten fuer verschiedene Events
|
||||||
|
*/
|
||||||
|
export type ConsentEventData = {
|
||||||
|
init: ConsentState | null;
|
||||||
|
change: ConsentState;
|
||||||
|
accept_all: ConsentState;
|
||||||
|
reject_all: ConsentState;
|
||||||
|
save_selection: ConsentState;
|
||||||
|
banner_show: undefined;
|
||||||
|
banner_hide: undefined;
|
||||||
|
settings_open: undefined;
|
||||||
|
settings_close: undefined;
|
||||||
|
vendor_enable: string;
|
||||||
|
vendor_disable: string;
|
||||||
|
error: Error;
|
||||||
|
};
|
||||||
@@ -1,438 +1,21 @@
|
|||||||
/**
|
/**
|
||||||
* Consent SDK Types
|
* Consent SDK Types — barrel re-export
|
||||||
*
|
*
|
||||||
* DSGVO/TTDSG-konforme Typdefinitionen für das Consent Management System.
|
* Domain files:
|
||||||
|
* core.ts — ConsentCategory, ConsentCategories, ConsentVendors, ConsentState, ConsentInput
|
||||||
|
* config.ts — BannerPosition, BannerLayout, BannerTheme, ConsentUIConfig,
|
||||||
|
* ConsentBehaviorConfig, TCFConfig, PWAConfig, ConsentConfig
|
||||||
|
* vendor.ts — CookieInfo, ConsentVendor
|
||||||
|
* api.ts — ConsentAPIResponse, SiteConfigResponse, CategoryConfig, LegalConfig
|
||||||
|
* events.ts — ConsentEventType, ConsentEventCallback, ConsentEventData
|
||||||
|
* storage.ts — ConsentStorageAdapter
|
||||||
|
* translations.ts — ConsentTranslations, SupportedLanguage
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// =============================================================================
|
export * from './core';
|
||||||
// Consent Categories
|
export * from './config';
|
||||||
// =============================================================================
|
export * from './vendor';
|
||||||
|
export * from './api';
|
||||||
/**
|
export * from './events';
|
||||||
* Standard-Consent-Kategorien nach IAB TCF 2.2
|
export * from './storage';
|
||||||
*/
|
export * from './translations';
|
||||||
export type ConsentCategory =
|
|
||||||
| 'essential' // Technisch notwendig (TTDSG § 25 Abs. 2)
|
|
||||||
| 'functional' // Personalisierung, Komfortfunktionen
|
|
||||||
| 'analytics' // Anonyme Nutzungsanalyse
|
|
||||||
| 'marketing' // Werbung, Retargeting
|
|
||||||
| 'social'; // Social Media Plugins
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consent-Status pro Kategorie
|
|
||||||
*/
|
|
||||||
export type ConsentCategories = Record<ConsentCategory, boolean>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consent-Status pro Vendor
|
|
||||||
*/
|
|
||||||
export type ConsentVendors = Record<string, boolean>;
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Consent State
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Aktueller Consent-Zustand
|
|
||||||
*/
|
|
||||||
export interface ConsentState {
|
|
||||||
/** Consent pro Kategorie */
|
|
||||||
categories: ConsentCategories;
|
|
||||||
|
|
||||||
/** Consent pro Vendor (optional, für granulare Kontrolle) */
|
|
||||||
vendors: ConsentVendors;
|
|
||||||
|
|
||||||
/** Zeitstempel der letzten Aenderung */
|
|
||||||
timestamp: string;
|
|
||||||
|
|
||||||
/** SDK-Version bei Erstellung */
|
|
||||||
version: string;
|
|
||||||
|
|
||||||
/** Eindeutige Consent-ID vom Backend */
|
|
||||||
consentId?: string;
|
|
||||||
|
|
||||||
/** Ablaufdatum */
|
|
||||||
expiresAt?: string;
|
|
||||||
|
|
||||||
/** IAB TCF String (falls aktiviert) */
|
|
||||||
tcfString?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Minimaler Consent-Input fuer setConsent()
|
|
||||||
*/
|
|
||||||
export type ConsentInput = Partial<ConsentCategories> | {
|
|
||||||
categories?: Partial<ConsentCategories>;
|
|
||||||
vendors?: ConsentVendors;
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Configuration
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UI-Position des Banners
|
|
||||||
*/
|
|
||||||
export type BannerPosition = 'bottom' | 'top' | 'center';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Banner-Layout
|
|
||||||
*/
|
|
||||||
export type BannerLayout = 'bar' | 'modal' | 'floating';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Farbschema
|
|
||||||
*/
|
|
||||||
export type BannerTheme = 'light' | 'dark' | 'auto';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UI-Konfiguration
|
|
||||||
*/
|
|
||||||
export interface ConsentUIConfig {
|
|
||||||
/** Position des Banners */
|
|
||||||
position?: BannerPosition;
|
|
||||||
|
|
||||||
/** Layout-Typ */
|
|
||||||
layout?: BannerLayout;
|
|
||||||
|
|
||||||
/** Farbschema */
|
|
||||||
theme?: BannerTheme;
|
|
||||||
|
|
||||||
/** Pfad zu Custom CSS */
|
|
||||||
customCss?: string;
|
|
||||||
|
|
||||||
/** z-index fuer Banner */
|
|
||||||
zIndex?: number;
|
|
||||||
|
|
||||||
/** Scroll blockieren bei Modal */
|
|
||||||
blockScrollOnModal?: boolean;
|
|
||||||
|
|
||||||
/** Custom Container-ID */
|
|
||||||
containerId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Consent-Verhaltens-Konfiguration
|
|
||||||
*/
|
|
||||||
export interface ConsentBehaviorConfig {
|
|
||||||
/** Muss Nutzer interagieren? */
|
|
||||||
required?: boolean;
|
|
||||||
|
|
||||||
/** "Alle ablehnen" Button sichtbar */
|
|
||||||
rejectAllVisible?: boolean;
|
|
||||||
|
|
||||||
/** "Alle akzeptieren" Button sichtbar */
|
|
||||||
acceptAllVisible?: boolean;
|
|
||||||
|
|
||||||
/** Einzelne Kategorien waehlbar */
|
|
||||||
granularControl?: boolean;
|
|
||||||
|
|
||||||
/** Einzelne Vendors waehlbar */
|
|
||||||
vendorControl?: boolean;
|
|
||||||
|
|
||||||
/** Auswahl speichern */
|
|
||||||
rememberChoice?: boolean;
|
|
||||||
|
|
||||||
/** Speicherdauer in Tagen */
|
|
||||||
rememberDays?: number;
|
|
||||||
|
|
||||||
/** Nur in EU anzeigen (Geo-Targeting) */
|
|
||||||
geoTargeting?: boolean;
|
|
||||||
|
|
||||||
/** Erneut nachfragen nach X Tagen */
|
|
||||||
recheckAfterDays?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TCF 2.2 Konfiguration
|
|
||||||
*/
|
|
||||||
export interface TCFConfig {
|
|
||||||
/** TCF aktivieren */
|
|
||||||
enabled?: boolean;
|
|
||||||
|
|
||||||
/** CMP ID */
|
|
||||||
cmpId?: number;
|
|
||||||
|
|
||||||
/** CMP Version */
|
|
||||||
cmpVersion?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PWA-spezifische Konfiguration
|
|
||||||
*/
|
|
||||||
export interface PWAConfig {
|
|
||||||
/** Offline-Unterstuetzung aktivieren */
|
|
||||||
offlineSupport?: boolean;
|
|
||||||
|
|
||||||
/** Bei Reconnect synchronisieren */
|
|
||||||
syncOnReconnect?: boolean;
|
|
||||||
|
|
||||||
/** Cache-Strategie */
|
|
||||||
cacheStrategy?: 'stale-while-revalidate' | 'network-first' | 'cache-first';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Haupt-Konfiguration fuer ConsentManager
|
|
||||||
*/
|
|
||||||
export interface ConsentConfig {
|
|
||||||
// Pflichtfelder
|
|
||||||
/** API-Endpunkt fuer Consent-Backend */
|
|
||||||
apiEndpoint: string;
|
|
||||||
|
|
||||||
/** Site-ID */
|
|
||||||
siteId: string;
|
|
||||||
|
|
||||||
// Sprache
|
|
||||||
/** Sprache (ISO 639-1) */
|
|
||||||
language?: string;
|
|
||||||
|
|
||||||
/** Fallback-Sprache */
|
|
||||||
fallbackLanguage?: string;
|
|
||||||
|
|
||||||
// UI
|
|
||||||
/** UI-Konfiguration */
|
|
||||||
ui?: ConsentUIConfig;
|
|
||||||
|
|
||||||
// Verhalten
|
|
||||||
/** Consent-Verhaltens-Konfiguration */
|
|
||||||
consent?: ConsentBehaviorConfig;
|
|
||||||
|
|
||||||
// Kategorien
|
|
||||||
/** Aktive Kategorien */
|
|
||||||
categories?: ConsentCategory[];
|
|
||||||
|
|
||||||
// TCF
|
|
||||||
/** TCF 2.2 Konfiguration */
|
|
||||||
tcf?: TCFConfig;
|
|
||||||
|
|
||||||
// PWA
|
|
||||||
/** PWA-Konfiguration */
|
|
||||||
pwa?: PWAConfig;
|
|
||||||
|
|
||||||
// Callbacks
|
|
||||||
/** Callback bei Consent-Aenderung */
|
|
||||||
onConsentChange?: (consent: ConsentState) => void;
|
|
||||||
|
|
||||||
/** Callback wenn Banner angezeigt wird */
|
|
||||||
onBannerShow?: () => void;
|
|
||||||
|
|
||||||
/** Callback wenn Banner geschlossen wird */
|
|
||||||
onBannerHide?: () => void;
|
|
||||||
|
|
||||||
/** Callback bei Fehler */
|
|
||||||
onError?: (error: Error) => void;
|
|
||||||
|
|
||||||
// Debug
|
|
||||||
/** Debug-Modus aktivieren */
|
|
||||||
debug?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Vendor Configuration
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cookie-Information
|
|
||||||
*/
|
|
||||||
export interface CookieInfo {
|
|
||||||
/** Cookie-Name */
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/** Cookie-Domain */
|
|
||||||
domain: string;
|
|
||||||
|
|
||||||
/** Ablaufzeit (z.B. "2 Jahre", "Session") */
|
|
||||||
expiration: string;
|
|
||||||
|
|
||||||
/** Speichertyp */
|
|
||||||
type: 'http' | 'localStorage' | 'sessionStorage' | 'indexedDB';
|
|
||||||
|
|
||||||
/** Beschreibung */
|
|
||||||
description: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Vendor-Definition
|
|
||||||
*/
|
|
||||||
export interface ConsentVendor {
|
|
||||||
/** Eindeutige Vendor-ID */
|
|
||||||
id: string;
|
|
||||||
|
|
||||||
/** Anzeigename */
|
|
||||||
name: string;
|
|
||||||
|
|
||||||
/** Kategorie */
|
|
||||||
category: ConsentCategory;
|
|
||||||
|
|
||||||
/** IAB TCF Purposes (falls relevant) */
|
|
||||||
purposes?: number[];
|
|
||||||
|
|
||||||
/** Legitimate Interests */
|
|
||||||
legitimateInterests?: number[];
|
|
||||||
|
|
||||||
/** Cookie-Liste */
|
|
||||||
cookies: CookieInfo[];
|
|
||||||
|
|
||||||
/** Link zur Datenschutzerklaerung */
|
|
||||||
privacyPolicyUrl: string;
|
|
||||||
|
|
||||||
/** Datenaufbewahrung */
|
|
||||||
dataRetention?: string;
|
|
||||||
|
|
||||||
/** Datentransfer (z.B. "USA (EU-US DPF)", "EU") */
|
|
||||||
dataTransfer?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// API Types
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API-Antwort fuer Consent-Erstellung
|
|
||||||
*/
|
|
||||||
export interface ConsentAPIResponse {
|
|
||||||
consentId: string;
|
|
||||||
timestamp: string;
|
|
||||||
expiresAt: string;
|
|
||||||
version: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* API-Antwort fuer Site-Konfiguration
|
|
||||||
*/
|
|
||||||
export interface SiteConfigResponse {
|
|
||||||
siteId: string;
|
|
||||||
siteName: string;
|
|
||||||
categories: CategoryConfig[];
|
|
||||||
ui: ConsentUIConfig;
|
|
||||||
legal: LegalConfig;
|
|
||||||
tcf?: TCFConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kategorie-Konfiguration vom Server
|
|
||||||
*/
|
|
||||||
export interface CategoryConfig {
|
|
||||||
id: ConsentCategory;
|
|
||||||
name: Record<string, string>;
|
|
||||||
description: Record<string, string>;
|
|
||||||
required: boolean;
|
|
||||||
vendors: ConsentVendor[];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rechtliche Konfiguration
|
|
||||||
*/
|
|
||||||
export interface LegalConfig {
|
|
||||||
privacyPolicyUrl: string;
|
|
||||||
imprintUrl: string;
|
|
||||||
dpo?: {
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Events
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event-Typen
|
|
||||||
*/
|
|
||||||
export type ConsentEventType =
|
|
||||||
| 'init'
|
|
||||||
| 'change'
|
|
||||||
| 'accept_all'
|
|
||||||
| 'reject_all'
|
|
||||||
| 'save_selection'
|
|
||||||
| 'banner_show'
|
|
||||||
| 'banner_hide'
|
|
||||||
| 'settings_open'
|
|
||||||
| 'settings_close'
|
|
||||||
| 'vendor_enable'
|
|
||||||
| 'vendor_disable'
|
|
||||||
| 'error';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event-Listener Callback
|
|
||||||
*/
|
|
||||||
export type ConsentEventCallback<T = unknown> = (data: T) => void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event-Daten fuer verschiedene Events
|
|
||||||
*/
|
|
||||||
export type ConsentEventData = {
|
|
||||||
init: ConsentState | null;
|
|
||||||
change: ConsentState;
|
|
||||||
accept_all: ConsentState;
|
|
||||||
reject_all: ConsentState;
|
|
||||||
save_selection: ConsentState;
|
|
||||||
banner_show: undefined;
|
|
||||||
banner_hide: undefined;
|
|
||||||
settings_open: undefined;
|
|
||||||
settings_close: undefined;
|
|
||||||
vendor_enable: string;
|
|
||||||
vendor_disable: string;
|
|
||||||
error: Error;
|
|
||||||
};
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Storage
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Storage-Adapter Interface
|
|
||||||
*/
|
|
||||||
export interface ConsentStorageAdapter {
|
|
||||||
/** Consent laden */
|
|
||||||
get(): ConsentState | null;
|
|
||||||
|
|
||||||
/** Consent speichern */
|
|
||||||
set(consent: ConsentState): void;
|
|
||||||
|
|
||||||
/** Consent loeschen */
|
|
||||||
clear(): void;
|
|
||||||
|
|
||||||
/** Pruefen ob Consent existiert */
|
|
||||||
exists(): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
|
||||||
// Translations
|
|
||||||
// =============================================================================
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Uebersetzungsstruktur
|
|
||||||
*/
|
|
||||||
export interface ConsentTranslations {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
acceptAll: string;
|
|
||||||
rejectAll: string;
|
|
||||||
settings: string;
|
|
||||||
saveSelection: string;
|
|
||||||
close: string;
|
|
||||||
categories: {
|
|
||||||
[K in ConsentCategory]: {
|
|
||||||
name: string;
|
|
||||||
description: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
footer: {
|
|
||||||
privacyPolicy: string;
|
|
||||||
imprint: string;
|
|
||||||
cookieDetails: string;
|
|
||||||
};
|
|
||||||
accessibility: {
|
|
||||||
closeButton: string;
|
|
||||||
categoryToggle: string;
|
|
||||||
requiredCategory: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Alle unterstuetzten Sprachen
|
|
||||||
*/
|
|
||||||
export type SupportedLanguage =
|
|
||||||
| 'de' | 'en' | 'fr' | 'es' | 'it' | 'nl' | 'pl' | 'pt'
|
|
||||||
| 'cs' | 'da' | 'el' | 'fi' | 'hu' | 'ro' | 'sk' | 'sl' | 'sv';
|
|
||||||
|
|||||||
26
consent-sdk/src/types/storage.ts
Normal file
26
consent-sdk/src/types/storage.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Consent SDK Types — Storage adapter interface
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ConsentState } from './core';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Storage
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Storage-Adapter Interface
|
||||||
|
*/
|
||||||
|
export interface ConsentStorageAdapter {
|
||||||
|
/** Consent laden */
|
||||||
|
get(): ConsentState | null;
|
||||||
|
|
||||||
|
/** Consent speichern */
|
||||||
|
set(consent: ConsentState): void;
|
||||||
|
|
||||||
|
/** Consent loeschen */
|
||||||
|
clear(): void;
|
||||||
|
|
||||||
|
/** Pruefen ob Consent existiert */
|
||||||
|
exists(): boolean;
|
||||||
|
}
|
||||||
45
consent-sdk/src/types/translations.ts
Normal file
45
consent-sdk/src/types/translations.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
/**
|
||||||
|
* Consent SDK Types — Translations and i18n
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ConsentCategory } from './core';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Translations
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uebersetzungsstruktur
|
||||||
|
*/
|
||||||
|
export interface ConsentTranslations {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
acceptAll: string;
|
||||||
|
rejectAll: string;
|
||||||
|
settings: string;
|
||||||
|
saveSelection: string;
|
||||||
|
close: string;
|
||||||
|
categories: {
|
||||||
|
[K in ConsentCategory]: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
footer: {
|
||||||
|
privacyPolicy: string;
|
||||||
|
imprint: string;
|
||||||
|
cookieDetails: string;
|
||||||
|
};
|
||||||
|
accessibility: {
|
||||||
|
closeButton: string;
|
||||||
|
categoryToggle: string;
|
||||||
|
requiredCategory: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alle unterstuetzten Sprachen
|
||||||
|
*/
|
||||||
|
export type SupportedLanguage =
|
||||||
|
| 'de' | 'en' | 'fr' | 'es' | 'it' | 'nl' | 'pl' | 'pt'
|
||||||
|
| 'cs' | 'da' | 'el' | 'fi' | 'hu' | 'ro' | 'sk' | 'sl' | 'sv';
|
||||||
61
consent-sdk/src/types/vendor.ts
Normal file
61
consent-sdk/src/types/vendor.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* Consent SDK Types — Vendor definitions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ConsentCategory } from './core';
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Vendor Configuration
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cookie-Information
|
||||||
|
*/
|
||||||
|
export interface CookieInfo {
|
||||||
|
/** Cookie-Name */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Cookie-Domain */
|
||||||
|
domain: string;
|
||||||
|
|
||||||
|
/** Ablaufzeit (z.B. "2 Jahre", "Session") */
|
||||||
|
expiration: string;
|
||||||
|
|
||||||
|
/** Speichertyp */
|
||||||
|
type: 'http' | 'localStorage' | 'sessionStorage' | 'indexedDB';
|
||||||
|
|
||||||
|
/** Beschreibung */
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vendor-Definition
|
||||||
|
*/
|
||||||
|
export interface ConsentVendor {
|
||||||
|
/** Eindeutige Vendor-ID */
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/** Anzeigename */
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
/** Kategorie */
|
||||||
|
category: ConsentCategory;
|
||||||
|
|
||||||
|
/** IAB TCF Purposes (falls relevant) */
|
||||||
|
purposes?: number[];
|
||||||
|
|
||||||
|
/** Legitimate Interests */
|
||||||
|
legitimateInterests?: number[];
|
||||||
|
|
||||||
|
/** Cookie-Liste */
|
||||||
|
cookies: CookieInfo[];
|
||||||
|
|
||||||
|
/** Link zur Datenschutzerklaerung */
|
||||||
|
privacyPolicyUrl: string;
|
||||||
|
|
||||||
|
/** Datenaufbewahrung */
|
||||||
|
dataRetention?: string;
|
||||||
|
|
||||||
|
/** Datentransfer (z.B. "USA (EU-US DPF)", "EU") */
|
||||||
|
dataTransfer?: string;
|
||||||
|
}
|
||||||
9
dsms-gateway/config.py
Normal file
9
dsms-gateway/config.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""
|
||||||
|
DSMS Gateway — runtime configuration from environment variables.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
IPFS_API_URL = os.getenv("IPFS_API_URL", "http://dsms-node:5001")
|
||||||
|
IPFS_GATEWAY_URL = os.getenv("IPFS_GATEWAY_URL", "http://dsms-node:8080")
|
||||||
|
JWT_SECRET = os.getenv("JWT_SECRET", "your-super-secret-jwt-key-change-in-production")
|
||||||
76
dsms-gateway/dependencies.py
Normal file
76
dsms-gateway/dependencies.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
DSMS Gateway — shared FastAPI dependencies and IPFS helper coroutines.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import Header, HTTPException
|
||||||
|
|
||||||
|
from config import IPFS_API_URL
|
||||||
|
|
||||||
|
|
||||||
|
async def verify_token(authorization: Optional[str] = Header(None)) -> dict:
|
||||||
|
"""Verifiziert JWT Token (vereinfacht für MVP)"""
|
||||||
|
if not authorization:
|
||||||
|
raise HTTPException(status_code=401, detail="Authorization header fehlt")
|
||||||
|
|
||||||
|
# In Produktion: JWT validieren
|
||||||
|
# Für MVP: Einfache Token-Prüfung
|
||||||
|
if not authorization.startswith("Bearer "):
|
||||||
|
raise HTTPException(status_code=401, detail="Ungültiges Token-Format")
|
||||||
|
|
||||||
|
return {"valid": True}
|
||||||
|
|
||||||
|
|
||||||
|
async def ipfs_add(content: bytes, pin: bool = True) -> dict:
|
||||||
|
"""Fügt Inhalt zu IPFS hinzu"""
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
files = {"file": ("document", content)}
|
||||||
|
params = {"pin": str(pin).lower()}
|
||||||
|
|
||||||
|
response = await client.post(
|
||||||
|
f"{IPFS_API_URL}/api/v0/add",
|
||||||
|
files=files,
|
||||||
|
params=params
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=502,
|
||||||
|
detail=f"IPFS Fehler: {response.text}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def ipfs_cat(cid: str) -> bytes:
|
||||||
|
"""Liest Inhalt von IPFS"""
|
||||||
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{IPFS_API_URL}/api/v0/cat",
|
||||||
|
params={"arg": cid}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Dokument nicht gefunden: {cid}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return response.content
|
||||||
|
|
||||||
|
|
||||||
|
async def ipfs_pin_ls() -> list:
|
||||||
|
"""Listet alle gepinnten Objekte"""
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{IPFS_API_URL}/api/v0/pin/ls",
|
||||||
|
params={"type": "recursive"}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
return []
|
||||||
|
|
||||||
|
data = response.json()
|
||||||
|
return list(data.get("Keys", {}).keys())
|
||||||
@@ -3,17 +3,18 @@ DSMS Gateway - REST API für dezentrales Speichersystem
|
|||||||
Bietet eine vereinfachte API über IPFS für BreakPilot
|
Bietet eine vereinfachte API über IPFS für BreakPilot
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
import os
|
import os
|
||||||
import json
|
|
||||||
import httpx
|
# Ensure the gateway directory itself is on the path so routers can use flat imports.
|
||||||
import hashlib
|
sys.path.insert(0, os.path.dirname(__file__))
|
||||||
from datetime import datetime
|
|
||||||
from typing import Optional
|
from fastapi import FastAPI
|
||||||
from fastapi import FastAPI, HTTPException, UploadFile, File, Header, Depends
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import StreamingResponse
|
|
||||||
from pydantic import BaseModel
|
from models import DocumentMetadata, StoredDocument, DocumentList # noqa: F401 — re-exported for tests
|
||||||
import io
|
from routers.documents import router as documents_router
|
||||||
|
from routers.node import router as node_router
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="DSMS Gateway",
|
title="DSMS Gateway",
|
||||||
@@ -30,436 +31,9 @@ app.add_middleware(
|
|||||||
allow_headers=["*"],
|
allow_headers=["*"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Configuration
|
# Router registration
|
||||||
IPFS_API_URL = os.getenv("IPFS_API_URL", "http://dsms-node:5001")
|
app.include_router(node_router)
|
||||||
IPFS_GATEWAY_URL = os.getenv("IPFS_GATEWAY_URL", "http://dsms-node:8080")
|
app.include_router(documents_router)
|
||||||
JWT_SECRET = os.getenv("JWT_SECRET", "your-super-secret-jwt-key-change-in-production")
|
|
||||||
|
|
||||||
|
|
||||||
# Models
|
|
||||||
class DocumentMetadata(BaseModel):
|
|
||||||
"""Metadaten für gespeicherte Dokumente"""
|
|
||||||
document_type: str # 'legal_document', 'consent_record', 'audit_log'
|
|
||||||
document_id: Optional[str] = None
|
|
||||||
version: Optional[str] = None
|
|
||||||
language: Optional[str] = "de"
|
|
||||||
created_at: Optional[str] = None
|
|
||||||
checksum: Optional[str] = None
|
|
||||||
encrypted: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class StoredDocument(BaseModel):
|
|
||||||
"""Antwort nach erfolgreichem Speichern"""
|
|
||||||
cid: str # Content Identifier (IPFS Hash)
|
|
||||||
size: int
|
|
||||||
metadata: DocumentMetadata
|
|
||||||
gateway_url: str
|
|
||||||
timestamp: str
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentList(BaseModel):
|
|
||||||
"""Liste der gespeicherten Dokumente"""
|
|
||||||
documents: list
|
|
||||||
total: int
|
|
||||||
|
|
||||||
|
|
||||||
# Helper Functions
|
|
||||||
async def verify_token(authorization: Optional[str] = Header(None)) -> dict:
|
|
||||||
"""Verifiziert JWT Token (vereinfacht für MVP)"""
|
|
||||||
if not authorization:
|
|
||||||
raise HTTPException(status_code=401, detail="Authorization header fehlt")
|
|
||||||
|
|
||||||
# In Produktion: JWT validieren
|
|
||||||
# Für MVP: Einfache Token-Prüfung
|
|
||||||
if not authorization.startswith("Bearer "):
|
|
||||||
raise HTTPException(status_code=401, detail="Ungültiges Token-Format")
|
|
||||||
|
|
||||||
return {"valid": True}
|
|
||||||
|
|
||||||
|
|
||||||
async def ipfs_add(content: bytes, pin: bool = True) -> dict:
|
|
||||||
"""Fügt Inhalt zu IPFS hinzu"""
|
|
||||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
||||||
files = {"file": ("document", content)}
|
|
||||||
params = {"pin": str(pin).lower()}
|
|
||||||
|
|
||||||
response = await client.post(
|
|
||||||
f"{IPFS_API_URL}/api/v0/add",
|
|
||||||
files=files,
|
|
||||||
params=params
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=502,
|
|
||||||
detail=f"IPFS Fehler: {response.text}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return response.json()
|
|
||||||
|
|
||||||
|
|
||||||
async def ipfs_cat(cid: str) -> bytes:
|
|
||||||
"""Liest Inhalt von IPFS"""
|
|
||||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
|
||||||
response = await client.post(
|
|
||||||
f"{IPFS_API_URL}/api/v0/cat",
|
|
||||||
params={"arg": cid}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=404,
|
|
||||||
detail=f"Dokument nicht gefunden: {cid}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return response.content
|
|
||||||
|
|
||||||
|
|
||||||
async def ipfs_pin_ls() -> list:
|
|
||||||
"""Listet alle gepinnten Objekte"""
|
|
||||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
||||||
response = await client.post(
|
|
||||||
f"{IPFS_API_URL}/api/v0/pin/ls",
|
|
||||||
params={"type": "recursive"}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
return []
|
|
||||||
|
|
||||||
data = response.json()
|
|
||||||
return list(data.get("Keys", {}).keys())
|
|
||||||
|
|
||||||
|
|
||||||
# API Endpoints
|
|
||||||
@app.get("/health")
|
|
||||||
async def health_check():
|
|
||||||
"""Health Check für DSMS Gateway"""
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient(timeout=5.0) as client:
|
|
||||||
response = await client.post(f"{IPFS_API_URL}/api/v0/id")
|
|
||||||
ipfs_status = response.status_code == 200
|
|
||||||
except Exception:
|
|
||||||
ipfs_status = False
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "healthy" if ipfs_status else "degraded",
|
|
||||||
"ipfs_connected": ipfs_status,
|
|
||||||
"timestamp": datetime.utcnow().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/v1/documents", response_model=StoredDocument)
|
|
||||||
async def store_document(
|
|
||||||
file: UploadFile = File(...),
|
|
||||||
document_type: str = "legal_document",
|
|
||||||
document_id: Optional[str] = None,
|
|
||||||
version: Optional[str] = None,
|
|
||||||
language: str = "de",
|
|
||||||
_auth: dict = Depends(verify_token)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Speichert ein Dokument im DSMS.
|
|
||||||
|
|
||||||
- **file**: Das zu speichernde Dokument
|
|
||||||
- **document_type**: Typ des Dokuments (legal_document, consent_record, audit_log)
|
|
||||||
- **document_id**: Optionale ID des Dokuments
|
|
||||||
- **version**: Optionale Versionsnummer
|
|
||||||
- **language**: Sprache (default: de)
|
|
||||||
"""
|
|
||||||
content = await file.read()
|
|
||||||
|
|
||||||
# Checksum berechnen
|
|
||||||
checksum = hashlib.sha256(content).hexdigest()
|
|
||||||
|
|
||||||
# Metadaten erstellen
|
|
||||||
metadata = DocumentMetadata(
|
|
||||||
document_type=document_type,
|
|
||||||
document_id=document_id,
|
|
||||||
version=version,
|
|
||||||
language=language,
|
|
||||||
created_at=datetime.utcnow().isoformat(),
|
|
||||||
checksum=checksum,
|
|
||||||
encrypted=False
|
|
||||||
)
|
|
||||||
|
|
||||||
# Dokument mit Metadaten als JSON verpacken
|
|
||||||
package = {
|
|
||||||
"metadata": metadata.model_dump(),
|
|
||||||
"content_base64": content.hex(), # Hex-encodiert für JSON
|
|
||||||
"filename": file.filename
|
|
||||||
}
|
|
||||||
|
|
||||||
package_bytes = json.dumps(package).encode()
|
|
||||||
|
|
||||||
# Zu IPFS hinzufügen
|
|
||||||
result = await ipfs_add(package_bytes)
|
|
||||||
|
|
||||||
cid = result.get("Hash")
|
|
||||||
size = int(result.get("Size", 0))
|
|
||||||
|
|
||||||
return StoredDocument(
|
|
||||||
cid=cid,
|
|
||||||
size=size,
|
|
||||||
metadata=metadata,
|
|
||||||
gateway_url=f"{IPFS_GATEWAY_URL}/ipfs/{cid}",
|
|
||||||
timestamp=datetime.utcnow().isoformat()
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/documents/{cid}")
|
|
||||||
async def get_document(
|
|
||||||
cid: str,
|
|
||||||
_auth: dict = Depends(verify_token)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Ruft ein Dokument aus dem DSMS ab.
|
|
||||||
|
|
||||||
- **cid**: Content Identifier (IPFS Hash)
|
|
||||||
"""
|
|
||||||
content = await ipfs_cat(cid)
|
|
||||||
|
|
||||||
try:
|
|
||||||
package = json.loads(content)
|
|
||||||
metadata = package.get("metadata", {})
|
|
||||||
original_content = bytes.fromhex(package.get("content_base64", ""))
|
|
||||||
filename = package.get("filename", "document")
|
|
||||||
|
|
||||||
return StreamingResponse(
|
|
||||||
io.BytesIO(original_content),
|
|
||||||
media_type="application/octet-stream",
|
|
||||||
headers={
|
|
||||||
"Content-Disposition": f'attachment; filename="{filename}"',
|
|
||||||
"X-DSMS-Document-Type": metadata.get("document_type", "unknown"),
|
|
||||||
"X-DSMS-Checksum": metadata.get("checksum", ""),
|
|
||||||
"X-DSMS-Created-At": metadata.get("created_at", "")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
# Wenn es kein DSMS-Paket ist, gib rohen Inhalt zurück
|
|
||||||
return StreamingResponse(
|
|
||||||
io.BytesIO(content),
|
|
||||||
media_type="application/octet-stream"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/documents/{cid}/metadata")
|
|
||||||
async def get_document_metadata(
|
|
||||||
cid: str,
|
|
||||||
_auth: dict = Depends(verify_token)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Ruft nur die Metadaten eines Dokuments ab.
|
|
||||||
|
|
||||||
- **cid**: Content Identifier (IPFS Hash)
|
|
||||||
"""
|
|
||||||
content = await ipfs_cat(cid)
|
|
||||||
|
|
||||||
try:
|
|
||||||
package = json.loads(content)
|
|
||||||
return {
|
|
||||||
"cid": cid,
|
|
||||||
"metadata": package.get("metadata", {}),
|
|
||||||
"filename": package.get("filename"),
|
|
||||||
"size": len(bytes.fromhex(package.get("content_base64", "")))
|
|
||||||
}
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
return {
|
|
||||||
"cid": cid,
|
|
||||||
"metadata": {},
|
|
||||||
"raw_size": len(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/documents", response_model=DocumentList)
|
|
||||||
async def list_documents(
|
|
||||||
_auth: dict = Depends(verify_token)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Listet alle gespeicherten Dokumente auf.
|
|
||||||
"""
|
|
||||||
cids = await ipfs_pin_ls()
|
|
||||||
|
|
||||||
documents = []
|
|
||||||
for cid in cids[:100]: # Limit auf 100 für Performance
|
|
||||||
try:
|
|
||||||
content = await ipfs_cat(cid)
|
|
||||||
package = json.loads(content)
|
|
||||||
documents.append({
|
|
||||||
"cid": cid,
|
|
||||||
"metadata": package.get("metadata", {}),
|
|
||||||
"filename": package.get("filename")
|
|
||||||
})
|
|
||||||
except Exception:
|
|
||||||
# Überspringe nicht-DSMS Objekte
|
|
||||||
continue
|
|
||||||
|
|
||||||
return DocumentList(
|
|
||||||
documents=documents,
|
|
||||||
total=len(documents)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.delete("/api/v1/documents/{cid}")
|
|
||||||
async def unpin_document(
|
|
||||||
cid: str,
|
|
||||||
_auth: dict = Depends(verify_token)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Entfernt ein Dokument aus dem lokalen Pin-Set.
|
|
||||||
Das Dokument bleibt im Netzwerk, wird aber bei GC entfernt.
|
|
||||||
|
|
||||||
- **cid**: Content Identifier (IPFS Hash)
|
|
||||||
"""
|
|
||||||
async with httpx.AsyncClient(timeout=30.0) as client:
|
|
||||||
response = await client.post(
|
|
||||||
f"{IPFS_API_URL}/api/v0/pin/rm",
|
|
||||||
params={"arg": cid}
|
|
||||||
)
|
|
||||||
|
|
||||||
if response.status_code != 200:
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=404,
|
|
||||||
detail=f"Konnte Pin nicht entfernen: {cid}"
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"status": "unpinned",
|
|
||||||
"cid": cid,
|
|
||||||
"message": "Dokument wird bei nächster Garbage Collection entfernt"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.post("/api/v1/legal-documents/archive")
|
|
||||||
async def archive_legal_document(
|
|
||||||
document_id: str,
|
|
||||||
version: str,
|
|
||||||
content: str,
|
|
||||||
language: str = "de",
|
|
||||||
_auth: dict = Depends(verify_token)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Archiviert eine rechtliche Dokumentversion dauerhaft.
|
|
||||||
Speziell für AGB, Datenschutzerklärung, etc.
|
|
||||||
|
|
||||||
- **document_id**: ID des Legal Documents
|
|
||||||
- **version**: Versionsnummer
|
|
||||||
- **content**: HTML/Markdown Inhalt
|
|
||||||
- **language**: Sprache
|
|
||||||
"""
|
|
||||||
# Checksum berechnen
|
|
||||||
content_bytes = content.encode('utf-8')
|
|
||||||
checksum = hashlib.sha256(content_bytes).hexdigest()
|
|
||||||
|
|
||||||
# Metadaten
|
|
||||||
metadata = {
|
|
||||||
"document_type": "legal_document",
|
|
||||||
"document_id": document_id,
|
|
||||||
"version": version,
|
|
||||||
"language": language,
|
|
||||||
"created_at": datetime.utcnow().isoformat(),
|
|
||||||
"checksum": checksum,
|
|
||||||
"content_type": "text/html"
|
|
||||||
}
|
|
||||||
|
|
||||||
# Paket erstellen
|
|
||||||
package = {
|
|
||||||
"metadata": metadata,
|
|
||||||
"content": content,
|
|
||||||
"archived_at": datetime.utcnow().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
package_bytes = json.dumps(package, ensure_ascii=False).encode('utf-8')
|
|
||||||
|
|
||||||
# Zu IPFS hinzufügen
|
|
||||||
result = await ipfs_add(package_bytes)
|
|
||||||
|
|
||||||
cid = result.get("Hash")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"cid": cid,
|
|
||||||
"document_id": document_id,
|
|
||||||
"version": version,
|
|
||||||
"checksum": checksum,
|
|
||||||
"archived_at": datetime.utcnow().isoformat(),
|
|
||||||
"verification_url": f"{IPFS_GATEWAY_URL}/ipfs/{cid}"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/verify/{cid}")
|
|
||||||
async def verify_document(cid: str):
|
|
||||||
"""
|
|
||||||
Verifiziert die Integrität eines Dokuments.
|
|
||||||
Öffentlich zugänglich für Audit-Zwecke.
|
|
||||||
|
|
||||||
- **cid**: Content Identifier (IPFS Hash)
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
content = await ipfs_cat(cid)
|
|
||||||
package = json.loads(content)
|
|
||||||
|
|
||||||
# Checksum verifizieren
|
|
||||||
stored_checksum = package.get("metadata", {}).get("checksum")
|
|
||||||
|
|
||||||
if "content_base64" in package:
|
|
||||||
original_content = bytes.fromhex(package["content_base64"])
|
|
||||||
calculated_checksum = hashlib.sha256(original_content).hexdigest()
|
|
||||||
elif "content" in package:
|
|
||||||
calculated_checksum = hashlib.sha256(
|
|
||||||
package["content"].encode('utf-8')
|
|
||||||
).hexdigest()
|
|
||||||
else:
|
|
||||||
calculated_checksum = None
|
|
||||||
|
|
||||||
integrity_valid = (
|
|
||||||
stored_checksum == calculated_checksum
|
|
||||||
if stored_checksum and calculated_checksum
|
|
||||||
else None
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"cid": cid,
|
|
||||||
"exists": True,
|
|
||||||
"integrity_valid": integrity_valid,
|
|
||||||
"metadata": package.get("metadata", {}),
|
|
||||||
"stored_checksum": stored_checksum,
|
|
||||||
"calculated_checksum": calculated_checksum,
|
|
||||||
"verified_at": datetime.utcnow().isoformat()
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {
|
|
||||||
"cid": cid,
|
|
||||||
"exists": False,
|
|
||||||
"error": str(e),
|
|
||||||
"verified_at": datetime.utcnow().isoformat()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/api/v1/node/info")
|
|
||||||
async def get_node_info():
|
|
||||||
"""
|
|
||||||
Gibt Informationen über den DSMS Node zurück.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
||||||
# Node ID
|
|
||||||
id_response = await client.post(f"{IPFS_API_URL}/api/v0/id")
|
|
||||||
node_info = id_response.json() if id_response.status_code == 200 else {}
|
|
||||||
|
|
||||||
# Repo Stats
|
|
||||||
stat_response = await client.post(f"{IPFS_API_URL}/api/v0/repo/stat")
|
|
||||||
repo_stats = stat_response.json() if stat_response.status_code == 200 else {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"node_id": node_info.get("ID"),
|
|
||||||
"protocol_version": node_info.get("ProtocolVersion"),
|
|
||||||
"agent_version": node_info.get("AgentVersion"),
|
|
||||||
"repo_size": repo_stats.get("RepoSize"),
|
|
||||||
"storage_max": repo_stats.get("StorageMax"),
|
|
||||||
"num_objects": repo_stats.get("NumObjects"),
|
|
||||||
"addresses": node_info.get("Addresses", [])[:5] # Erste 5
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
return {"error": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
32
dsms-gateway/models.py
Normal file
32
dsms-gateway/models.py
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
"""
|
||||||
|
DSMS Gateway — Pydantic request/response models.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from typing import Optional
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentMetadata(BaseModel):
|
||||||
|
"""Metadaten für gespeicherte Dokumente"""
|
||||||
|
document_type: str # 'legal_document', 'consent_record', 'audit_log'
|
||||||
|
document_id: Optional[str] = None
|
||||||
|
version: Optional[str] = None
|
||||||
|
language: Optional[str] = "de"
|
||||||
|
created_at: Optional[str] = None
|
||||||
|
checksum: Optional[str] = None
|
||||||
|
encrypted: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
class StoredDocument(BaseModel):
|
||||||
|
"""Antwort nach erfolgreichem Speichern"""
|
||||||
|
cid: str # Content Identifier (IPFS Hash)
|
||||||
|
size: int
|
||||||
|
metadata: DocumentMetadata
|
||||||
|
gateway_url: str
|
||||||
|
timestamp: str
|
||||||
|
|
||||||
|
|
||||||
|
class DocumentList(BaseModel):
|
||||||
|
"""Liste der gespeicherten Dokumente"""
|
||||||
|
documents: list
|
||||||
|
total: int
|
||||||
0
dsms-gateway/routers/__init__.py
Normal file
0
dsms-gateway/routers/__init__.py
Normal file
256
dsms-gateway/routers/documents.py
Normal file
256
dsms-gateway/routers/documents.py
Normal file
@@ -0,0 +1,256 @@
|
|||||||
|
"""
|
||||||
|
Documents router — handles /api/v1/documents and /api/v1/legal-documents endpoints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
import io
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
|
||||||
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
|
from models import DocumentList, DocumentMetadata, StoredDocument
|
||||||
|
from dependencies import verify_token, ipfs_add, ipfs_cat, ipfs_pin_ls
|
||||||
|
from config import IPFS_API_URL, IPFS_GATEWAY_URL
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/documents", response_model=StoredDocument)
|
||||||
|
async def store_document(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
document_type: str = "legal_document",
|
||||||
|
document_id: Optional[str] = None,
|
||||||
|
version: Optional[str] = None,
|
||||||
|
language: str = "de",
|
||||||
|
_auth: dict = Depends(verify_token)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Speichert ein Dokument im DSMS.
|
||||||
|
|
||||||
|
- **file**: Das zu speichernde Dokument
|
||||||
|
- **document_type**: Typ des Dokuments (legal_document, consent_record, audit_log)
|
||||||
|
- **document_id**: Optionale ID des Dokuments
|
||||||
|
- **version**: Optionale Versionsnummer
|
||||||
|
- **language**: Sprache (default: de)
|
||||||
|
"""
|
||||||
|
content = await file.read()
|
||||||
|
|
||||||
|
# Checksum berechnen
|
||||||
|
checksum = hashlib.sha256(content).hexdigest()
|
||||||
|
|
||||||
|
# Metadaten erstellen
|
||||||
|
metadata = DocumentMetadata(
|
||||||
|
document_type=document_type,
|
||||||
|
document_id=document_id,
|
||||||
|
version=version,
|
||||||
|
language=language,
|
||||||
|
created_at=datetime.utcnow().isoformat(),
|
||||||
|
checksum=checksum,
|
||||||
|
encrypted=False
|
||||||
|
)
|
||||||
|
|
||||||
|
# Dokument mit Metadaten als JSON verpacken
|
||||||
|
package = {
|
||||||
|
"metadata": metadata.model_dump(),
|
||||||
|
"content_base64": content.hex(), # Hex-encodiert für JSON
|
||||||
|
"filename": file.filename
|
||||||
|
}
|
||||||
|
|
||||||
|
package_bytes = json.dumps(package).encode()
|
||||||
|
|
||||||
|
# Zu IPFS hinzufügen
|
||||||
|
result = await ipfs_add(package_bytes)
|
||||||
|
|
||||||
|
cid = result.get("Hash")
|
||||||
|
size = int(result.get("Size", 0))
|
||||||
|
|
||||||
|
return StoredDocument(
|
||||||
|
cid=cid,
|
||||||
|
size=size,
|
||||||
|
metadata=metadata,
|
||||||
|
gateway_url=f"{IPFS_GATEWAY_URL}/ipfs/{cid}",
|
||||||
|
timestamp=datetime.utcnow().isoformat()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/documents/{cid}")
|
||||||
|
async def get_document(
|
||||||
|
cid: str,
|
||||||
|
_auth: dict = Depends(verify_token)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Ruft ein Dokument aus dem DSMS ab.
|
||||||
|
|
||||||
|
- **cid**: Content Identifier (IPFS Hash)
|
||||||
|
"""
|
||||||
|
content = await ipfs_cat(cid)
|
||||||
|
|
||||||
|
try:
|
||||||
|
package = json.loads(content)
|
||||||
|
metadata = package.get("metadata", {})
|
||||||
|
original_content = bytes.fromhex(package.get("content_base64", ""))
|
||||||
|
filename = package.get("filename", "document")
|
||||||
|
|
||||||
|
return StreamingResponse(
|
||||||
|
io.BytesIO(original_content),
|
||||||
|
media_type="application/octet-stream",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f'attachment; filename="{filename}"',
|
||||||
|
"X-DSMS-Document-Type": metadata.get("document_type", "unknown"),
|
||||||
|
"X-DSMS-Checksum": metadata.get("checksum", ""),
|
||||||
|
"X-DSMS-Created-At": metadata.get("created_at", "")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
# Wenn es kein DSMS-Paket ist, gib rohen Inhalt zurück
|
||||||
|
return StreamingResponse(
|
||||||
|
io.BytesIO(content),
|
||||||
|
media_type="application/octet-stream"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/documents/{cid}/metadata")
|
||||||
|
async def get_document_metadata(
|
||||||
|
cid: str,
|
||||||
|
_auth: dict = Depends(verify_token)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Ruft nur die Metadaten eines Dokuments ab.
|
||||||
|
|
||||||
|
- **cid**: Content Identifier (IPFS Hash)
|
||||||
|
"""
|
||||||
|
content = await ipfs_cat(cid)
|
||||||
|
|
||||||
|
try:
|
||||||
|
package = json.loads(content)
|
||||||
|
return {
|
||||||
|
"cid": cid,
|
||||||
|
"metadata": package.get("metadata", {}),
|
||||||
|
"filename": package.get("filename"),
|
||||||
|
"size": len(bytes.fromhex(package.get("content_base64", "")))
|
||||||
|
}
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return {
|
||||||
|
"cid": cid,
|
||||||
|
"metadata": {},
|
||||||
|
"raw_size": len(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/documents", response_model=DocumentList)
|
||||||
|
async def list_documents(
|
||||||
|
_auth: dict = Depends(verify_token)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Listet alle gespeicherten Dokumente auf.
|
||||||
|
"""
|
||||||
|
cids = await ipfs_pin_ls()
|
||||||
|
|
||||||
|
documents = []
|
||||||
|
for cid in cids[:100]: # Limit auf 100 für Performance
|
||||||
|
try:
|
||||||
|
content = await ipfs_cat(cid)
|
||||||
|
package = json.loads(content)
|
||||||
|
documents.append({
|
||||||
|
"cid": cid,
|
||||||
|
"metadata": package.get("metadata", {}),
|
||||||
|
"filename": package.get("filename")
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
# Überspringe nicht-DSMS Objekte
|
||||||
|
continue
|
||||||
|
|
||||||
|
return DocumentList(
|
||||||
|
documents=documents,
|
||||||
|
total=len(documents)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/api/v1/documents/{cid}")
|
||||||
|
async def unpin_document(
|
||||||
|
cid: str,
|
||||||
|
_auth: dict = Depends(verify_token)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Entfernt ein Dokument aus dem lokalen Pin-Set.
|
||||||
|
Das Dokument bleibt im Netzwerk, wird aber bei GC entfernt.
|
||||||
|
|
||||||
|
- **cid**: Content Identifier (IPFS Hash)
|
||||||
|
"""
|
||||||
|
async with httpx.AsyncClient(timeout=30.0) as client:
|
||||||
|
response = await client.post(
|
||||||
|
f"{IPFS_API_URL}/api/v0/pin/rm",
|
||||||
|
params={"arg": cid}
|
||||||
|
)
|
||||||
|
|
||||||
|
if response.status_code != 200:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=404,
|
||||||
|
detail=f"Konnte Pin nicht entfernen: {cid}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "unpinned",
|
||||||
|
"cid": cid,
|
||||||
|
"message": "Dokument wird bei nächster Garbage Collection entfernt"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/api/v1/legal-documents/archive")
|
||||||
|
async def archive_legal_document(
|
||||||
|
document_id: str,
|
||||||
|
version: str,
|
||||||
|
content: str,
|
||||||
|
language: str = "de",
|
||||||
|
_auth: dict = Depends(verify_token)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Archiviert eine rechtliche Dokumentversion dauerhaft.
|
||||||
|
Speziell für AGB, Datenschutzerklärung, etc.
|
||||||
|
|
||||||
|
- **document_id**: ID des Legal Documents
|
||||||
|
- **version**: Versionsnummer
|
||||||
|
- **content**: HTML/Markdown Inhalt
|
||||||
|
- **language**: Sprache
|
||||||
|
"""
|
||||||
|
# Checksum berechnen
|
||||||
|
content_bytes = content.encode('utf-8')
|
||||||
|
checksum = hashlib.sha256(content_bytes).hexdigest()
|
||||||
|
|
||||||
|
# Metadaten
|
||||||
|
metadata = {
|
||||||
|
"document_type": "legal_document",
|
||||||
|
"document_id": document_id,
|
||||||
|
"version": version,
|
||||||
|
"language": language,
|
||||||
|
"created_at": datetime.utcnow().isoformat(),
|
||||||
|
"checksum": checksum,
|
||||||
|
"content_type": "text/html"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Paket erstellen
|
||||||
|
package = {
|
||||||
|
"metadata": metadata,
|
||||||
|
"content": content,
|
||||||
|
"archived_at": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
package_bytes = json.dumps(package, ensure_ascii=False).encode('utf-8')
|
||||||
|
|
||||||
|
# Zu IPFS hinzufügen
|
||||||
|
result = await ipfs_add(package_bytes)
|
||||||
|
|
||||||
|
cid = result.get("Hash")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cid": cid,
|
||||||
|
"document_id": document_id,
|
||||||
|
"version": version,
|
||||||
|
"checksum": checksum,
|
||||||
|
"archived_at": datetime.utcnow().isoformat(),
|
||||||
|
"verification_url": f"{IPFS_GATEWAY_URL}/ipfs/{cid}"
|
||||||
|
}
|
||||||
109
dsms-gateway/routers/node.py
Normal file
109
dsms-gateway/routers/node.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
"""
|
||||||
|
Node router — handles /health, /api/v1/verify/{cid}, and /api/v1/node/info endpoints.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from fastapi import APIRouter
|
||||||
|
|
||||||
|
from dependencies import ipfs_cat
|
||||||
|
from config import IPFS_API_URL
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/health")
|
||||||
|
async def health_check():
|
||||||
|
"""Health Check für DSMS Gateway"""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=5.0) as client:
|
||||||
|
response = await client.post(f"{IPFS_API_URL}/api/v0/id")
|
||||||
|
ipfs_status = response.status_code == 200
|
||||||
|
except Exception:
|
||||||
|
ipfs_status = False
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "healthy" if ipfs_status else "degraded",
|
||||||
|
"ipfs_connected": ipfs_status,
|
||||||
|
"timestamp": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/verify/{cid}")
|
||||||
|
async def verify_document(cid: str):
|
||||||
|
"""
|
||||||
|
Verifiziert die Integrität eines Dokuments.
|
||||||
|
Öffentlich zugänglich für Audit-Zwecke.
|
||||||
|
|
||||||
|
- **cid**: Content Identifier (IPFS Hash)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
content = await ipfs_cat(cid)
|
||||||
|
package = json.loads(content)
|
||||||
|
|
||||||
|
# Checksum verifizieren
|
||||||
|
stored_checksum = package.get("metadata", {}).get("checksum")
|
||||||
|
|
||||||
|
if "content_base64" in package:
|
||||||
|
original_content = bytes.fromhex(package["content_base64"])
|
||||||
|
calculated_checksum = hashlib.sha256(original_content).hexdigest()
|
||||||
|
elif "content" in package:
|
||||||
|
calculated_checksum = hashlib.sha256(
|
||||||
|
package["content"].encode('utf-8')
|
||||||
|
).hexdigest()
|
||||||
|
else:
|
||||||
|
calculated_checksum = None
|
||||||
|
|
||||||
|
integrity_valid = (
|
||||||
|
stored_checksum == calculated_checksum
|
||||||
|
if stored_checksum and calculated_checksum
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"cid": cid,
|
||||||
|
"exists": True,
|
||||||
|
"integrity_valid": integrity_valid,
|
||||||
|
"metadata": package.get("metadata", {}),
|
||||||
|
"stored_checksum": stored_checksum,
|
||||||
|
"calculated_checksum": calculated_checksum,
|
||||||
|
"verified_at": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"cid": cid,
|
||||||
|
"exists": False,
|
||||||
|
"error": str(e),
|
||||||
|
"verified_at": datetime.utcnow().isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/api/v1/node/info")
|
||||||
|
async def get_node_info():
|
||||||
|
"""
|
||||||
|
Gibt Informationen über den DSMS Node zurück.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||||
|
# Node ID
|
||||||
|
id_response = await client.post(f"{IPFS_API_URL}/api/v0/id")
|
||||||
|
node_info = id_response.json() if id_response.status_code == 200 else {}
|
||||||
|
|
||||||
|
# Repo Stats
|
||||||
|
stat_response = await client.post(f"{IPFS_API_URL}/api/v0/repo/stat")
|
||||||
|
repo_stats = stat_response.json() if stat_response.status_code == 200 else {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"node_id": node_info.get("ID"),
|
||||||
|
"protocol_version": node_info.get("ProtocolVersion"),
|
||||||
|
"agent_version": node_info.get("AgentVersion"),
|
||||||
|
"repo_size": repo_stats.get("RepoSize"),
|
||||||
|
"storage_max": repo_stats.get("StorageMax"),
|
||||||
|
"num_objects": repo_stats.get("NumObjects"),
|
||||||
|
"addresses": node_info.get("Addresses", [])[:5] # Erste 5
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
@@ -56,7 +56,7 @@ class TestHealthCheck:
|
|||||||
|
|
||||||
def test_health_check_ipfs_connected(self):
|
def test_health_check_ipfs_connected(self):
|
||||||
"""Test: Health Check wenn IPFS verbunden ist"""
|
"""Test: Health Check wenn IPFS verbunden ist"""
|
||||||
with patch("main.httpx.AsyncClient") as mock_client:
|
with patch("routers.node.httpx.AsyncClient") as mock_client:
|
||||||
mock_instance = AsyncMock()
|
mock_instance = AsyncMock()
|
||||||
mock_instance.post.return_value = MagicMock(status_code=200)
|
mock_instance.post.return_value = MagicMock(status_code=200)
|
||||||
mock_client.return_value.__aenter__.return_value = mock_instance
|
mock_client.return_value.__aenter__.return_value = mock_instance
|
||||||
@@ -71,7 +71,7 @@ class TestHealthCheck:
|
|||||||
|
|
||||||
def test_health_check_ipfs_disconnected(self):
|
def test_health_check_ipfs_disconnected(self):
|
||||||
"""Test: Health Check wenn IPFS nicht erreichbar"""
|
"""Test: Health Check wenn IPFS nicht erreichbar"""
|
||||||
with patch("main.httpx.AsyncClient") as mock_client:
|
with patch("routers.node.httpx.AsyncClient") as mock_client:
|
||||||
mock_instance = AsyncMock()
|
mock_instance = AsyncMock()
|
||||||
mock_instance.post.side_effect = Exception("Connection failed")
|
mock_instance.post.side_effect = Exception("Connection failed")
|
||||||
mock_client.return_value.__aenter__.return_value = mock_instance
|
mock_client.return_value.__aenter__.return_value = mock_instance
|
||||||
@@ -104,7 +104,7 @@ class TestAuthorization:
|
|||||||
|
|
||||||
def test_documents_endpoint_with_valid_token_format(self, valid_auth_header):
|
def test_documents_endpoint_with_valid_token_format(self, valid_auth_header):
|
||||||
"""Test: Gültiges Token-Format wird akzeptiert"""
|
"""Test: Gültiges Token-Format wird akzeptiert"""
|
||||||
with patch("main.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
|
with patch("routers.documents.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
|
||||||
mock_pin_ls.return_value = []
|
mock_pin_ls.return_value = []
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
@@ -122,7 +122,7 @@ class TestDocumentStorage:
|
|||||||
|
|
||||||
def test_store_document_success(self, valid_auth_header, mock_ipfs_response):
|
def test_store_document_success(self, valid_auth_header, mock_ipfs_response):
|
||||||
"""Test: Dokument erfolgreich speichern"""
|
"""Test: Dokument erfolgreich speichern"""
|
||||||
with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add:
|
with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add:
|
||||||
mock_add.return_value = mock_ipfs_response
|
mock_add.return_value = mock_ipfs_response
|
||||||
|
|
||||||
test_content = b"Test document content"
|
test_content = b"Test document content"
|
||||||
@@ -148,7 +148,7 @@ class TestDocumentStorage:
|
|||||||
|
|
||||||
def test_store_document_calculates_checksum(self, valid_auth_header, mock_ipfs_response):
|
def test_store_document_calculates_checksum(self, valid_auth_header, mock_ipfs_response):
|
||||||
"""Test: Checksum wird korrekt berechnet"""
|
"""Test: Checksum wird korrekt berechnet"""
|
||||||
with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add:
|
with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add:
|
||||||
mock_add.return_value = mock_ipfs_response
|
mock_add.return_value = mock_ipfs_response
|
||||||
|
|
||||||
test_content = b"Test content for checksum"
|
test_content = b"Test content for checksum"
|
||||||
@@ -191,7 +191,7 @@ class TestDocumentRetrieval:
|
|||||||
"filename": "test.txt"
|
"filename": "test.txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
||||||
mock_cat.return_value = json.dumps(package).encode()
|
mock_cat.return_value = json.dumps(package).encode()
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
@@ -204,7 +204,7 @@ class TestDocumentRetrieval:
|
|||||||
|
|
||||||
def test_get_document_not_found(self, valid_auth_header):
|
def test_get_document_not_found(self, valid_auth_header):
|
||||||
"""Test: Nicht existierendes Dokument gibt 404 zurück"""
|
"""Test: Nicht existierendes Dokument gibt 404 zurück"""
|
||||||
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
||||||
from fastapi import HTTPException
|
from fastapi import HTTPException
|
||||||
mock_cat.side_effect = HTTPException(status_code=404, detail="Not found")
|
mock_cat.side_effect = HTTPException(status_code=404, detail="Not found")
|
||||||
|
|
||||||
@@ -228,7 +228,7 @@ class TestDocumentRetrieval:
|
|||||||
"filename": "test.txt"
|
"filename": "test.txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
||||||
mock_cat.return_value = json.dumps(package).encode()
|
mock_cat.return_value = json.dumps(package).encode()
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
@@ -249,7 +249,7 @@ class TestDocumentList:
|
|||||||
|
|
||||||
def test_list_documents_empty(self, valid_auth_header):
|
def test_list_documents_empty(self, valid_auth_header):
|
||||||
"""Test: Leere Dokumentenliste"""
|
"""Test: Leere Dokumentenliste"""
|
||||||
with patch("main.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
|
with patch("routers.documents.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
|
||||||
mock_pin_ls.return_value = []
|
mock_pin_ls.return_value = []
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
@@ -270,10 +270,10 @@ class TestDocumentList:
|
|||||||
"filename": "test.txt"
|
"filename": "test.txt"
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch("main.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
|
with patch("routers.documents.ipfs_pin_ls", new_callable=AsyncMock) as mock_pin_ls:
|
||||||
mock_pin_ls.return_value = ["QmCid1", "QmCid2"]
|
mock_pin_ls.return_value = ["QmCid1", "QmCid2"]
|
||||||
|
|
||||||
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
with patch("routers.documents.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
||||||
mock_cat.return_value = json.dumps(package).encode()
|
mock_cat.return_value = json.dumps(package).encode()
|
||||||
|
|
||||||
response = client.get(
|
response = client.get(
|
||||||
@@ -293,7 +293,7 @@ class TestDocumentDeletion:
|
|||||||
|
|
||||||
def test_unpin_document_success(self, valid_auth_header):
|
def test_unpin_document_success(self, valid_auth_header):
|
||||||
"""Test: Dokument erfolgreich unpinnen"""
|
"""Test: Dokument erfolgreich unpinnen"""
|
||||||
with patch("main.httpx.AsyncClient") as mock_client:
|
with patch("routers.documents.httpx.AsyncClient") as mock_client:
|
||||||
mock_instance = AsyncMock()
|
mock_instance = AsyncMock()
|
||||||
mock_instance.post.return_value = MagicMock(status_code=200)
|
mock_instance.post.return_value = MagicMock(status_code=200)
|
||||||
mock_client.return_value.__aenter__.return_value = mock_instance
|
mock_client.return_value.__aenter__.return_value = mock_instance
|
||||||
@@ -310,7 +310,7 @@ class TestDocumentDeletion:
|
|||||||
|
|
||||||
def test_unpin_document_not_found(self, valid_auth_header):
|
def test_unpin_document_not_found(self, valid_auth_header):
|
||||||
"""Test: Nicht existierendes Dokument unpinnen"""
|
"""Test: Nicht existierendes Dokument unpinnen"""
|
||||||
with patch("main.httpx.AsyncClient") as mock_client:
|
with patch("routers.documents.httpx.AsyncClient") as mock_client:
|
||||||
mock_instance = AsyncMock()
|
mock_instance = AsyncMock()
|
||||||
mock_instance.post.return_value = MagicMock(status_code=404)
|
mock_instance.post.return_value = MagicMock(status_code=404)
|
||||||
mock_client.return_value.__aenter__.return_value = mock_instance
|
mock_client.return_value.__aenter__.return_value = mock_instance
|
||||||
@@ -330,7 +330,7 @@ class TestLegalDocumentArchive:
|
|||||||
|
|
||||||
def test_archive_legal_document_success(self, valid_auth_header, mock_ipfs_response):
|
def test_archive_legal_document_success(self, valid_auth_header, mock_ipfs_response):
|
||||||
"""Test: Legal Document erfolgreich archivieren"""
|
"""Test: Legal Document erfolgreich archivieren"""
|
||||||
with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add:
|
with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add:
|
||||||
mock_add.return_value = mock_ipfs_response
|
mock_add.return_value = mock_ipfs_response
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
@@ -357,7 +357,7 @@ class TestLegalDocumentArchive:
|
|||||||
content = "<h1>Test Content</h1>"
|
content = "<h1>Test Content</h1>"
|
||||||
expected_checksum = hashlib.sha256(content.encode('utf-8')).hexdigest()
|
expected_checksum = hashlib.sha256(content.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
with patch("main.ipfs_add", new_callable=AsyncMock) as mock_add:
|
with patch("routers.documents.ipfs_add", new_callable=AsyncMock) as mock_add:
|
||||||
mock_add.return_value = mock_ipfs_response
|
mock_add.return_value = mock_ipfs_response
|
||||||
|
|
||||||
response = client.post(
|
response = client.post(
|
||||||
@@ -393,7 +393,7 @@ class TestDocumentVerification:
|
|||||||
"content": content
|
"content": content
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
||||||
mock_cat.return_value = json.dumps(package).encode()
|
mock_cat.return_value = json.dumps(package).encode()
|
||||||
|
|
||||||
response = client.get("/api/v1/verify/QmTestCid123")
|
response = client.get("/api/v1/verify/QmTestCid123")
|
||||||
@@ -415,7 +415,7 @@ class TestDocumentVerification:
|
|||||||
"content": "Actual content"
|
"content": "Actual content"
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
||||||
mock_cat.return_value = json.dumps(package).encode()
|
mock_cat.return_value = json.dumps(package).encode()
|
||||||
|
|
||||||
response = client.get("/api/v1/verify/QmTestCid123")
|
response = client.get("/api/v1/verify/QmTestCid123")
|
||||||
@@ -427,7 +427,7 @@ class TestDocumentVerification:
|
|||||||
|
|
||||||
def test_verify_document_not_found(self):
|
def test_verify_document_not_found(self):
|
||||||
"""Test: Nicht existierendes Dokument verifizieren"""
|
"""Test: Nicht existierendes Dokument verifizieren"""
|
||||||
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
||||||
mock_cat.side_effect = Exception("Not found")
|
mock_cat.side_effect = Exception("Not found")
|
||||||
|
|
||||||
response = client.get("/api/v1/verify/QmNonExistent")
|
response = client.get("/api/v1/verify/QmNonExistent")
|
||||||
@@ -444,7 +444,7 @@ class TestDocumentVerification:
|
|||||||
"content": "test"
|
"content": "test"
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch("main.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
with patch("routers.node.ipfs_cat", new_callable=AsyncMock) as mock_cat:
|
||||||
mock_cat.return_value = json.dumps(package).encode()
|
mock_cat.return_value = json.dumps(package).encode()
|
||||||
|
|
||||||
# Kein Authorization Header!
|
# Kein Authorization Header!
|
||||||
@@ -472,7 +472,7 @@ class TestNodeInfo:
|
|||||||
"NumObjects": 42
|
"NumObjects": 42
|
||||||
}
|
}
|
||||||
|
|
||||||
with patch("main.httpx.AsyncClient") as mock_client:
|
with patch("routers.node.httpx.AsyncClient") as mock_client:
|
||||||
mock_instance = AsyncMock()
|
mock_instance = AsyncMock()
|
||||||
|
|
||||||
async def mock_post(url, **kwargs):
|
async def mock_post(url, **kwargs):
|
||||||
@@ -497,7 +497,7 @@ class TestNodeInfo:
|
|||||||
|
|
||||||
def test_get_node_info_public_access(self):
|
def test_get_node_info_public_access(self):
|
||||||
"""Test: Node-Info ist öffentlich zugänglich"""
|
"""Test: Node-Info ist öffentlich zugänglich"""
|
||||||
with patch("main.httpx.AsyncClient") as mock_client:
|
with patch("routers.node.httpx.AsyncClient") as mock_client:
|
||||||
mock_instance = AsyncMock()
|
mock_instance = AsyncMock()
|
||||||
mock_instance.post.return_value = MagicMock(
|
mock_instance.post.return_value = MagicMock(
|
||||||
status_code=200,
|
status_code=200,
|
||||||
|
|||||||
Reference in New Issue
Block a user