Refactor backend and frontend cleanup paths

This commit is contained in:
curo1305
2026-06-16 11:50:17 +02:00
parent 6b56763689
commit e97ca164d7
29 changed files with 1106 additions and 2280 deletions
+53 -20
View File
@@ -1,22 +1,16 @@
"""Shared constants and Pydantic request models for the documents API package.
CODE-08: Single definition of _CLOUD_PROVIDERS, UploadUrlRequest, and DocumentPatch.
These are imported by upload.py and crud.py — never duplicated.
T-05-06-01: _CLOUD_PROVIDERS is an allowlist frozenset; target_backend validated
against it (never against user-supplied strings).
T-05-09-01: DocumentPatch fields declared explicitly — mass assignment prevented.
T-05-09-02: filename_no_path_separators validator preserved verbatim (path traversal
defense at the API boundary — D-11 analysis: stays in Pydantic model).
"""
"""Shared constants, request models, and access helpers for document routes."""
from __future__ import annotations
import uuid
from typing import Optional
from fastapi import HTTPException
from pydantic import BaseModel, Field, field_validator
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from db.models import Document, Share
# Valid cloud backend slugs (T-05-06-01: validated against allowlist, not user-supplied string)
_CLOUD_PROVIDERS = frozenset({"google_drive", "onedrive", "nextcloud", "webdav"})
@@ -26,14 +20,6 @@ class UploadUrlRequest(BaseModel):
class DocumentPatch(BaseModel):
"""Pydantic model for PATCH /api/documents/{doc_id}.
Optional fields — model_fields_set distinguishes "not provided" from "set to null".
At least one field must be present in model_fields_set (enforced in the handler).
T-05-09-01: explicit field declaration prevents mass assignment.
T-05-09-02: only filename and folder_id are accepted — no other fields can be set.
"""
filename: Optional[str] = Field(None, min_length=1, max_length=255)
folder_id: Optional[uuid.UUID] = None
@@ -43,3 +29,50 @@ class DocumentPatch(BaseModel):
if v is not None and ("/" in v or "\\" in v):
raise ValueError("filename must not contain path separators")
return v
def _document_not_found() -> HTTPException:
return HTTPException(status_code=404, detail="Document not found")
def parse_document_uuid(doc_id: str) -> uuid.UUID:
try:
return uuid.UUID(doc_id)
except ValueError:
raise _document_not_found()
async def get_owned_document(
session: AsyncSession,
doc_id: str,
user_id: uuid.UUID,
) -> Document:
uid = parse_document_uuid(doc_id)
doc = await session.get(Document, uid)
if doc is None or doc.user_id != user_id:
raise _document_not_found()
return doc
async def get_accessible_document(
session: AsyncSession,
doc_id: str,
user_id: uuid.UUID,
) -> tuple[Document, bool]:
uid = parse_document_uuid(doc_id)
doc = await session.get(Document, uid)
if doc is None:
raise _document_not_found()
if doc.user_id == user_id:
return doc, False
result = await session.execute(
select(Share).where(
Share.document_id == doc.id,
Share.recipient_id == user_id,
)
)
if result.scalar_one_or_none() is None:
raise _document_not_found()
return doc, True