Olympus/backend/hermes/app/api/knowledge.py
2026-07-03 18:11:58 +02:00

334 lines
17 KiB
Python

import logging
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
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
file_path = document.file_path
delete_or_conflict(db, document)
if file_path:
KnowledgeService.delete_storage_key(file_path)
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)
path = KnowledgeService.open_document_file(document)
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]