""" Consent Service Client für BreakPilot Kommuniziert mit dem Consent Management Service für GDPR-Compliance """ import httpx import jwt from datetime import datetime, timedelta from typing import Optional, List, Dict, Any from dataclasses import dataclass from enum import Enum import os import uuid # Consent Service URL (aus Umgebungsvariable oder Standard für lokale Entwicklung) CONSENT_SERVICE_URL = os.getenv("CONSENT_SERVICE_URL", "http://localhost:8081") # JWT Secret - MUSS mit dem Go Consent Service übereinstimmen! JWT_SECRET = os.getenv("JWT_SECRET", "breakpilot-dev-jwt-secret-2024") def generate_jwt_token( user_id: str = None, email: str = "demo@breakpilot.app", role: str = "user", expires_hours: int = 24 ) -> str: """ Generiert einen JWT Token für die Authentifizierung beim Consent Service. Args: user_id: Die User-ID (wird generiert falls nicht angegeben) email: Die E-Mail-Adresse des Benutzers role: Die Rolle (user, admin, super_admin) expires_hours: Gültigkeitsdauer in Stunden Returns: JWT Token als String """ if user_id is None: user_id = str(uuid.uuid4()) payload = { "user_id": user_id, "email": email, "role": role, "exp": datetime.utcnow() + timedelta(hours=expires_hours), "iat": datetime.utcnow(), } return jwt.encode(payload, JWT_SECRET, algorithm="HS256") def generate_demo_token() -> str: """Generiert einen Demo-Token für nicht-authentifizierte Benutzer""" return generate_jwt_token( user_id="demo-user-" + str(uuid.uuid4())[:8], email="demo@breakpilot.app", role="user" ) class DocumentType(str, Enum): TERMS = "terms" PRIVACY = "privacy" COOKIES = "cookies" COMMUNITY = "community" @dataclass class ConsentStatus: has_consent: bool current_version_id: Optional[str] = None consented_version: Optional[str] = None needs_update: bool = False consented_at: Optional[str] = None @dataclass class DocumentVersion: id: str document_id: str version: str language: str title: str content: str summary: Optional[str] = None class ConsentClient: """Client für die Kommunikation mit dem Consent Service""" def __init__(self, base_url: str = CONSENT_SERVICE_URL): self.base_url = base_url.rstrip("/") self.api_url = f"{self.base_url}/api/v1" def _get_headers(self, jwt_token: str) -> Dict[str, str]: """Erstellt die Header mit JWT Token""" return { "Authorization": f"Bearer {jwt_token}", "Content-Type": "application/json" } async def check_consent( self, jwt_token: str, document_type: DocumentType, language: str = "de" ) -> ConsentStatus: """ Prüft ob der Benutzer dem Dokument zugestimmt hat. Gibt zurück ob eine Zustimmung vorliegt und ob sie aktuell ist. """ async with httpx.AsyncClient() as client: try: response = await client.get( f"{self.api_url}/consent/check/{document_type.value}", headers=self._get_headers(jwt_token), params={"language": language}, timeout=10.0 ) if response.status_code == 200: data = response.json() return ConsentStatus( has_consent=data.get("has_consent", False), current_version_id=data.get("current_version_id"), consented_version=data.get("consented_version"), needs_update=data.get("needs_update", False), consented_at=data.get("consented_at") ) else: return ConsentStatus(has_consent=False, needs_update=True) except httpx.RequestError: # Bei Verbindungsproblemen: Consent nicht erzwingen return ConsentStatus(has_consent=True, needs_update=False) async def check_all_mandatory_consents( self, jwt_token: str, language: str = "de" ) -> Dict[str, ConsentStatus]: """ Prüft alle verpflichtenden Dokumente (Terms, Privacy). Gibt ein Dictionary mit dem Status für jedes Dokument zurück. """ mandatory_docs = [DocumentType.TERMS, DocumentType.PRIVACY] results = {} for doc_type in mandatory_docs: results[doc_type.value] = await self.check_consent(jwt_token, doc_type, language) return results async def get_pending_consents( self, jwt_token: str, language: str = "de" ) -> List[Dict[str, Any]]: """ Gibt eine Liste aller Dokumente zurück, die noch Zustimmung benötigen. Nützlich für die Anzeige beim Login/Registration. """ pending = [] statuses = await self.check_all_mandatory_consents(jwt_token, language) for doc_type, status in statuses.items(): if not status.has_consent or status.needs_update: # Hole das aktuelle Dokument doc = await self.get_latest_document(jwt_token, doc_type, language) if doc: pending.append({ "type": doc_type, "version_id": status.current_version_id, "title": doc.title, "content": doc.content, "summary": doc.summary, "is_update": status.has_consent and status.needs_update }) return pending async def get_latest_document( self, jwt_token: str, document_type: str, language: str = "de" ) -> Optional[DocumentVersion]: """Holt die aktuellste Version eines Dokuments""" async with httpx.AsyncClient() as client: try: response = await client.get( f"{self.api_url}/documents/{document_type}/latest", headers=self._get_headers(jwt_token), params={"language": language}, timeout=10.0 ) if response.status_code == 200: data = response.json() return DocumentVersion( id=data["id"], document_id=data["document_id"], version=data["version"], language=data["language"], title=data["title"], content=data["content"], summary=data.get("summary") ) return None except httpx.RequestError: return None async def give_consent( self, jwt_token: str, document_type: str, version_id: str, consented: bool = True ) -> bool: """ Speichert die Zustimmung des Benutzers. Gibt True zurück bei Erfolg. """ async with httpx.AsyncClient() as client: try: response = await client.post( f"{self.api_url}/consent", headers=self._get_headers(jwt_token), json={ "document_type": document_type, "version_id": version_id, "consented": consented }, timeout=10.0 ) return response.status_code == 201 except httpx.RequestError: return False async def get_cookie_categories( self, jwt_token: str, language: str = "de" ) -> List[Dict[str, Any]]: """Holt alle Cookie-Kategorien für das Cookie-Banner""" async with httpx.AsyncClient() as client: try: response = await client.get( f"{self.api_url}/cookies/categories", headers=self._get_headers(jwt_token), params={"language": language}, timeout=10.0 ) if response.status_code == 200: return response.json().get("categories", []) return [] except httpx.RequestError: return [] async def set_cookie_consent( self, jwt_token: str, categories: List[Dict[str, Any]] ) -> bool: """ Speichert die Cookie-Präferenzen. categories: [{"category_id": "...", "consented": true/false}, ...] """ async with httpx.AsyncClient() as client: try: response = await client.post( f"{self.api_url}/cookies/consent", headers=self._get_headers(jwt_token), json={"categories": categories}, timeout=10.0 ) return response.status_code == 200 except httpx.RequestError: return False async def get_my_data(self, jwt_token: str) -> Optional[Dict[str, Any]]: """GDPR Art. 15: Holt alle Daten des Benutzers""" async with httpx.AsyncClient() as client: try: response = await client.get( f"{self.api_url}/privacy/my-data", headers=self._get_headers(jwt_token), timeout=30.0 ) if response.status_code == 200: return response.json() return None except httpx.RequestError: return None async def request_data_export(self, jwt_token: str) -> Optional[str]: """GDPR Art. 20: Fordert einen Datenexport an""" async with httpx.AsyncClient() as client: try: response = await client.post( f"{self.api_url}/privacy/export", headers=self._get_headers(jwt_token), timeout=10.0 ) if response.status_code == 202: return response.json().get("request_id") return None except httpx.RequestError: return None async def request_data_deletion( self, jwt_token: str, reason: Optional[str] = None ) -> Optional[str]: """GDPR Art. 17: Fordert Löschung aller Daten an""" async with httpx.AsyncClient() as client: try: response = await client.post( f"{self.api_url}/privacy/delete", headers=self._get_headers(jwt_token), json={"reason": reason} if reason else {}, timeout=10.0 ) if response.status_code == 202: return response.json().get("request_id") return None except httpx.RequestError: return None async def health_check(self) -> bool: """Prüft ob der Consent Service erreichbar ist""" async with httpx.AsyncClient() as client: try: response = await client.get( f"{self.base_url}/health", timeout=5.0 ) return response.status_code == 200 except httpx.RequestError: return False # Singleton-Instanz für einfachen Zugriff consent_client = ConsentClient()