Moves phases 08–11 execution artifacts from .planning/phases/ to .planning/milestones/v0.2-phases/ to keep .planning/phases/ clean for the next milestone. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
16 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, tags, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | tags | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-stack-upgrade-backend-decomposition | 05 | execute | 2 |
|
|
true |
|
|
|
Purpose: CODE-02 (decomposition) + CODE-08 (single-definition discipline for UploadUrlRequest, DocumentPatch, _CLOUD_PROVIDERS).
Output: backend/api/documents/ package; backend/api/documents.py monolith deleted.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-CONTEXT.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-RESEARCH.md @.planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md @backend/api/documents.py @backend/api/folders.py @backend/main.py @CLAUDE.md Endpoint to sub-module map (locked per RESEARCH.md §"api/documents.py → api/documents/ package"):upload.py: request_upload_url (POST /upload-url, line 93-135), upload_document (POST /upload, line 137-296), confirm_upload (POST /{id}/confirm, line 298-404) crud.py: list_documents (GET "", line 406-529), get_document (GET /{id}, line 531-576), patch_document (PATCH /{id}, line 578-630), delete_document (DELETE /{id}, line 632-705), classify_document (POST /{id}/classify, line 707-742) — placed here per D-08 content.py: stream_document_content (GET /{id}/content, line 765 onwards) + _parse_range helper (line 744-760)
Pydantic model + constant to file map:
shared.py: _CLOUD_PROVIDERS frozenset (line 59), UploadUrlRequest (line 66-69), DocumentPatch (line 71-88)
Package aggregator pattern for backend/api/documents/__init__.py:
from fastapi import APIRouter from api.documents.upload import router as upload_router from api.documents.crud import router as crud_router from api.documents.content import router as content_router
router = APIRouter(prefix="/api/documents", tags=["documents"]) router.include_router(upload_router) router.include_router(crud_router) router.include_router(content_router)
Task 1: Create backend/api/documents/ package — shared.py, upload.py, crud.py, content.py, __init__.py backend/api/documents/__init__.py, backend/api/documents/shared.py, backend/api/documents/upload.py, backend/api/documents/crud.py, backend/api/documents/content.py - backend/api/documents.py lines 1-852 (full source — every endpoint, every Pydantic model, every helper, every import) - backend/api/folders.py (ownership assertion pattern: `if doc is None or doc.user_id != current_user.id: raise HTTPException(status_code=404, ...)`) - .planning/phases/08-stack-upgrade-backend-decomposition/08-PATTERNS.md sections for `backend/api/documents/*` (exact import lists, sub-router declaration, ownership assertion pattern) - CLAUDE.md §"Key Architectural Rules" (atomic quota UPDATE pattern, ownership checks; preserved verbatim) This task creates the package and ALL five files. Execute in this order so the package is in a valid state on disk after each sub-step:(A) Create backend/api/documents/shared.py. Add from __future__ import annotations. Import from typing import Optional and from pydantic import BaseModel, field_validator. Move the constant _CLOUD_PROVIDERS = frozenset({"google_drive", "onedrive", "nextcloud", "webdav"}) from documents.py line 59. Move UploadUrlRequest (line 66-69) and DocumentPatch (line 71-88) verbatim, including the filename_no_path_separators validator which is a security validator at the API boundary (RESEARCH.md confirms it stays in the Pydantic model — D-11 analysis).
(B) Create backend/api/documents/upload.py. Header: from __future__ import annotations. Imports per PATTERNS.md §"backend/api/documents/upload.py" plus from api.documents.shared import UploadUrlRequest, _CLOUD_PROVIDERS. router = APIRouter() — NO prefix (D-04). Move handler functions verbatim including decorators: request_upload_url (lines 93-135), upload_document (lines 137-296), confirm_upload (lines 298-404). Every handler injects current_user: User = Depends(get_current_user) and asserts ownership on any pre-existing document lookup using the pattern if doc is None or doc.user_id != current_user.id: raise HTTPException(status_code=404, ...).
(C) Create backend/api/documents/crud.py. Header + imports per PATTERNS.md §"backend/api/documents/crud.py" plus from api.documents.shared import DocumentPatch, _CLOUD_PROVIDERS. router = APIRouter() — NO prefix. Move handlers verbatim: list_documents (406-529), get_document (531-576), patch_document (578-630), delete_document (632-705), classify_document (707-742) — placed here per D-08. Each handler injects current_user: User = Depends(get_current_user) and includes the ownership assertion.
(D) Create backend/api/documents/content.py. Header: from __future__ import annotations. Imports per PATTERNS.md §"backend/api/documents/content.py" (FastAPI APIRouter, Depends, HTTPException, Request; FastAPI responses StreamingResponse; SQLAlchemy AsyncSession; deps.auth.get_current_user; deps.db.get_db). router = APIRouter() — NO prefix. Move _parse_range(range_header: str, file_size: int) helper verbatim from line 744-760 as a private module-level function. Move stream_document_content handler verbatim from line 765 onwards. The handler injects current_user: User = Depends(get_current_user) and asserts ownership before serving bytes (DOC-04 / SEC-04).
(E) Create backend/api/documents/__init__.py. Contents per PATTERNS.md §"backend/api/documents/__init__.py": import APIRouter from fastapi, import router as upload_router, router as crud_router, router as content_router from the three sub-modules, declare router = APIRouter(prefix="/api/documents", tags=["documents"]), call router.include_router(upload_router), router.include_router(crud_router), router.include_router(content_router). ONLY aggregation — no helpers, no models, no logic.
To avoid Python's package-vs-module name collision, immediately after creating the package directory, rename backend/api/documents.py to backend/api/documents_OLD_REMOVE_IN_TASK_2.py. Task 2 verifies tests pass and then deletes the renamed file.
The route registration order matters: crud.py registers GET "" (matches /api/documents), and crud.py registers GET /{doc_id} which could shadow upload.py's POST /upload-url if route precedence is wrong. FastAPI matches by exact path + method, so HTTP-method differentiation prevents collision; verify by listing routes after include.
cd backend && python -c "from api.documents import router; paths = sorted({(r.path, list(r.methods)[0] if r.methods else '') for r in router.routes}); print('\n'.join(f'{m} {p}' for p, m in paths))"
<acceptance_criteria>
- Files exist: backend/api/documents/__init__.py, backend/api/documents/shared.py, backend/api/documents/upload.py, backend/api/documents/crud.py, backend/api/documents/content.py
- grep -v '^#' backend/api/documents/__init__.py | grep -c 'router = APIRouter(prefix="/api/documents"' returns 1
- grep -v '^#' backend/api/documents/upload.py | grep -c 'router = APIRouter()' returns 1 (NO prefix)
- grep -v '^#' backend/api/documents/crud.py | grep -c 'router = APIRouter()' returns 1 (NO prefix)
- grep -v '^#' backend/api/documents/content.py | grep -c 'router = APIRouter()' returns 1 (NO prefix)
- grep -c "_CLOUD_PROVIDERS = frozenset" backend/api/documents/shared.py returns 1
- grep -rn "_CLOUD_PROVIDERS = frozenset" backend/api/documents/ returns exactly one match (in shared.py)
- grep -c "Depends(get_current_user)" backend/api/documents/upload.py ≥ 3
- grep -c "Depends(get_current_user)" backend/api/documents/crud.py ≥ 5
- grep -c "Depends(get_current_user)" backend/api/documents/content.py ≥ 1
- grep -c "doc.user_id != current_user.id\\|user_id != current_user.id" backend/api/documents/crud.py ≥ 3 (ownership checks on get/patch/delete and classify)
- cd backend && python -c "from api.documents import router; assert len(router.routes) >= 9" exits 0 (3 upload + 5 crud + 1 content = 9)
</acceptance_criteria>
Package exists with all 5 files; sub-router count totals 9; no sub-router carries a prefix; ownership checks preserved; monolith renamed (not yet deleted).
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| Regular user → document endpoints | Every handler enforces get_current_user AND resource.user_id == current_user.id to prevent IDOR (DOC-04 / SEC-04) |
| Browser → MinIO presigned PUT URL | Generated server-side scoped to {user_id}/{document_id}/{uuid4}; never includes admin or other user paths |
| Range-header → byte offset arithmetic | _parse_range must reject malformed input; copied verbatim from monolith |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-08-05-01 | Spoofing/Elevation | IDOR on document endpoints | mitigate | All handlers copied verbatim including current_user.id ownership assertion. Acceptance criterion grep counts ≥ 3 ownership checks in crud.py. |
| T-08-05-02 | Tampering | Sub-router prefix doubling | mitigate | All three sub-routers declare router = APIRouter() with NO prefix; only __init__.py carries prefix="/api/documents". |
| T-08-05-03 | Information Disclosure | DocumentPatch.filename validator | mitigate | filename_no_path_separators validator preserved verbatim in shared.py (path traversal defense). |
| T-08-05-04 | Tampering | Atomic quota UPDATE invariant | mitigate | confirm_upload and delete_document copied verbatim — atomic UPDATE quotas SET used_bytes = used_bytes + $delta WHERE … pattern preserved (CLAUDE.md non-negotiable). |
| T-08-05-05 | Denial of Service | Circular import via __init__.py |
mitigate | __init__.py does ONLY aggregation; shared models in shared.py. |
| T-08-05-SC | Supply Chain | No new packages | accept | This plan installs zero new packages. |
| </threat_model> |
<success_criteria>
backend/api/documents/package with 5 filesbackend/api/documents.pydeleted- All document URL paths unchanged, response shapes unchanged
- Every handler preserves ownership assertion
- All tests pass </success_criteria>