--- phase: 05-cloud-storage-backends plan: 08 type: execute wave: 7 depends_on: - "05-07" files_modified: - frontend/src/components/layout/AppSidebar.vue - frontend/src/components/cloud/CloudProviderTreeItem.vue - frontend/src/components/cloud/CloudFolderTreeItem.vue autonomous: false requirements: - CLOUD-03 - CLOUD-04 user_setup: - service: google_oauth_app why: "Google Drive OAuth integration requires a GCP app with OAuth credentials" env_vars: - name: GOOGLE_CLIENT_ID source: "GCP Console → APIs & Services → Credentials → OAuth 2.0 Client IDs" - name: GOOGLE_CLIENT_SECRET source: "GCP Console → APIs & Services → Credentials → OAuth 2.0 Client IDs → client secret" dashboard_config: - task: "Enable Google Drive API" location: "GCP Console → APIs & Services → Enable APIs → Google Drive API" - task: "Add redirect URI" location: "GCP Console → OAuth 2.0 Client → Authorized redirect URIs → add: {BACKEND_URL}/api/cloud/oauth/callback/google_drive" - service: onedrive_app_registration why: "OneDrive OAuth requires an Azure App Registration" env_vars: - name: ONEDRIVE_CLIENT_ID source: "Azure Portal → App registrations → {app} → Application (client) ID" - name: ONEDRIVE_CLIENT_SECRET source: "Azure Portal → App registrations → {app} → Certificates & secrets → New client secret" - name: ONEDRIVE_TENANT_ID source: "Azure Portal → App registrations → {app} → Directory (tenant) ID (or use 'common')" dashboard_config: - task: "Register application" location: "Azure Portal → Azure Active Directory → App registrations → New registration" - task: "Add redirect URI" location: "Azure Portal → App registrations → {app} → Authentication → Add redirect URI → {BACKEND_URL}/api/cloud/oauth/callback/onedrive" - task: "Add Files.ReadWrite and offline_access API permissions" location: "Azure Portal → App registrations → {app} → API permissions → Add permission → Microsoft Graph" - service: cloud_creds_key why: "HKDF master key for encrypting cloud credentials — must be 32 random bytes" env_vars: - name: CLOUD_CREDS_KEY source: "Generate with: python -c \"import secrets; print(secrets.token_hex(32))\"" must_haves: truths: - "AppSidebar has a 'Cloud Storage' collapsible section below Folders, above Topics" - "Each ACTIVE cloud connection appears as a CloudProviderTreeItem in the sidebar" - "Expanding a cloud provider node lazy-loads the first level of cloud folders via GET /api/cloud/folders/{provider}/root" - "CloudFolderTreeItem renders nested cloud sub-folders with lazy-load expand" - "Cloud nodes not shown in sidebar for REQUIRES_REAUTH or ERROR status connections (only ACTIVE)" - "Human checkpoint: user verifies cloud section appears in sidebar and can expand a provider node" artifacts: - path: "frontend/src/components/layout/AppSidebar.vue" provides: "Sidebar with Cloud Storage collapsible section" contains: "CloudProviderTreeItem" - path: "frontend/src/components/cloud/CloudProviderTreeItem.vue" provides: "Provider root node in sidebar tree" contains: "class CloudProviderTreeItem" - path: "frontend/src/components/cloud/CloudFolderTreeItem.vue" provides: "Cloud sub-folder node" contains: "class CloudFolderTreeItem" key_links: - from: "frontend/src/components/layout/AppSidebar.vue" to: "frontend/src/stores/cloudConnections.js" via: "useCloudConnectionsStore for activeCloudConnections" pattern: "useCloudConnectionsStore" - from: "frontend/src/components/cloud/CloudProviderTreeItem.vue" to: "frontend/src/api/client.js" via: "GET /api/cloud/folders/{provider}/{folder_id}" pattern: "getCloudFolders" --- Add the Cloud Storage section to AppSidebar and create the CloudProviderTreeItem and CloudFolderTreeItem components for lazy-loading cloud folder trees. Purpose: Complete the sidebar integration so users can navigate cloud storage alongside local folders. Human checkpoint verifies the UI renders correctly. Output: AppSidebar extended with cloud section; CloudProviderTreeItem; CloudFolderTreeItem. @/Users/nik/.claude/get-shit-done/workflows/execute-plan.md @/Users/nik/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/05-cloud-storage-backends/05-CONTEXT.md @.planning/phases/05-cloud-storage-backends/05-UI-SPEC.md @.planning/phases/05-cloud-storage-backends/05-07-SUMMARY.md From AppSidebar.vue: existing Folders collapsible section (foldersExpanded, FolderTreeItem pattern) Pattern:
wrapping a collapsible section header + toggle + content FolderTreeItem: depth prop, toggle expand on arrow click, navigate on name click Pattern: paddingLeft = depth * 12 + 'px', rotate-90 class on expand, lazy-load children Exact markup for cloud section header, cloudExpanded toggle, CloudProviderTreeItem rendering providerIconColor: google_drive=text-blue-500, onedrive=text-sky-500, nextcloud=text-orange-500, webdav=text-gray-500 depth * 12 px left padding formula useCloudConnectionsStore: connections (ref[]), fetchConnections() activeCloudConnections = connections.filter(c => c.status === "ACTIVE") getCloudFolders(provider, folderId): GET /api/cloud/folders/{provider}/{folderId} Returns: { items: [{id, name, is_dir, size}, ...] } Task 1: Create CloudProviderTreeItem, CloudFolderTreeItem, and add API function frontend/src/components/cloud/CloudProviderTreeItem.vue, frontend/src/components/cloud/CloudFolderTreeItem.vue, frontend/src/api/client.js - frontend/src/components/folders/FolderTreeItem.vue — lazy-load tree item pattern (expand toggle, depth padding, children loading) - frontend/src/api/client.js — request() pattern for new getCloudFolders function - .planning/phases/05-cloud-storage-backends/05-UI-SPEC.md — Surface 5 exact component markup CloudProviderTreeItem.vue: - Props: connection (Object: {id, provider, display_name, status}), depth (Number, default 1) - Local state: expanded (ref false), children (ref []), loading (ref false), loadError (ref false) - On toggle expand: if !expanded and children.length==0, fetch via api.getCloudFolders(connection.provider, 'root'); set loading during fetch; set loadError on error - On retry click (load error state): re-fetch children - providerIconColor computed from connection.provider (map per UI-SPEC) - navigate to cloud folder root on name click — emit 'navigate' or use router.push('/cloud/{provider}/root') - Renders CloudFolderTreeItem for each child CloudFolderTreeItem.vue: - Props: folder (Object: {id, name, is_dir, size}), provider (String), depth (Number) - Local state: expanded (ref false), children (ref []), loading (ref false), loadError (ref false) - Only renders expand arrow if folder.is_dir === true - On toggle expand: fetch api.getCloudFolders(provider, folder.id); same loading/error pattern - Indentation: depth * 12 px - Navigate to /cloud/{provider}/{folder.id} on click (router.push) Add to frontend/src/api/client.js (append after disconnectCloud/connectWebDav): export function getCloudFolders(provider, folderId) { return request(`/api/cloud/folders/${provider}/${folderId}`) } First: append getCloudFolders to frontend/src/api/client.js (after updateDefaultStorage or at end of cloud section). Create frontend/src/components/cloud/CloudProviderTreeItem.vue following the UI-SPEC Surface 5 exact markup: - Template mirrors FolderTreeItem structure: expand arrow button + name button - Expand arrow: svg chevron, rotate-90 when expanded - Provider name button: uses providerIconColor, active/hover classes per UI-SPEC - Loading state: text-xs text-gray-400 "Loading…" at pl-12 - Error state: text-xs text-red-500 "Failed to load — tap to retry" with @click=retry - Children loop: CloudFolderTreeItem for each child in children - paddingLeft style: `${depth * 12}px` Create frontend/src/components/cloud/CloudFolderTreeItem.vue: - Simpler than CloudProviderTreeItem: folder icon (text-gray-400), name, expand arrow if is_dir - Same loading/error pattern as CloudProviderTreeItem - Navigate via router.push on name click - Recursively renders CloudFolderTreeItem for nested children - paddingLeft style: `${depth * 12}px` Both components use Options API (consistent with existing Phase 4 components) or Composition API with script setup — match the style used in FolderTreeItem.vue (whichever pattern it uses). cd /Users/nik/Documents/Progamming/document_scanner/frontend && node -e " const fs = require('fs'); ['src/components/cloud/CloudProviderTreeItem.vue', 'src/components/cloud/CloudFolderTreeItem.vue'].forEach(f => { if (!fs.existsSync(f)) throw new Error('Missing: ' + f); const c = fs.readFileSync(f, 'utf8'); console.log('EXISTS OK:', f); }); const api = fs.readFileSync('src/api/client.js', 'utf8'); if (!api.includes('getCloudFolders')) throw new Error('Missing getCloudFolders in client.js'); console.log('OK: getCloudFolders in client.js'); " && npm --prefix /Users/nik/Documents/Progamming/document_scanner/frontend run build 2>&1 | tail -5 - CloudProviderTreeItem.vue exists; contains providerIconColor logic and CloudFolderTreeItem usage - CloudFolderTreeItem.vue exists; contains expand arrow, loading state, and recursive CloudFolderTreeItem - client.js contains getCloudFolders function calling /api/cloud/folders/{provider}/{folderId} - `npm run build` exits 0 Both cloud tree components created; getCloudFolders added to API client; Vite build passes Task 2: Add Cloud Storage section to AppSidebar frontend/src/components/layout/AppSidebar.vue - frontend/src/components/layout/AppSidebar.vue — current structure; find the Folders section and Topics section; insert cloud section between them - frontend/src/stores/cloudConnections.js — useCloudConnectionsStore (created in Plan 07) - frontend/src/components/cloud/CloudProviderTreeItem.vue — component to render - .planning/phases/05-cloud-storage-backends/05-UI-SPEC.md — Surface 5 exact AppSidebar markup - AppSidebar gains a "Cloud Storage" collapsible section placed after the Folders section closing div and before the Topics section - Section uses cloudExpanded ref (default true — expanded by default for discoverability) - Section header: cloud icon (text-sky-500) + "Cloud Storage" label — clicking navigates to /settings (plain href="/settings") - Expand/collapse chevron: same pattern as Folders section - When expanded: renders one CloudProviderTreeItem per ACTIVE connection - When no ACTIVE connections: "No cloud storage connected" text at pl-7 text-xs text-gray-400 - While loading: "Loading…" text at pl-7 text-xs text-gray-400 - useCloudConnectionsStore called in AppSidebar; connections fetched on component mount (if not already fetched by SettingsView) - activeCloudConnections computed: connections.filter(c => c.status === 'ACTIVE') Read AppSidebar.vue fully to find insertion point (after Folders section closing div, before Topics section). Import in script section: import CloudProviderTreeItem from '../cloud/CloudProviderTreeItem.vue' import { useCloudConnectionsStore } from '../../stores/cloudConnections.js' Add to reactive data / setup: cloudExpanded = ref(true) (or data() equivalent) cloudConnectionsStore = useCloudConnectionsStore() Add computed: activeCloudConnections: return cloudConnectionsStore.connections.filter(c => c.status === 'ACTIVE') loadingCloudConnections: return cloudConnectionsStore.loading In onMounted (or mounted lifecycle): cloudConnectionsStore.fetchConnections() Insert cloud section template per UI-SPEC Surface 5 exact markup: - Section header with cloud icon (SVG cloud path d="M3 15a4 4 0 004 4h9a5 5 0 10-.1-9.999 5.002 5.002 0 10-9.78 2.096A4.001 4.001 0 003 15z") - class="w-4 h-4 mr-2 shrink-0 text-sky-500" on cloud SVG - a href="/settings" with nav-link class for "Cloud Storage" label - CloudProviderTreeItem v-for over activeCloudConnections - Loading and empty state text per behavior spec cd /Users/nik/Documents/Progamming/document_scanner/frontend && node -e " const fs = require('fs'); const sidebar = fs.readFileSync('src/components/layout/AppSidebar.vue', 'utf8'); if (!sidebar.includes('CloudProviderTreeItem')) throw new Error('Missing CloudProviderTreeItem in AppSidebar'); if (!sidebar.includes('cloudExpanded')) throw new Error('Missing cloudExpanded ref'); if (!sidebar.includes('useCloudConnectionsStore')) throw new Error('Missing cloudConnectionsStore'); if (!sidebar.includes('Cloud Storage')) throw new Error('Missing Cloud Storage section label'); console.log('AppSidebar cloud section: OK'); " && npm --prefix /Users/nik/Documents/Progamming/document_scanner/frontend run build 2>&1 | tail -5 - AppSidebar.vue contains CloudProviderTreeItem import and usage - AppSidebar.vue contains cloudExpanded ref and cloud section template - AppSidebar.vue contains useCloudConnectionsStore import and fetchConnections call - "Cloud Storage" label present in sidebar template - `npm run build` exits 0, 0 errors - Existing Folders and Topics sections in sidebar are unmodified AppSidebar extended with Cloud Storage section; CloudProviderTreeItem renders active connections; Vite build passes Phase 5 is now fully implemented: - 4 cloud storage backends (Google Drive, OneDrive, Nextcloud, WebDAV) via StorageBackend ABC - HKDF per-user credential encryption (CLOUD_CREDS_KEY master key) - SSRF prevention on WebDAV/Nextcloud user-supplied URLs - OAuth flow: initiate → provider consent → callback → encrypt+save → redirect to /settings?cloud_connected= - Cloud Storage tab in SettingsView: all 4 providers with status badges, connect/disconnect actions - WebDAV/Nextcloud credential modal with app-password recommendation - Cloud Storage section in AppSidebar: lazy-load folder tree per connected provider - Cloud upload routing through FastAPI; cloud content proxy via existing /api/documents/{id}/content - All 15 Phase 5 tests passing 1. Start the stack: `docker compose up` — verify no startup errors 2. Run backend tests: `cd backend && pytest -v` — verify zero failures 3. Start frontend: `cd frontend && npm run dev` 4. Open http://localhost:5173 and log in 5. Navigate to Settings → Cloud Storage tab - Verify: all 4 providers (Google Drive, OneDrive, Nextcloud, WebDAV server) visible with "Not connected" badges - Verify: "Connect Google Drive" button is indigo 6. Test WebDAV connect with invalid URL: - Click "Connect WebDAV server" → modal opens - Enter server URL: http://192.168.1.1/dav, username: test, password: test - Click "Connect WebDAV server" button - Verify: connection fails with "Connection failed" error (SSRF blocked or connection refused) 7. Check sidebar: - Verify: "Cloud Storage" collapsible section appears in sidebar below Folders - When no connections: section shows "No cloud storage connected" 8. (Optional — requires real credentials): Connect Nextcloud with valid credentials - Verify: connection saves with ACTIVE status badge in Settings - Verify: provider appears as tree node in sidebar - Verify: expanding provider node shows cloud folders (or "Empty") 9. Test REQUIRES_REAUTH via DB (optional): - Run: `docker exec -it document_scanner-postgres-1 psql -U docuvault_app docuvault -c "UPDATE cloud_connections SET status='REQUIRES_REAUTH' WHERE true;"` - Reload Settings → Cloud Storage tab - Verify: yellow "Reconnect needed" badge and "Reconnect {provider}" button visible 10. Run security gates: `cd backend && bandit -r . -x ./tests/ 2>&1 | grep -E "HIGH|CRITICAL"` `cd backend && pip audit` `cd frontend && npm audit --audit-level=high` Type "approved" after verifying the UI and test suite, or describe any issues found. ## Trust Boundaries | Boundary | Description | |----------|-------------| | Sidebar → /api/cloud/folders | Cloud folder listings loaded via authenticated API; no direct provider calls from browser | | window.location.href → /api/cloud/oauth/initiate | OAuth redirect is a browser navigation — no token in JavaScript | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-05-08-01 | Information Disclosure | CloudProviderTreeItem — folder names in DOM | accept | Folder names are user's own cloud content; displayed only to authenticated user; no PII or credentials | | T-05-08-02 | Denial of Service | Sidebar fetch on mount | mitigate | fetchConnections called once on AppSidebar mount; TTLCache on server prevents repeated API calls for folder listings within 60s | | T-05-08-03 | Spoofing | CloudFolderTreeItem folder navigation URL | accept | Route /cloud/{provider}/{folder_id} uses folder_id from API response; never from user-typed input | | T-05-08-04 | Information Disclosure | AppSidebar shows ACTIVE connections | mitigate | Only ACTIVE connections shown; REQUIRES_REAUTH/ERROR hidden from sidebar (user directed to Settings to resolve) | cd /Users/nik/Documents/Progamming/document_scanner && cd backend && pytest -v && cd ../frontend && npm run build 2>&1 | tail -5 - CloudProviderTreeItem.vue: provider icon colors, expand/collapse, lazy-load children, loading/error states - CloudFolderTreeItem.vue: folder icon, is_dir expand, lazy-load nested, depth padding - AppSidebar.vue: Cloud Storage section after Folders; cloudExpanded; CloudProviderTreeItem v-for over ACTIVE connections - Vite build passes with 0 errors - pytest -v (backend): 0 failures - Human checkpoint: user confirms cloud section visible in sidebar; WebDAV SSRF rejection works; tests pass Create `.planning/phases/05-cloud-storage-backends/05-08-SUMMARY.md` when done