""" 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, timezone 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.now(timezone.utc) + timedelta(hours=expires_hours), "iat": datetime.now(timezone.utc), } 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()