--- 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 `