import asyncio from concurrent.futures import ProcessPoolExecutor from fastapi import APIRouter, BackgroundTasks, Depends, FastAPI, File, HTTPException, Query, UploadFile, status from fastapi.security.utils import get_authorization_scheme_param from .models import ( DocumentEnvelope, ThreadType, InteractionThread, Message, ExportRequest, ExportJob, Document, Thread, Message as DBMessage, Metadata ) from .database import async_session, init_db from .auth import get_current_user from .services import route_document, rate_limit, validate_encrypted_payload, get_s3_client, redis_settings from .workers import export_worker # Router router = APIRouter() @router.post("/documents", response_model=DocumentEnvelope, status_code=201) async def upload_document( background_tasks: BackgroundTasks, metadata: Metadata, file: UploadFile = File(...), current_user=Depends(get_current_user), db: AsyncSession = Depends(async_session), redis=Depends(redis_settings.connection) ): if current_user["role"] != "CITIZEN": raise HTTPException(403, "Only citizens can submit") await rate_limit(current_user["user_id"], redis) content = await file.read() encrypted_payload = base64.b64encode(content).decode('utf-8') loop = asyncio.get_running_loop() is_valid = await loop.run_in_executor(ProcessPoolExecutor(), validate_encrypted_payload, encrypted_payload) if not is_valid: raise HTTPException(400, "Invalid encrypted payload") assigned_unit = route_document(metadata) s3 = await get_s3_client() storage_path = f"blobs/{uuid.uuid4()}.enc" await s3.put_object(Bucket="dvd-blobs", Key=storage_path, Body=content) doc = Document( reference_number=metadata.reference_number, authority_id=metadata.authority_id, doc_type=metadata.doc_type, status="ROUTED" if assigned_unit != "Fallback-Team" else "RECEIVED", storage_path=storage_path, issued_at=metadata.issued_at, retention_date=datetime.now() + timedelta(days=30) ) db.add(doc) await db.commit() await db.refresh(doc) background_tasks.add_task(lambda: print(f"Notify {assigned_unit} about {doc.id}")) return DocumentEnvelope( id=doc.id, metadata=metadata, encrypted_payload=encrypted_payload, status=doc.status, retention_date=doc.retention_date ) @router.post("/documents/{doc_id}/threads", response_model=InteractionThread, status_code=201) async def start_thread( doc_id: str, thread_data: ThreadType, current_user=Depends(get_current_user), db: AsyncSession = Depends(async_session) ): if current_user["role"] != "CITIZEN": raise HTTPException(403) doc = await db.get(Document, doc_id) if not doc: raise HTTPException(404, "Document not found") thread = Thread(document_id=doc_id, type=thread_data.type, status="PENDING_OFFICIAL") db.add(thread) await db.commit() await db.refresh(thread) if thread_data.initial_message: msg = DBMessage(thread_id=thread.id, sender_role="CITIZEN", content=base64.b64encode(thread_data.initial_message.encode()).decode()) db.add(msg) await db.commit() return InteractionThread(id=thread.id, document_id=doc_id, type=thread.type, status=thread.status) @router.get("/threads/{thread_id}/messages", response_model=List[Message]) async def get_messages( thread_id: str, limit: int = Query(default=20, ge=1, le=100), before=None, current_user=Depends(get_current_user), db: AsyncSession = Depends(async_session) ): query = db.query(DBMessage).filter(DBMessage.thread_id == thread_id).order_by(DBMessage.timestamp.desc()) if before: query = query.filter(DBMessage.timestamp < before) messages = await query.limit(limit).all() return [Message.model_validate(m) for m in reversed(messages)] @router.post("/exports", response_model=ExportJob, status_code=202) async def request_export( export_data: ExportRequest, current_user=Depends(get_current_user), arq=Depends(redis_settings.worker) ): if "official" not in current_user["scopes"]: raise HTTPException(403) job_id = str(uuid.uuid4()) await arq.enqueue_job("export_worker", export_data.dict(), job_id=job_id) return ExportJob(job_id=job_id) # App app = FastAPI(title="DirektVermittlungDe API", version="1.0.0") app.include_router(router) @app.on_event("startup") async def startup(): await init_db() @app.get("/") async def root(): return {"message": "DirektVermittlungDe API"}