klg-asutk-app/backend/app/api/routes/attachments.py
Yuriy 891e17972c fix(klg): безопасность, deps/security, attachments, CSP, api-client, удаление lib/api.ts
- deps: авторизация через app.services.security (JWT/OIDC), без oidc fallback
- main: AUTH_DEPENDENCY для роутеров, RequestLoggerMiddleware, убран on_event(startup)
- attachments: защита path traversal, проверка владельца/authority
- docker-compose: SECRET_KEY обязателен, отдельная БД keycloak-db
- middleware: ужесточён CSP (без unsafe-eval в prod, без api.openai.com)
- api-client: токен только в памяти, без sessionStorage
- cert_applications: _next_number с SELECT FOR UPDATE
- Удалён lib/api.ts, импорты на @/lib/api/api-client
- docs ERROR_HANDLING: aircraftApi.list(), middleware __init__.py

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 23:06:22 +03:00

100 lines
4.2 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from pathlib import Path
from fastapi import APIRouter, Depends, File, HTTPException, UploadFile
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session
import os
from app.api.deps import get_current_user
from app.api.helpers import audit, is_authority
from app.api.deps import get_db
from app.core.config import settings
from app.models import Attachment
from app.schemas.attachment import AttachmentOut
from app.services.storage import save_upload
router = APIRouter(tags=["attachments"])
@router.post("/attachments/{owner_kind}/{owner_id}", response_model=AttachmentOut)
async def upload_attachment(owner_kind: str, owner_id: str, file: UploadFile = File(...), db: Session = Depends(get_db), user=Depends(get_current_user)):
storage_path, filename = await save_upload(owner_kind, owner_id, file)
att = Attachment(
owner_kind=owner_kind,
owner_id=owner_id,
filename=filename,
content_type=file.content_type,
storage_path=storage_path,
uploaded_by_user_id=user.id,
)
db.add(att)
audit(db, user, "create", "attachment", description=f"Uploaded {filename} for {owner_kind}/{owner_id}")
db.commit()
db.refresh(att)
return att
@router.get("/attachments/{attachment_id}", response_model=AttachmentOut)
def get_attachment_meta(attachment_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
att = db.query(Attachment).filter(Attachment.id == attachment_id).first()
if not att:
raise HTTPException(status_code=404, detail="Not found")
if not is_authority(user) and getattr(att, "uploaded_by_user_id", None) != user.id:
raise HTTPException(status_code=403, detail="Нет доступа к этому вложению")
return att
@router.get("/attachments/{attachment_id}/download")
def download_attachment(attachment_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
att = db.query(Attachment).filter(Attachment.id == attachment_id).first()
if not att:
raise HTTPException(status_code=404, detail="Not found")
if not is_authority(user) and getattr(att, "uploaded_by_user_id", None) != user.id:
raise HTTPException(status_code=403, detail="Нет доступа к этому вложению")
storage_root = Path(settings.INBOX_DATA_DIR).resolve()
real_path = Path(att.storage_path).resolve()
if not str(real_path).startswith(str(storage_root)):
raise HTTPException(status_code=403, detail="Доступ к файлу запрещён")
if not real_path.exists():
raise HTTPException(status_code=404, detail="Файл не найден на диске")
return FileResponse(path=str(real_path), filename=att.filename, media_type=att.content_type)
@router.delete("/attachments/{attachment_id}", status_code=204)
def delete_attachment(attachment_id: str, db: Session = Depends(get_db), user=Depends(get_current_user)):
att = db.query(Attachment).filter(Attachment.id == attachment_id).first()
if not att:
raise HTTPException(status_code=404, detail="Not found")
if not is_authority(user) and getattr(att, "uploaded_by_user_id", None) != user.id:
raise HTTPException(status_code=403, detail="Нет доступа к этому вложению")
# Удаляем файл с диска
if os.path.exists(att.storage_path):
try:
os.remove(att.storage_path)
except Exception as e:
# Логируем ошибку, но продолжаем удаление записи из БД
print(f"Error deleting file {att.storage_path}: {e}")
# Удаляем запись из БД
audit(db, user, "delete", "attachment", attachment_id, description=f"Deleted {att.filename}")
db.delete(att)
db.commit()
return None
@router.get("/attachments/{owner_kind}/{owner_id}", response_model=list[AttachmentOut])
def list_attachments(
owner_kind: str,
owner_id: str,
db: Session = Depends(get_db),
user=Depends(get_current_user),
):
return (
db.query(Attachment)
.filter(Attachment.owner_kind == owner_kind)
.filter(Attachment.owner_id == owner_id)
.order_by(Attachment.created_at.desc())
.all()
)