This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/consent_client.py
BreakPilot Dev 19855efacc
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed
feat: BreakPilot PWA - Full codebase (clean push without large binaries)
All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
2026-02-11 13:25:58 +01:00

360 lines
12 KiB
Python

"""
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()