import logging from pathlib import Path from fastapi import APIRouter, Depends, File, Form, HTTPException, Query, Response, UploadFile, status from fastapi.responses import FileResponse from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from starlette.requests import Request from app.core.rbac import require_permission from app.db.database import get_db from app.models.user import User from app.repositories.knowledge_repository import KnowledgeRepository from app.schemas.knowledge import ( DocumentType, KnowledgeDeviceCreate, KnowledgeDeviceResponse, KnowledgeDeviceUpdate, KnowledgeDocumentCreate, KnowledgeDocumentResponse, KnowledgeDocumentUpdate, KnowledgeManufacturerCreate, KnowledgeManufacturerResponse, KnowledgeManufacturerUpdate, KnowledgeNoteCreate, KnowledgeNoteResponse, KnowledgeNoteUpdate, KnowledgeSearchResult, NoteType, normalize_tags, ) from app.services.audit_service import sanitize, write_audit_log from app.services.knowledge_service import KnowledgeService, assert_safe_path logger = logging.getLogger(__name__) router = APIRouter(prefix="/knowledge", tags=["Knowledge"]) def get_manufacturer_or_404(db: Session, manufacturer_id: int): manufacturer = KnowledgeRepository.get_manufacturer(db, manufacturer_id) if manufacturer is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Hersteller nicht gefunden") return manufacturer def get_device_or_404(db: Session, device_id: int): device = KnowledgeRepository.get_device(db, device_id) if device is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Gerät nicht gefunden") return device def get_document_or_404(db: Session, document_id: int): document = KnowledgeRepository.get_document(db, document_id) if document is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument nicht gefunden") return document def get_note_or_404(db: Session, note_id: int): note = KnowledgeRepository.get_note(db, note_id) if note is None: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Notiz nicht gefunden") return note def delete_or_conflict(db: Session, entity) -> None: try: db.delete(entity) db.commit() except IntegrityError as exc: db.rollback() raise HTTPException(status_code=status.HTTP_409_CONFLICT, detail="Eintrag wird noch verwendet") from exc @router.get("/manufacturers", response_model=list[KnowledgeManufacturerResponse]) def list_manufacturers( q: str = "", limit: int = Query(50, ge=1, le=100), offset: int = Query(0, ge=0), db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.read")), ): logger.info("knowledge.manufacturers.list", extra={"actor_user_id": current_user.id}) return KnowledgeRepository.list_manufacturers(db, q=q, limit=limit, offset=offset) @router.get("/manufacturers/{manufacturer_id}", response_model=KnowledgeManufacturerResponse) def get_manufacturer(manufacturer_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.read"))): logger.info("knowledge.manufacturers.detail", extra={"actor_user_id": current_user.id, "target_manufacturer_id": manufacturer_id}) return get_manufacturer_or_404(db, manufacturer_id) @router.post("/manufacturers", response_model=KnowledgeManufacturerResponse, status_code=status.HTTP_201_CREATED) def create_manufacturer( payload: KnowledgeManufacturerCreate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.create")), ): manufacturer = KnowledgeService.create_manufacturer(db, payload) write_audit_log(db, action="knowledge.manufacturers.create", entity_type="knowledge_manufacturers", entity_id=manufacturer.id, entity_label=manufacturer.name, actor=current_user, request=request, after_data=manufacturer) return manufacturer @router.put("/manufacturers/{manufacturer_id}", response_model=KnowledgeManufacturerResponse) def update_manufacturer( manufacturer_id: int, payload: KnowledgeManufacturerUpdate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.update")), ): manufacturer = get_manufacturer_or_404(db, manufacturer_id) before_data = sanitize(manufacturer) updated = KnowledgeService.update_manufacturer(db, manufacturer, payload) write_audit_log(db, action="knowledge.manufacturers.update", entity_type="knowledge_manufacturers", entity_id=updated.id, entity_label=updated.name, actor=current_user, request=request, before_data=before_data, after_data=updated) return updated @router.delete("/manufacturers/{manufacturer_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_manufacturer( manufacturer_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.delete")), ): manufacturer = get_manufacturer_or_404(db, manufacturer_id) before_data = sanitize(manufacturer) label = manufacturer.name delete_or_conflict(db, manufacturer) write_audit_log(db, action="knowledge.manufacturers.delete", entity_type="knowledge_manufacturers", entity_id=manufacturer_id, entity_label=label, actor=current_user, request=request, before_data=before_data) return Response(status_code=status.HTTP_204_NO_CONTENT) @router.get("/devices", response_model=list[KnowledgeDeviceResponse]) def list_devices( q: str = "", manufacturer_id: int | None = None, device_type: str = "", limit: int = Query(50, ge=1, le=100), offset: int = Query(0, ge=0), db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.read")), ): logger.info("knowledge.devices.list", extra={"actor_user_id": current_user.id}) return KnowledgeRepository.list_devices(db, q=q, manufacturer_id=manufacturer_id, device_type=device_type, limit=limit, offset=offset) @router.get("/devices/{device_id}", response_model=KnowledgeDeviceResponse) def get_device(device_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.read"))): logger.info("knowledge.devices.detail", extra={"actor_user_id": current_user.id, "target_device_id": device_id}) return get_device_or_404(db, device_id) @router.post("/devices", response_model=KnowledgeDeviceResponse, status_code=status.HTTP_201_CREATED) def create_device(payload: KnowledgeDeviceCreate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.create"))): device = KnowledgeService.create_device(db, payload) write_audit_log(db, action="knowledge.devices.create", entity_type="knowledge_devices", entity_id=device.id, entity_label=device.name, actor=current_user, request=request, after_data=device) return device @router.put("/devices/{device_id}", response_model=KnowledgeDeviceResponse) def update_device(device_id: int, payload: KnowledgeDeviceUpdate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.update"))): device = get_device_or_404(db, device_id) before_data = sanitize(device) updated = KnowledgeService.update_device(db, device, payload) write_audit_log(db, action="knowledge.devices.update", entity_type="knowledge_devices", entity_id=updated.id, entity_label=updated.name, actor=current_user, request=request, before_data=before_data, after_data=updated) return updated @router.delete("/devices/{device_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_device(device_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.delete"))): device = get_device_or_404(db, device_id) before_data = sanitize(device) label = device.name delete_or_conflict(db, device) write_audit_log(db, action="knowledge.devices.delete", entity_type="knowledge_devices", entity_id=device_id, entity_label=label, actor=current_user, request=request, before_data=before_data) return Response(status_code=status.HTTP_204_NO_CONTENT) @router.get("/documents", response_model=list[KnowledgeDocumentResponse]) def list_documents( q: str = "", manufacturer_id: int | None = None, device_id: int | None = None, document_type: str = "", language: str = "", tag: str = "", limit: int = Query(50, ge=1, le=100), offset: int = Query(0, ge=0), db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.read")), ): logger.info("knowledge.documents.list", extra={"actor_user_id": current_user.id}) return KnowledgeRepository.list_documents(db, q=q, manufacturer_id=manufacturer_id, device_id=device_id, document_type=document_type, language=language, tag=tag, limit=limit, offset=offset) @router.get("/documents/{document_id:int}", response_model=KnowledgeDocumentResponse) def get_document(document_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.read"))): logger.info("knowledge.documents.detail", extra={"actor_user_id": current_user.id, "target_document_id": document_id}) return get_document_or_404(db, document_id) @router.post("/documents", response_model=KnowledgeDocumentResponse, status_code=status.HTTP_201_CREATED) def create_document(payload: KnowledgeDocumentCreate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.create"))): document = KnowledgeService.create_document(db, payload) write_audit_log(db, action="knowledge.documents.create", entity_type="knowledge_documents", entity_id=document.id, entity_label=document.title, actor=current_user, request=request, after_data=document) return document @router.post("/documents/upload", response_model=KnowledgeDocumentResponse, status_code=status.HTTP_201_CREATED) async def upload_document( request: Request, manufacturer_id: int = Form(...), title: str = Form(...), document_type: DocumentType = Form(...), file: UploadFile = File(...), device_id: int | None = Form(default=None), language: str = Form(default="de"), external_url: str = Form(default=""), paperless_document_id: str = Form(default=""), description: str = Form(default=""), tags: str = Form(default=""), db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.upload")), ): payload = KnowledgeDocumentCreate( manufacturer_id=manufacturer_id, device_id=device_id, title=title, document_type=document_type, language=language, external_url=external_url or None, paperless_document_id=paperless_document_id, description=description, tags=normalize_tags(tags), ) document = await KnowledgeService.upload_document(db, file=file, payload=payload) write_audit_log(db, action="knowledge.documents.upload", entity_type="knowledge_documents", entity_id=document.id, entity_label=document.title, actor=current_user, request=request, after_data={key: value for key, value in sanitize(document).items() if key != "file_path"}) return document @router.put("/documents/{document_id:int}", response_model=KnowledgeDocumentResponse) def update_document(document_id: int, payload: KnowledgeDocumentUpdate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.update"))): document = get_document_or_404(db, document_id) before_data = sanitize(document) updated = KnowledgeService.update_document(db, document, payload) write_audit_log(db, action="knowledge.documents.update", entity_type="knowledge_documents", entity_id=updated.id, entity_label=updated.title, actor=current_user, request=request, before_data=before_data, after_data=updated) return updated @router.delete("/documents/{document_id:int}", status_code=status.HTTP_204_NO_CONTENT) def delete_document(document_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.delete"))): document = get_document_or_404(db, document_id) before_data = sanitize(document) label = document.title delete_or_conflict(db, document) write_audit_log(db, action="knowledge.documents.delete", entity_type="knowledge_documents", entity_id=document_id, entity_label=label, actor=current_user, request=request, before_data=before_data) return Response(status_code=status.HTTP_204_NO_CONTENT) @router.get("/documents/{document_id:int}/download") def download_document(document_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.download"))): document = get_document_or_404(db, document_id) if not document.file_path: raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Dokument hat keine lokale Datei") path = assert_safe_path(Path(document.file_path)) if not path.exists() or not path.is_file(): raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Datei nicht gefunden") logger.info("knowledge.documents.download", extra={"actor_user_id": current_user.id, "target_document_id": document_id}) return FileResponse(path, media_type=document.mime_type or "application/octet-stream", filename=document.file_name) @router.get("/notes", response_model=list[KnowledgeNoteResponse]) def list_notes( q: str = "", manufacturer_id: int | None = None, device_id: int | None = None, note_type: str = "", tag: str = "", limit: int = Query(50, ge=1, le=100), offset: int = Query(0, ge=0), db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.read")), ): logger.info("knowledge.notes.list", extra={"actor_user_id": current_user.id}) return KnowledgeRepository.list_notes(db, q=q, manufacturer_id=manufacturer_id, device_id=device_id, note_type=note_type, tag=tag, limit=limit, offset=offset) @router.get("/notes/{note_id}", response_model=KnowledgeNoteResponse) def get_note(note_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.read"))): logger.info("knowledge.notes.detail", extra={"actor_user_id": current_user.id, "target_note_id": note_id}) return get_note_or_404(db, note_id) @router.post("/notes", response_model=KnowledgeNoteResponse, status_code=status.HTTP_201_CREATED) def create_note(payload: KnowledgeNoteCreate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.create"))): note = KnowledgeService.create_note(db, payload) write_audit_log(db, action="knowledge.notes.create", entity_type="knowledge_notes", entity_id=note.id, entity_label=note.title, actor=current_user, request=request, after_data=note) return note @router.put("/notes/{note_id}", response_model=KnowledgeNoteResponse) def update_note(note_id: int, payload: KnowledgeNoteUpdate, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.update"))): note = get_note_or_404(db, note_id) before_data = sanitize(note) updated = KnowledgeService.update_note(db, note, payload) write_audit_log(db, action="knowledge.notes.update", entity_type="knowledge_notes", entity_id=updated.id, entity_label=updated.title, actor=current_user, request=request, before_data=before_data, after_data=updated) return updated @router.delete("/notes/{note_id}", status_code=status.HTTP_204_NO_CONTENT) def delete_note(note_id: int, request: Request, db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.delete"))): note = get_note_or_404(db, note_id) before_data = sanitize(note) label = note.title delete_or_conflict(db, note) write_audit_log(db, action="knowledge.notes.delete", entity_type="knowledge_notes", entity_id=note_id, entity_label=label, actor=current_user, request=request, before_data=before_data) return Response(status_code=status.HTTP_204_NO_CONTENT) @router.get("/search", response_model=list[KnowledgeSearchResult]) def search_knowledge(q: str = Query(..., min_length=1), db: Session = Depends(get_db), current_user: User = Depends(require_permission("knowledge.read"))): logger.info("knowledge.search", extra={"actor_user_id": current_user.id}) manufacturers = KnowledgeRepository.list_manufacturers(db, q=q, limit=10, offset=0) devices = KnowledgeRepository.list_devices(db, q=q, limit=10, offset=0) documents = KnowledgeRepository.list_documents(db, q=q, limit=10, offset=0) notes = KnowledgeRepository.list_notes(db, q=q, limit=10, offset=0) results: list[KnowledgeSearchResult] = [] results.extend(KnowledgeSearchResult(entity_type="manufacturer", id=item.id, title=item.name, href=f"/knowledge/manufacturers?selected={item.id}") for item in manufacturers) results.extend(KnowledgeSearchResult(entity_type="device", id=item.id, title=item.name, subtitle=item.model_number, href=f"/knowledge/devices/{item.id}") for item in devices) results.extend(KnowledgeSearchResult(entity_type="document", id=item.id, title=item.title, subtitle=item.document_type, href=f"/knowledge/documents/{item.id}") for item in documents) results.extend(KnowledgeSearchResult(entity_type="note", id=item.id, title=item.title, subtitle=item.note_type, href=f"/knowledge/notes?selected={item.id}") for item in notes) return results[:30]