c6feb5faf2
- Create 04-03-SUMMARY.md with full frontmatter, decisions, threat surface scan - Update STATE.md: plan 3/9, new decisions, session continuity - Update ROADMAP.md: mark 04-01, 04-02, 04-03 plans complete (3/9) - Update REQUIREMENTS.md: mark FOLD-01..FOLD-05 complete
140 lines
7.0 KiB
Markdown
140 lines
7.0 KiB
Markdown
---
|
|
phase: 04-folders-sharing-quotas-document-ux
|
|
plan: 03
|
|
subsystem: api
|
|
tags: [fastapi, sqlalchemy, postgresql, sqlite, folders, audit-log, fts, quota]
|
|
|
|
# Dependency graph
|
|
requires:
|
|
- phase: 03-document-migration-multi-user-isolation
|
|
provides: Document ORM model with folder_id FK, Quota model with CASE WHEN decrement pattern, get_regular_user dep, ownership-assertion-404 pattern
|
|
- phase: 04-01
|
|
provides: AuditLog ORM model with metadata_ attribute, Folder ORM model with UniqueConstraint, Share ORM model
|
|
provides:
|
|
- write_audit_log() async helper in backend/services/audit.py (flush-not-commit, never-raises)
|
|
- POST /api/folders — create folder with parent_id, IntegrityError → 409
|
|
- GET /api/folders — list top-level folders (parent_id IS NULL)
|
|
- GET /api/folders/{id} — folder metadata + breadcrumb array (iterative Python walk)
|
|
- PATCH /api/folders/{id} — rename folder
|
|
- DELETE /api/folders/{id} — cascade-delete with WITH RECURSIVE CTE + atomic quota decrement
|
|
- PATCH /api/documents/{id}/folder — move document to folder (or root)
|
|
- GET /api/documents extended with sort, order, folder_id, q params and is_shared field
|
|
affects: [04-04, 04-05, 04-06, 04-07]
|
|
|
|
# Tech tracking
|
|
tech-stack:
|
|
added: []
|
|
patterns:
|
|
- "write_audit_log: flush-not-commit (session.flush only); never-raises (bare except + warning log)"
|
|
- "Folder IDOR prevention: all ownership failures return 404 not 403"
|
|
- "IntegrityError → 409 for duplicate folder name under same parent"
|
|
- "WITH RECURSIVE CTE for subtree collection + OperationalError fallback for SQLite tests"
|
|
- "Atomic quota decrement: CASE WHEN used_bytes > delta THEN ... ELSE 0 END (SQLite compat)"
|
|
- "MinIO deletion best-effort: per-object try/except, never aborts primary operation"
|
|
- "Breadcrumb: iterative Python parent-walk (not SQL recursive CTE) for SQLite compat"
|
|
- "document_move_router: separate APIRouter with /api/documents prefix on folders module"
|
|
- "FTS via plainto_tsquery wrapped in try/except for SQLite unit test compat"
|
|
|
|
key-files:
|
|
created:
|
|
- backend/services/audit.py
|
|
- backend/api/folders.py
|
|
modified:
|
|
- backend/api/documents.py
|
|
- backend/main.py
|
|
|
|
key-decisions:
|
|
- "write_audit_log uses session.flush() not session.commit() — D-14 architectural constraint: caller owns the transaction"
|
|
- "Breadcrumb built by iterative Python walk (not WITH RECURSIVE) so unit tests on SQLite pass without special handling"
|
|
- "document_move_router (PATCH /api/documents/{id}/folder) placed in folders.py not documents.py as a separate APIRouter with /api/documents prefix — logically a folder operation"
|
|
- "FTS query (plainto_tsquery) wrapped in try/except so SQLite unit tests are not broken — silently degrades to unfiltered results on SQLite"
|
|
- "Backward-compat fast path in list_documents: when no new params (sort=date, order=desc, no folder_id, no q), delegates to storage.list_metadata() for full topic-filter compat"
|
|
|
|
requirements-completed:
|
|
- FOLD-01
|
|
- FOLD-02
|
|
- FOLD-03
|
|
- FOLD-04
|
|
- FOLD-05
|
|
|
|
# Metrics
|
|
duration: 5min
|
|
completed: 2026-05-25
|
|
---
|
|
|
|
# Phase 4 Plan 03: Folders API (FOLD-01..05) Summary
|
|
|
|
**Folder CRUD REST API with breadcrumb navigation, document move, FTS + sort extensions, and a flush-not-commit audit helper usable by all Phase 4 plans**
|
|
|
|
## Performance
|
|
|
|
- **Duration:** 5 min
|
|
- **Started:** 2026-05-25T15:53:05Z
|
|
- **Completed:** 2026-05-25T15:58:00Z
|
|
- **Tasks:** 2
|
|
- **Files modified:** 4
|
|
|
|
## Accomplishments
|
|
|
|
- Created `backend/services/audit.py` with `write_audit_log()` — fire-and-forget audit helper that flushes within the caller's transaction (never commits), catches all exceptions, logs warnings, never re-raises
|
|
- Created `backend/api/folders.py` with 5 FOLD endpoints (create, list, get+breadcrumb, rename, cascade-delete) and the document-move endpoint — all guarded by `get_regular_user`, all returning 404 (not 403) for IDOR prevention
|
|
- Extended `backend/api/documents.py` list endpoint with sort (name/date/size), order (asc/desc), folder_id filter, full-text search via `plainto_tsquery`, and `is_shared` field per document
|
|
|
|
## Task Commits
|
|
|
|
Each task was committed atomically:
|
|
|
|
1. **Task 1: Create backend/services/audit.py** - `259a154` (feat)
|
|
2. **Task 2: Create backend/api/folders.py + extend documents.py + register in main.py** - `33a6f9a` (feat)
|
|
|
|
**Plan metadata:** (included in SUMMARY commit)
|
|
|
|
## Files Created/Modified
|
|
|
|
- `/Users/nik/Documents/Progamming/document_scanner/backend/services/audit.py` - write_audit_log() async helper; flush-not-commit; never-raises
|
|
- `/Users/nik/Documents/Progamming/document_scanner/backend/api/folders.py` - FOLD-01..05 endpoints + document move endpoint; all with 404-not-403 IDOR protection
|
|
- `/Users/nik/Documents/Progamming/document_scanner/backend/api/documents.py` - Extended list_documents with sort/order/folder_id/q/is_shared; backward-compat fast path preserved
|
|
- `/Users/nik/Documents/Progamming/document_scanner/backend/main.py` - Registered folders_router and document_move_router
|
|
|
|
## Decisions Made
|
|
|
|
- write_audit_log uses `session.flush()` — the caller commits; flush queues the entry in the same unit of work without committing
|
|
- Breadcrumb uses iterative Python walk (not `WITH RECURSIVE` in Python/SQLAlchemy) — ensures SQLite unit tests pass
|
|
- `PATCH /api/documents/{id}/folder` placed in `folders.py` as a separate `document_move_router` with the `/api/documents` prefix — avoids circular import between folders and documents modules
|
|
- FTS (`plainto_tsquery`) wrapped in `try/except Exception` in list_documents — SQLite silently degrades to unfiltered results; PostgreSQL works fully
|
|
|
|
## Deviations from Plan
|
|
|
|
None — plan executed exactly as written.
|
|
|
|
## Issues Encountered
|
|
|
|
- Pre-existing test failure `test_extractor.py::test_extract_docx` (missing `python-docx` package) was present before this plan and is out of scope. Not introduced by this plan.
|
|
|
|
## Threat Surface Scan
|
|
|
|
No new trust boundaries introduced beyond what is documented in the plan's `<threat_model>`. All mitigations applied:
|
|
|
|
| Threat | Mitigation Confirmed |
|
|
|--------|----------------------|
|
|
| T-04-03-01: Elevation via admin on folder endpoints | `get_regular_user` on all 6 endpoints |
|
|
| T-04-03-02: FTS cross-user data leak | `Document.user_id == current_user.id` always in WHERE |
|
|
| T-04-03-03: Quota decrement race | Atomic CASE WHEN UPDATE; best-effort MinIO delete |
|
|
| T-04-03-04: Folder IDOR | All ownership failures return 404 (not 403) |
|
|
| T-04-03-05: Cross-user folder assignment | Both doc and target folder ownership checked separately |
|
|
| T-04-03-06: IntegrityError 500 | Caught → 409 Conflict with descriptive message |
|
|
|
|
## Known Stubs
|
|
|
|
None — all endpoints are fully wired and functional.
|
|
|
|
## Next Phase Readiness
|
|
|
|
- `write_audit_log()` available for Plans 04-04 (shares), 04-05 (proxy), 04-06 (admin audit viewer)
|
|
- Folder CRUD complete; Plans 04-04 and later can create folders as test fixtures
|
|
- Document list extended with folder_id and FTS — frontend integration can proceed
|
|
|
|
---
|
|
*Phase: 04-folders-sharing-quotas-document-ux*
|
|
*Completed: 2026-05-25*
|