From 4adc77d8cca695a3dd6ea0df3b7b67d84879629e Mon Sep 17 00:00:00 2001 From: curo1305 Date: Sun, 31 May 2026 11:36:33 +0200 Subject: [PATCH] docs(06.2): create 4-plan phase covering SHARE-03, SHARE-05, cloud-delete, ADMIN-06 Wave 0: 11 xfail stubs across test_shares/test_documents/test_audit Wave 1 (parallel): SHARE-05 badge + SHARE-03 permission control; cloud-delete propagation Wave 2: audit handle enrichment, user_handle filter, CSV fetch+Blob, daily-export UI Co-Authored-By: Claude Sonnet 4.6 --- .planning/ROADMAP.md | 34 +- .../06.2-01-PLAN.md | 204 +++++++++ .../06.2-02-PLAN.md | 262 ++++++++++++ .../06.2-03-PLAN.md | 290 +++++++++++++ .../06.2-04-PLAN.md | 393 ++++++++++++++++++ 5 files changed, 1176 insertions(+), 7 deletions(-) create mode 100644 .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-01-PLAN.md create mode 100644 .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-02-PLAN.md create mode 100644 .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-03-PLAN.md create mode 100644 .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-04-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index d6c7a40..d3c298d 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -1,6 +1,6 @@ # DocuVault — v1 Roadmap -_Last updated: 2026-05-25_ +_Last updated: 2026-05-31_ ## Mandatory Cross-Cutting Gates (every phase) @@ -320,21 +320,41 @@ Before any phase is marked complete, all three gates must pass: ### Phase 6.2: Close v1 sharing + cloud-delete + CSV export gaps -**Goal**: Close remaining v1 gaps — sharing edge cases, cloud document deletion propagation to the remote backend, and CSV export for the admin audit log. +**Goal**: Close remaining v1 gaps — sharing edge cases (SHARE-03/SHARE-05), cloud document deletion propagation to the remote backend, and CSV export + daily export UI for the admin audit log (ADMIN-06). **Mode:** mvp **Depends on**: Phase 6.1 -**Requirements**: TBD +**Requirements**: SHARE-03, SHARE-05, ADMIN-06 **Success Criteria** (what must be TRUE): -1. TBD +1. Documents shared with others display a "Shared" badge in the owner's list view (reads doc.is_shared, not doc.share_count) +2. Owner can set permission to "view" or "edit" when creating a share and toggle it per-recipient afterward; PATCH /api/shares/{id} enforces IDOR protection (404 on wrong owner) +3. Deleting a cloud document propagates the delete to the cloud provider; failure shows a warning modal with "Remove from app" fallback; ?remove_only=true removes only the DB record; cloud docs never affect quota on delete +4. Admin can download filtered audit log CSV via fetch+Blob (not window.location.href); audit log entries show user handles instead of raw UUIDs; user filter accepts handles (not UUIDs) +5. Admin can list and download Celery-generated daily audit export files from a new section in the Audit Log tab -**Plans**: TBD +**Plans**: 4 plans + +**Wave 0** — Test stubs + +- [ ] 06.2-01-PLAN.md — 11 xfail stubs across test_shares.py, test_documents.py, test_audit.py + +**Wave 1** — Feature slices (parallel) + +- [ ] 06.2-02-PLAN.md — SHARE-05 badge fix + SHARE-03 permission control (backend PATCH + frontend dropdown + toggle) +- [ ] 06.2-03-PLAN.md — Cloud-delete propagation + structured error response + remove_only path + DocumentView warning modal + +**Wave 2** — Audit log enrichment + +- [ ] 06.2-04-PLAN.md — Audit handle JOIN + user_handle filter + CSV fetch+Blob fix + daily-export list + download endpoints + AuditLogTab UI **Phase gates (must pass before Phase 6.2 is complete):** -- [ ] `pytest -v` — zero failures +- [ ] `pytest -v` — zero failures; all 11 promoted tests passing - [ ] Security agent: bandit + pip audit + npm audit all clean +- [ ] IDOR on PATCH /api/shares/{id}: test_share_patch_idor passes +- [ ] Date regex validation confirmed: GET /api/admin/audit-log/daily-exports/invalid-date returns 404 +- [ ] window.location.href removed from AuditLogTab.vue confirmed by grep --- @@ -349,4 +369,4 @@ Before any phase is marked complete, all three gates must pass: | 5. Cloud Storage Backends | 12/12 | Complete | 2026-05-30 | | 6. Performance & Production Hardening | 0/TBD | Not started | — | | 6.1. Close v1.0 audit gaps | 2/2 | Complete | 2026-05-30 | -| 6.2. Close v1 sharing + cloud-delete + CSV export gaps | 0/TBD | Not started | — | +| 6.2. Close v1 sharing + cloud-delete + CSV export gaps | 0/4 | Not started | — | diff --git a/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-01-PLAN.md b/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-01-PLAN.md new file mode 100644 index 0000000..4f9de95 --- /dev/null +++ b/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-01-PLAN.md @@ -0,0 +1,204 @@ +--- +phase: "06.2" +plan: "01" +type: execute +wave: 0 +depends_on: [] +files_modified: + - backend/tests/test_shares.py + - backend/tests/test_documents.py + - backend/tests/test_audit.py +autonomous: true +requirements: + - SHARE-03 + - SHARE-05 + - ADMIN-06 +must_haves: + truths: + - "pytest exits 0 after adding 11 xfail stubs — no new failures" + - "Each stub is reachable by name so Wave 1 and 2 plans can promote them individually" + - "Stubs use strict=False so they report as xfail, not xpass, while implementation is absent" + artifacts: + - path: "backend/tests/test_shares.py" + provides: "xfail stubs for test_share_create_with_permission, test_share_patch_permission, test_share_patch_idor" + contains: "pytest.xfail" + - path: "backend/tests/test_documents.py" + provides: "xfail stubs for test_delete_cloud_document_propagates, test_delete_cloud_document_failure, test_delete_cloud_remove_only" + contains: "pytest.xfail" + - path: "backend/tests/test_audit.py" + provides: "xfail stubs for 5 audit gap tests" + contains: "pytest.xfail" + key_links: + - from: "backend/tests/test_shares.py" + to: "Wave 1 Plan 06.2-02" + via: "test function names (must match exactly)" + pattern: "test_share_create_with_permission|test_share_patch_permission|test_share_patch_idor" + - from: "backend/tests/test_documents.py" + to: "Wave 1 Plan 06.2-03" + via: "test function names (must match exactly)" + pattern: "test_delete_cloud_document_propagates|test_delete_cloud_document_failure|test_delete_cloud_remove_only" + - from: "backend/tests/test_audit.py" + to: "Wave 2 Plan 06.2-04" + via: "test function names (must match exactly)" + pattern: "test_audit_log_includes_user_handle|test_audit_log_filter_by_handle|test_audit_log_filter_unknown_handle|test_daily_exports_list|test_daily_export_download" +--- + + +Add 11 xfail test stubs — one per Wave 0 gap identified in VALIDATION.md — across three test files. These stubs establish the Nyquist contract: each gap has a named, runnable test before any implementation begins. Wave 1 and Wave 2 plans promote individual stubs to real tests. + +Purpose: Nyquist compliance — no task in Waves 1 or 2 can complete without a matching automated test. Stubs guarantee the test function names exist before any executor tries to promote them. + +Output: 11 new test functions (3 in test_shares.py, 3 in test_documents.py, 5 in test_audit.py), all marked xfail(strict=False). + + + +@/Users/nik/Documents/Progamming/document_scanner/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-VALIDATION.md + + + +@/Users/nik/Documents/Progamming/document_scanner/.planning/ROADMAP.md +@/Users/nik/Documents/Progamming/document_scanner/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-CONTEXT.md + + + + + + Task 1: Add xfail stubs to test_shares.py (SHARE-03) + backend/tests/test_shares.py + + - backend/tests/test_shares.py — read the full file to understand the existing async_client/auth_user/second_auth_user/db_session fixture pattern and function naming conventions before appending + - .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-VALIDATION.md — Wave 0 requirements section, exact test names + + +Append three new async test functions to the end of backend/tests/test_shares.py. Each uses the `pytest.xfail("not implemented yet")` call immediately as its first statement (no imports, no fixtures consumed). Use `@pytest.mark.xfail(strict=False, reason="Phase 6.2 — not implemented yet")` decorator OR inline `pytest.xfail(...)` at function start — inline is preferred to match the existing xfail pattern in test_documents.py (which uses the inline call, not the decorator). + +The three function signatures to add are: + +1. `async def test_share_create_with_permission(async_client, auth_user, second_auth_user, db_session):` + - Docstring: "POST /api/shares respects permission field from request body (SHARE-03, D-08, D-10)" + - Body: `pytest.xfail("Phase 6.2 — not implemented yet")` + +2. `async def test_share_patch_permission(async_client, auth_user, second_auth_user, db_session):` + - Docstring: "PATCH /api/shares/{id} changes permission to edit (SHARE-03, D-09)" + - Body: `pytest.xfail("Phase 6.2 — not implemented yet")` + +3. `async def test_share_patch_idor(async_client, auth_user, second_auth_user, db_session):` + - Docstring: "PATCH /api/shares/{id} by non-owner returns 404 — IDOR protection (SHARE-03, D-09, T-IDOR)" + - Body: `pytest.xfail("Phase 6.2 — not implemented yet")` + +Do NOT add any imports — `pytest` is already imported at the top of the file. Do NOT implement any logic beyond the xfail call. + + + cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest tests/test_shares.py::test_share_create_with_permission tests/test_shares.py::test_share_patch_permission tests/test_shares.py::test_share_patch_idor -v 2>&1 | grep -E "xfail|XFAIL|passed|failed" | head -20 + + All three new tests collected and reported as XFAIL (not ERROR, not FAILED); `pytest tests/test_shares.py -x -q` exits 0 + + + + Task 2: Add xfail stubs to test_documents.py (cloud-delete) + backend/tests/test_documents.py + + - backend/tests/test_documents.py — read the full file to confirm import structure, existing xfail pattern (inline pytest.xfail call), and where to append new functions + - .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-VALIDATION.md — Wave 0 requirements section + + +Append three new async test functions to the end of backend/tests/test_documents.py. Use inline `pytest.xfail("Phase 6.2 — not implemented yet")` as the first statement — matching the existing pattern in the file (which uses `@pytest.mark.xfail(strict=False, ...)` decorator on legacy tests at the top, but newer additions in this file use the inline call pattern from VALIDATION.md guidance). + +The three function signatures to add are: + +1. `async def test_delete_cloud_document_propagates(async_client, auth_user, db_session):` + - Docstring: "DELETE /api/documents/{id} for a cloud doc calls cloud backend delete_object (D-01)" + - Body: `pytest.xfail("Phase 6.2 — not implemented yet")` + +2. `async def test_delete_cloud_document_failure(async_client, auth_user, db_session):` + - Docstring: "DELETE /api/documents/{id} returns cloud_delete_failed=True when provider raises (D-03)" + - Body: `pytest.xfail("Phase 6.2 — not implemented yet")` + +3. `async def test_delete_cloud_remove_only(async_client, auth_user, db_session):` + - Docstring: "DELETE /api/documents/{id}?remove_only=true skips cloud delete, removes DB row only (D-02)" + - Body: `pytest.xfail("Phase 6.2 — not implemented yet")` + +All three stubs must have `pytestmark = pytest.mark.asyncio` coverage — confirm this is already at the top of the file or add it if missing. Do not implement any logic beyond xfail. + + + cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest tests/test_documents.py::test_delete_cloud_document_propagates tests/test_documents.py::test_delete_cloud_document_failure tests/test_documents.py::test_delete_cloud_remove_only -v 2>&1 | grep -E "xfail|XFAIL|passed|failed" | head -20 + + All three new tests collected and reported as XFAIL; `pytest tests/test_documents.py -x -q` exits 0 + + + + Task 3: Add xfail stubs to test_audit.py (ADMIN-06 gaps) + backend/tests/test_audit.py + + - backend/tests/test_audit.py — read the full file to see existing helpers (_seed_audit), fixture usage (async_client, admin_user, db_session), and where to append + - .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-VALIDATION.md — Wave 0 requirements section + + +Append five new async test functions to the end of backend/tests/test_audit.py. Use inline `pytest.xfail("Phase 6.2 — not implemented yet")` as the first statement in each body. + +The five function signatures to add are: + +1. `async def test_audit_log_includes_user_handle(async_client, admin_user, db_session):` + - Docstring: "Audit log items include user_handle and actor_handle strings (D-11)" + - Body: `pytest.xfail("Phase 6.2 — not implemented yet")` + +2. `async def test_audit_log_filter_by_handle(async_client, admin_user, db_session):` + - Docstring: "GET /api/admin/audit-log?user_handle=X filters to matching entries (D-12)" + - Body: `pytest.xfail("Phase 6.2 — not implemented yet")` + +3. `async def test_audit_log_filter_unknown_handle(async_client, admin_user, db_session):` + - Docstring: "GET /api/admin/audit-log?user_handle=unknown returns empty items list, not 422 (D-12)" + - Body: `pytest.xfail("Phase 6.2 — not implemented yet")` + +4. `async def test_daily_exports_list(async_client, admin_user):` + - Docstring: "GET /api/admin/audit-log/daily-exports returns {items: [...]} (D-15)" + - Body: `pytest.xfail("Phase 6.2 — not implemented yet")` + +5. `async def test_daily_export_download(async_client, admin_user):` + - Docstring: "GET /api/admin/audit-log/daily-exports/{date} returns CSV bytes with Content-Disposition (D-16)" + - Body: `pytest.xfail("Phase 6.2 — not implemented yet")` + +Do not add any new imports beyond what is already at the top of the file. Do not implement any logic. + + + cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest tests/test_audit.py::test_audit_log_includes_user_handle tests/test_audit.py::test_audit_log_filter_by_handle tests/test_audit.py::test_audit_log_filter_unknown_handle tests/test_audit.py::test_daily_exports_list tests/test_audit.py::test_daily_export_download -v 2>&1 | grep -E "xfail|XFAIL|passed|failed" | head -20 + + All five new tests collected and reported as XFAIL; `pytest tests/test_audit.py -x -q` exits 0; total xfail count in test_audit.py increases by 5 + + + + + +## Trust Boundaries + +| Boundary | Description | +|----------|-------------| +| test runner → test files | xfail stubs must not execute any production code paths | + +## STRIDE Threat Register + +| Threat ID | Category | Component | Disposition | Mitigation Plan | +|-----------|----------|-----------|-------------|-----------------| +| T-06.2-01-01 | Tampering | xfail stubs accidentally implementing logic | accept | Stubs contain only `pytest.xfail(...)` — no imports, no API calls, no fixtures consumed beyond signature | + + + +After all three tasks complete: + +``` +cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest tests/test_shares.py tests/test_audit.py tests/test_documents.py -x -q +``` + +Expected: exits 0, all 11 new stubs reported as xfail. Pre-existing 310 passing tests must remain passing. Pre-existing `test_extract_docx` failure is allowed. + + + +- 11 new xfail stubs collected across the three test files +- `pytest tests/test_shares.py tests/test_audit.py tests/test_documents.py -x -q` exits 0 +- Every stub matches the exact function name from VALIDATION.md Wave 0 Requirements +- No existing passing tests are broken + + + +Create `.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-01-SUMMARY.md` when done. + diff --git a/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-02-PLAN.md b/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-02-PLAN.md new file mode 100644 index 0000000..1ee1e4a --- /dev/null +++ b/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-02-PLAN.md @@ -0,0 +1,262 @@ +--- +phase: "06.2" +plan: "02" +type: execute +wave: 1 +depends_on: + - "06.2-01" +files_modified: + - backend/api/shares.py + - frontend/src/components/documents/DocumentCard.vue + - frontend/src/components/sharing/ShareModal.vue + - frontend/src/stores/documents.js + - backend/tests/test_shares.py +autonomous: true +requirements: + - SHARE-03 + - SHARE-05 +must_haves: + truths: + - "Documents shared with others display a 'Shared' pill in DocumentCard (reads doc.is_shared, not doc.share_count)" + - "Owner can set permission to 'view' or 'edit' when creating a share" + - "Owner can toggle permission per share row after creation" + - "PATCH /api/shares/{id} by the wrong owner returns 404 (IDOR protection)" + - "POST /api/shares respects the permission field from the request body" + artifacts: + - path: "backend/api/shares.py" + provides: "ShareCreate model with permission field; PATCH /{share_id} endpoint" + contains: "class SharePermissionPatch" + - path: "frontend/src/components/documents/DocumentCard.vue" + provides: "Corrected is_shared guard on Shared pill" + contains: "v-if=\"doc.is_shared\"" + - path: "frontend/src/components/sharing/ShareModal.vue" + provides: "Permission dropdown in creation row; View/Edit toggle per share row" + contains: "Permission level" + key_links: + - from: "frontend/src/components/sharing/ShareModal.vue" + to: "PATCH /api/shares/{id}" + via: "docsStore.updateSharePermission(shareId, permission)" + pattern: "updateSharePermission" + - from: "backend/api/shares.py PATCH" + to: "Share.owner_id" + via: "IDOR check — 404 on mismatch" + pattern: "share.owner_id != current_user.id" +--- + + +Close SHARE-05 (badge uses wrong field) and SHARE-03 (no permission control) in a single vertical slice. Delivers: corrected is_shared badge, permission dropdown at share creation, View/Edit toggle per share row, and the backing PATCH endpoint with IDOR protection. + +Purpose: Users can now see which documents they've shared (correct badge), set the permission level when sharing, and change it afterward. Closes two open v1 requirements. + +Output: Modified shares.py (new ShareCreate.permission field + PATCH endpoint), modified DocumentCard.vue (badge fix), modified ShareModal.vue (dropdown + toggle UI), modified documents store (updateSharePermission action), three promoted test stubs. + + + +@/Users/nik/Documents/Progamming/document_scanner/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-RESEARCH.md +@/Users/nik/Documents/Progamming/document_scanner/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-UI-SPEC.md + + + +@/Users/nik/Documents/Progamming/document_scanner/.planning/ROADMAP.md +@/Users/nik/Documents/Progamming/document_scanner/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-CONTEXT.md + + + + +From backend/api/shares.py (current state): + class ShareCreate(BaseModel): + document_id: str + recipient_handle: str + # permission="view" hardcoded at line 97 in grant_share() + + @router.delete("/{share_id}", status_code=204) + async def revoke_share(share_id: str, ...) -> None: + sid = uuid.UUID(share_id) # 404 on ValueError + share = await session.get(Share, sid) + if share is None or share.owner_id != current_user.id: + raise HTTPException(404, "Share not found") # IDOR pattern to mirror + + # Route ordering: GET /received defined BEFORE DELETE /{share_id} + +From backend/db/models.py (Share model — key fields): + Share.id: UUID + Share.document_id: UUID + Share.owner_id: UUID (FK to User) + Share.recipient_id: UUID (FK to User) + Share.permission: str # column exists, default "view" — no migration needed + +From frontend/src/components/documents/DocumentCard.vue (line 31, buggy): + v-if="doc.share_count > 0" # BUG — backend sends is_shared: bool, not share_count + +From frontend/src/components/sharing/ShareModal.vue (shares list row, line 75): + view + # This static "view" span must be replaced with the View/Edit toggle (C-2) + +From frontend/src/stores/documents.js — existing share methods to reference: + shareDocument(docId, recipientHandle) — calls POST /api/shares + revokeShare(shareId) — calls DELETE /api/shares/{id} + listShares(docId) — calls GET /api/shares?document_id={docId} + + + + + + + Task 1: Backend — ShareCreate permission field + PATCH endpoint + backend/api/shares.py, backend/tests/test_shares.py + + - backend/api/shares.py — read the full file; understand ShareCreate model (line 38), grant_share handler (hardcoded permission="view" at line 97), revoke_share IDOR pattern (lines 239-265), route ordering comments + - backend/tests/test_shares.py — read the full file; understand async_client/auth_user/second_auth_user/db_session fixture pattern and _make_doc helper + - .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-RESEARCH.md — Pattern 2 (PATCH IDOR-safe pattern) and Anti-Patterns section (route ordering) + + + - test_share_create_with_permission: POST /api/shares with {"permission": "edit"} returns 201 and body["permission"] == "edit"; POST with no permission field defaults to "view" + - test_share_patch_permission: PATCH /api/shares/{valid_id} with {"permission": "edit"} returns 200 and {"permission": "edit"}; a second PATCH with {"permission": "view"} returns 200 and {"permission": "view"} + - test_share_patch_idor: PATCH /api/shares/{id_owned_by_user_A} authenticated as user_B returns 404 (not 403, not 401) + + +Make two changes to backend/api/shares.py: + +CHANGE 1 — ShareCreate model (add permission field): +Add `permission: str = "view"` to the ShareCreate model. Add a field_validator named `validate_permission` that checks the value is in `{"view", "edit"}` and raises ValueError otherwise. Import `field_validator` from pydantic if not already imported. + +In the `grant_share` handler, change the hardcoded `permission="view"` in the Share(...) constructor (line 97) to `permission=body.permission`. + +CHANGE 2 — Add SharePermissionPatch model and PATCH endpoint: +Add a new Pydantic model class `SharePermissionPatch(BaseModel)` with a single field `permission: str` and a `field_validator("permission")` classmethod that validates `v in {"view", "edit"}` (same pattern as above). + +Add the PATCH endpoint `@router.patch("/{share_id}", status_code=200)` as `async def update_share_permission(...)`. Place it BEFORE the existing `@router.delete("/{share_id}", ...)` in the file (style consistency; method discrimination makes ordering safe, but before DELETE is conventional). The handler body: +- Parse `share_id` as `uuid.UUID(share_id)`, raising HTTPException(404) on ValueError +- `share = await session.get(Share, sid)` — 404 if None +- IDOR check: `if share is None or share.owner_id != current_user.id: raise HTTPException(404, "Share not found")` — mirrors revoke_share exactly (T-04-04-02) +- `share.permission = body.permission` +- `await session.commit()` +- Return `{"id": str(share.id), "permission": share.permission}` + +Then in backend/tests/test_shares.py, promote the three xfail stubs added in Plan 06.2-01 to real tests. Replace the `pytest.xfail(...)` body with actual test logic following the _make_doc helper pattern and async_client fixture conventions already in the file. + + + cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest tests/test_shares.py::test_share_create_with_permission tests/test_shares.py::test_share_patch_permission tests/test_shares.py::test_share_patch_idor -x -v 2>&1 | tail -20 + + + - `pytest tests/test_shares.py -x -q` exits 0 — all 10 tests pass (7 pre-existing + 3 promoted) + - `grep "class SharePermissionPatch" backend/api/shares.py` returns a match + - `grep "share.owner_id != current_user.id" backend/api/shares.py` returns at least 2 matches (one in revoke_share, one in update_share_permission) + - PATCH /api/shares/{id} with wrong owner returns 404 (confirmed by test_share_patch_idor) + + + + + Task 2: Frontend — is_shared badge fix + permission dropdown + View/Edit toggle + frontend/src/components/documents/DocumentCard.vue, frontend/src/components/sharing/ShareModal.vue, frontend/src/stores/documents.js + + - frontend/src/components/documents/DocumentCard.vue — read lines 25-40 to see the share_count bug at line 31 and surrounding template context + - frontend/src/components/sharing/ShareModal.vue — read the full file; understand the flex gap-2 creation row (lines 31-50), the static "view" span in the recipient list row (line 75), and how handleRevoke uses docsStore + - frontend/src/stores/documents.js — find shareDocument(), revokeShare(), and listShares() methods; understand the request() wrapper used by these methods so updateSharePermission follows the same pattern + - .planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-UI-SPEC.md — Component Contracts C-1 (permission dropdown markup), C-2 (View/Edit toggle markup), Copywriting Contract (label copy) + + +Make three frontend changes: + +CHANGE 1 — DocumentCard.vue line 31 (per D-06): +Change `v-if="doc.share_count > 0"` to `v-if="doc.is_shared"`. This is a one-word change. No other modifications to DocumentCard.vue. + +CHANGE 2 — ShareModal.vue — permission dropdown in creation row (per D-08, C-1 from UI-SPEC): +Add a `permission` reactive ref defaulting to `"view"` in the script setup section. + +In the template, inside the `
` creation row (the row containing the handle input and the "Share document" button), insert a `` and the submit `