747303246a
Folders, Sharing, Quotas & Document UX — plans verified (0 blockers, 2 non-blocking warnings). Covers FOLD-01..05, SHARE-01..05, SEC-08/09, ADMIN-06, DOC-01/02. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
244 lines
14 KiB
Markdown
244 lines
14 KiB
Markdown
---
|
|
phase: 04-folders-sharing-quotas-document-ux
|
|
plan: 08
|
|
type: execute
|
|
wave: 6
|
|
depends_on:
|
|
- "04-07"
|
|
files_modified:
|
|
- frontend/src/api/client.js
|
|
- frontend/src/stores/folders.js
|
|
- frontend/src/stores/documents.js
|
|
- frontend/src/router/index.js
|
|
autonomous: true
|
|
requirements:
|
|
- FOLD-01
|
|
- FOLD-02
|
|
- FOLD-03
|
|
- FOLD-04
|
|
- FOLD-05
|
|
- SHARE-01
|
|
- SHARE-02
|
|
- SHARE-03
|
|
- SHARE-04
|
|
- SHARE-05
|
|
- DOC-01
|
|
- DOC-02
|
|
|
|
must_haves:
|
|
truths:
|
|
- "All backend API endpoints have matching client.js wrapper functions"
|
|
- "useFoldersStore exposes state and actions for folder CRUD and navigation"
|
|
- "useDocumentsStore extended with search, sort, folderId, shareDocument, revokeShare, listShares"
|
|
- "Vue Router has routes for /folders/:folderId and /shared"
|
|
- "Access token injected via existing request() helper — no new auth logic added"
|
|
artifacts:
|
|
- path: "frontend/src/api/client.js"
|
|
provides: "listFolders, createFolder, renameFolder, deleteFolder, moveDocument, createShare, deleteShare, listShares, getSharedWithMe, getMyPreferences, updateMyPreferences, getDocumentContentUrl"
|
|
- path: "frontend/src/stores/folders.js"
|
|
provides: "useFoldersStore: folders, currentFolderId, breadcrumb, loading, error + folder CRUD actions"
|
|
- path: "frontend/src/stores/documents.js"
|
|
provides: "extended with folderId, searchQuery, sort, order state + share actions"
|
|
- path: "frontend/src/router/index.js"
|
|
provides: "/folders/:folderId route + /shared route"
|
|
key_links:
|
|
- from: "frontend/src/stores/folders.js"
|
|
to: "frontend/src/api/client.js"
|
|
via: "all folder CRUD actions call api.* functions"
|
|
pattern: "import.*api"
|
|
- from: "frontend/src/router/index.js"
|
|
to: "frontend/src/views/FolderView.vue"
|
|
via: "route /folders/:folderId component lazy-loaded"
|
|
pattern: "FolderView"
|
|
---
|
|
|
|
<objective>
|
|
Wire the frontend data layer: API client functions, Pinia stores, and router routes for all
|
|
Phase 4 backend endpoints. This plan creates no UI components — those are in plan 04-09.
|
|
Stores and API client are the contracts the UI components depend on.
|
|
|
|
Purpose: Establish frontend data contracts before writing view and component code.
|
|
Output: client.js additions + folders store + extended documents store + new routes.
|
|
</objective>
|
|
|
|
<execution_context>
|
|
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
|
@$HOME/.claude/get-shit-done/templates/summary.md
|
|
</execution_context>
|
|
|
|
<context>
|
|
@.planning/phases/04-folders-sharing-quotas-document-ux/04-CONTEXT.md
|
|
@.planning/phases/04-folders-sharing-quotas-document-ux/04-PATTERNS.md
|
|
@.planning/phases/04-folders-sharing-quotas-document-ux/04-UI-SPEC.md
|
|
@frontend/src/api/client.js
|
|
@frontend/src/stores/documents.js
|
|
@frontend/src/stores/topics.js
|
|
@frontend/src/router/index.js
|
|
</context>
|
|
|
|
<interfaces>
|
|
From frontend/src/api/client.js: read actual file to confirm request() signature.
|
|
Pattern: all exported functions call the internal request() helper which injects
|
|
the access token from the auth store and handles 401 refresh.
|
|
|
|
From frontend/src/stores/topics.js: full defineStore pattern to replicate.
|
|
State refs: typed refs, loading, error. Actions: async try/catch/finally pattern.
|
|
|
|
From frontend/src/router/index.js: read to find the route array and lazy-import
|
|
pattern for view components (e.g., () => import('../views/SomeView.vue')).
|
|
</interfaces>
|
|
|
|
<tasks>
|
|
|
|
<task type="auto">
|
|
<name>Task 1: Add Phase 4 API functions to client.js and create folders store</name>
|
|
<files>frontend/src/api/client.js, frontend/src/stores/folders.js</files>
|
|
<read_first>
|
|
frontend/src/api/client.js — read the ENTIRE file; identify the request() helper signature; find the last exported function; note if any listFolders or share functions already exist
|
|
frontend/src/stores/topics.js — read ENTIRE file for the exact defineStore + ref + async action pattern
|
|
frontend/src/stores/documents.js — read lines 1-30 to confirm existing imports
|
|
</read_first>
|
|
<action>
|
|
Modify frontend/src/api/client.js: APPEND these named export functions after existing functions. Follow the existing named-export style exactly.
|
|
|
|
Folder API: listFolders(parentId = null) → GET /api/folders with optional ?parent_id= query param; createFolder(name, parentId = null) → POST /api/folders with JSON body {name, parent_id: parentId || null}; getFolder(folderId) → GET /api/folders/{folderId}; renameFolder(folderId, name) → PATCH /api/folders/{folderId} with JSON {name}; deleteFolder(folderId) → DELETE /api/folders/{folderId}; moveDocument(docId, folderId) → PATCH /api/documents/{docId}/folder with JSON {folder_id: folderId || null}.
|
|
|
|
Share API: createShare(docId, recipientHandle) → POST /api/shares with JSON {document_id: docId, recipient_handle: recipientHandle}; listShares(docId) → GET /api/shares?document_id={docId}; deleteShare(shareId) → DELETE /api/shares/{shareId}; getSharedWithMe() → GET /api/shares/received.
|
|
|
|
Preference API: getMyPreferences() → GET /api/auth/me/preferences; updateMyPreferences(payload) → PATCH /api/auth/me/preferences with JSON payload.
|
|
|
|
PDF proxy URL helper: getDocumentContentUrl(docId) → returns the string `/api/documents/${docId}/content` — pure string, no fetch. Used for iframe src or window.open.
|
|
|
|
For listDocuments (the existing function): read its current signature. If it does not already accept folderId, q, sort, order params (added in plan 04-03), update it to pass these as query params via URLSearchParams, including params only when non-null and non-empty.
|
|
|
|
Create frontend/src/stores/folders.js as a NEW file. Follow the exact useTopicsStore pattern.
|
|
Imports: `import { defineStore } from 'pinia'`, `import { ref } from 'vue'`, `import * as api from '../api/client.js'`.
|
|
State refs: folders (ref([])), currentFolderId (ref(null)), breadcrumb (ref([])), loading (ref(false)), error (ref(null)).
|
|
Actions:
|
|
- fetchFolders(parentId = null): calls api.listFolders(parentId); sets folders.value
|
|
- createFolder(name, parentId = null): calls api.createFolder(name, parentId); pushes result to folders.value
|
|
- renameFolder(folderId, name): calls api.renameFolder; updates matching entry in folders.value
|
|
- deleteFolder(folderId): calls api.deleteFolder; removes from folders.value
|
|
- navigateTo(folderId): sets currentFolderId.value = folderId; if folderId is not null, calls api.getFolder(folderId) and sets breadcrumb.value = response.breadcrumb; if folderId is null, sets breadcrumb.value = []
|
|
All actions follow the loading/error/finally pattern from topics store.
|
|
Export: `export const useFoldersStore = defineStore('folders', () => { ... return { folders, currentFolderId, breadcrumb, loading, error, fetchFolders, createFolder, renameFolder, deleteFolder, navigateTo } })`.
|
|
</action>
|
|
<verify>
|
|
Run: grep -n "listFolders\|createShare\|getMyPreferences\|getDocumentContentUrl\|getSharedWithMe" /Users/nik/Documents/Progamming/document_scanner/frontend/src/api/client.js
|
|
|
|
Expected: all five function names appear in the grep output.
|
|
|
|
Also: grep -n "useFoldersStore\|navigateTo\|currentFolderId" /Users/nik/Documents/Progamming/document_scanner/frontend/src/stores/folders.js
|
|
|
|
Expected: all three identifiers appear.
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- frontend/src/api/client.js contains: listFolders, createFolder, getFolder, renameFolder, deleteFolder, moveDocument, createShare, listShares, deleteShare, getSharedWithMe, getMyPreferences, updateMyPreferences, getDocumentContentUrl (13 new functions — grep confirms each name)
|
|
- frontend/src/stores/folders.js exists and exports useFoldersStore
|
|
- useFoldersStore returns: folders, currentFolderId, breadcrumb, loading, error, fetchFolders, createFolder, renameFolder, deleteFolder, navigateTo
|
|
- listDocuments in client.js accepts folderId, q, sort, order params
|
|
- No existing API functions are modified (only appended to)
|
|
</acceptance_criteria>
|
|
<done>client.js has all Phase 4 API wrappers; folders store is created and exported.</done>
|
|
</task>
|
|
|
|
<task type="auto">
|
|
<name>Task 2: Extend documents store with folder/search/share actions + add Vue Router routes</name>
|
|
<files>frontend/src/stores/documents.js, frontend/src/router/index.js</files>
|
|
<read_first>
|
|
frontend/src/stores/documents.js — read the ENTIRE file; identify fetchDocuments signature; find existing actions (upload, remove, etc.); find the return statement at the end to know what to add to it
|
|
frontend/src/router/index.js — read the ENTIRE file; identify the routes array; find the lazy import pattern; note auth guard usage (requiresAuth meta, beforeEach hook) to replicate for new routes
|
|
</read_first>
|
|
<action>
|
|
Modify frontend/src/stores/documents.js:
|
|
|
|
Add new refs (insert after existing state refs, before existing actions):
|
|
- currentFolderId: ref(null)
|
|
- searchQuery: ref('')
|
|
- sortField: ref('date') — 'name' | 'date' | 'size'
|
|
- sortOrder: ref('desc') — 'asc' | 'desc'
|
|
|
|
Add debounced search watcher (RESEARCH.md Pattern 10 — no lodash):
|
|
After existing watch declarations (or before return), add:
|
|
let _searchTimer = null
|
|
watch(searchQuery, (newVal) => {
|
|
clearTimeout(_searchTimer)
|
|
if (newVal.length < 2) { fetchDocuments({ folderId: currentFolderId.value }); return }
|
|
_searchTimer = setTimeout(() => { fetchDocuments({ q: newVal, folderId: currentFolderId.value, sort: sortField.value, order: sortOrder.value }) }, 300)
|
|
})
|
|
|
|
Update fetchDocuments() to accept and pass {folderId, q, sort, order} params to api.listDocuments().
|
|
|
|
Add new actions (append before return statement):
|
|
- shareDocument(docId, recipientHandle): try { return await api.createShare(docId, recipientHandle) } catch (e) { throw e }
|
|
- revokeShare(shareId): try { await api.deleteShare(shareId) } catch (e) { throw e }
|
|
- listShares(docId): try { return await api.listShares(docId) } catch (e) { throw e }
|
|
|
|
Add new state/actions to the return statement: currentFolderId, searchQuery, sortField, sortOrder, shareDocument, revokeShare, listShares.
|
|
|
|
Modify frontend/src/router/index.js:
|
|
Add two new route objects to the routes array. Follow the exact lazy-import pattern from existing routes. Apply the same requiresAuth guard as existing protected routes.
|
|
|
|
Route 1: { path: '/folders/:folderId', name: 'folder', component: () => import('../views/FolderView.vue'), meta: { requiresAuth: true } }
|
|
Route 2: { path: '/shared', name: 'shared', component: () => import('../views/SharedView.vue'), meta: { requiresAuth: true } }
|
|
|
|
Note: FolderView.vue and SharedView.vue do not exist yet (created in plan 04-09). The router import is lazy-loaded so missing files do not cause import errors at router init — only at navigation time.
|
|
</action>
|
|
<verify>
|
|
Run: grep -n "currentFolderId\|searchQuery\|sortField\|shareDocument\|revokeShare" /Users/nik/Documents/Progamming/document_scanner/frontend/src/stores/documents.js
|
|
|
|
Expected: all five identifiers appear.
|
|
|
|
Also: grep -n "FolderView\|SharedView\|/folders\|/shared" /Users/nik/Documents/Progamming/document_scanner/frontend/src/router/index.js
|
|
|
|
Expected: both view names and both paths appear.
|
|
</verify>
|
|
<acceptance_criteria>
|
|
- documents.js contains: currentFolderId ref, searchQuery ref, sortField ref, sortOrder ref
|
|
- documents.js contains debounced watch on searchQuery with 300ms timeout and 2-char minimum
|
|
- documents.js return statement includes: currentFolderId, searchQuery, sortField, sortOrder, shareDocument, revokeShare, listShares
|
|
- router/index.js contains /folders/:folderId route and /shared route
|
|
- Both new routes use lazy component import and requiresAuth meta (grep confirms)
|
|
- No existing routes modified (only new routes appended)
|
|
</acceptance_criteria>
|
|
<done>Documents store extended with folder/search/share state; router has two new protected routes.</done>
|
|
</task>
|
|
|
|
</tasks>
|
|
|
|
<threat_model>
|
|
## Trust Boundaries
|
|
|
|
| Boundary | Description |
|
|
|----------|-------------|
|
|
| Frontend store → backend API | All requests go through request() helper which injects JWT; no auth logic duplicated in new stores |
|
|
| Vue Router → view components | New routes use same requiresAuth guard as existing protected routes |
|
|
|
|
## STRIDE Threat Register
|
|
|
|
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
|
|-----------|----------|-----------|-------------|-----------------|
|
|
| T-04-08-01 | Broken Access Control | /folders/:folderId route without auth guard | mitigate | requiresAuth: true meta applied to both new routes; same router beforeEach guard as existing protected routes |
|
|
| T-04-08-02 | Information Disclosure | Access token stored in localStorage via new store | accept | All token handling is in existing auth store + request() helper; folders store and documents store extension do not touch auth state |
|
|
| T-04-08-03 | Tampering | Debounced search fires before 2-char minimum | mitigate | searchQuery watch checks newVal.length < 2 before firing API call; short inputs restore full list without API call |
|
|
| T-04-SC | Tampering | npm/pip/cargo installs | accept | No new packages installed in this plan |
|
|
</threat_model>
|
|
|
|
<verification>
|
|
1. API functions: grep -c "export function" /Users/nik/Documents/Progamming/document_scanner/frontend/src/api/client.js
|
|
2. Folders store: grep -n "useFoldersStore\|navigateTo\|breadcrumb" /Users/nik/Documents/Progamming/document_scanner/frontend/src/stores/folders.js
|
|
3. Documents store: grep -n "currentFolderId\|shareDocument\|searchQuery" /Users/nik/Documents/Progamming/document_scanner/frontend/src/stores/documents.js
|
|
4. Router: grep -n "FolderView\|SharedView" /Users/nik/Documents/Progamming/document_scanner/frontend/src/router/index.js
|
|
</verification>
|
|
|
|
<success_criteria>
|
|
- All 13 new API functions exist in client.js (verified by grep)
|
|
- useFoldersStore is created and exports folder CRUD actions + navigation state
|
|
- documents store return includes all new state/action exports
|
|
- Vue Router has /folders/:folderId and /shared routes with requiresAuth guard
|
|
</success_criteria>
|
|
|
|
<output>
|
|
Create `.planning/phases/04-folders-sharing-quotas-document-ux/04-08-SUMMARY.md` when done.
|
|
</output>
|