336 lines
17 KiB
Python
336 lines
17 KiB
Python
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]
|