Services: Admin-Compliance, Backend-Compliance, AI-Compliance-SDK, Consent-SDK, Developer-Portal, PCA-Platform, DSMS Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
468 lines
13 KiB
Python
468 lines
13 KiB
Python
"""
|
|
DSMS Gateway - REST API für dezentrales Speichersystem
|
|
Bietet eine vereinfachte API über IPFS für BreakPilot
|
|
"""
|
|
|
|
import os
|
|
import json
|
|
import httpx
|
|
import hashlib
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
from fastapi import FastAPI, HTTPException, UploadFile, File, Header, Depends
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from fastapi.responses import StreamingResponse
|
|
from pydantic import BaseModel
|
|
import io
|
|
|
|
app = FastAPI(
|
|
title="DSMS Gateway",
|
|
description="Dezentrales Daten Speicher System Gateway für BreakPilot",
|
|
version="1.0.0"
|
|
)
|
|
|
|
# CORS Configuration
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["http://localhost:8000", "http://backend:8000", "*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Configuration
|
|
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")
|
|
|
|
|
|
# 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__":
|
|
import uvicorn
|
|
uvicorn.run(app, host="0.0.0.0", port=8082)
|