docs(05): create phase 5 plan — cloud storage backends (8 plans, 7 waves)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
---
|
||||
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"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/nik/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/Users/nik/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.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
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
<!-- From frontend/src/components/layout/AppSidebar.vue — existing structure -->
|
||||
From AppSidebar.vue: existing Folders collapsible section (foldersExpanded, FolderTreeItem pattern)
|
||||
Pattern: <div class="mt-3"> wrapping a collapsible section header + toggle + content
|
||||
|
||||
<!-- From frontend/src/components/folders/FolderTreeItem.vue (Phase 4) -->
|
||||
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
|
||||
|
||||
<!-- From 05-UI-SPEC.md — Surface 5: Cloud Provider Nodes -->
|
||||
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
|
||||
|
||||
<!-- From frontend/src/stores/cloudConnections.js (Plan 07) -->
|
||||
useCloudConnectionsStore: connections (ref[]), fetchConnections()
|
||||
activeCloudConnections = connections.filter(c => c.status === "ACTIVE")
|
||||
|
||||
<!-- New API function to add to client.js -->
|
||||
getCloudFolders(provider, folderId): GET /api/cloud/folders/{provider}/{folderId}
|
||||
Returns: { items: [{id, name, is_dir, size}, ...] }
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create CloudProviderTreeItem, CloudFolderTreeItem, and add API function</name>
|
||||
<files>
|
||||
frontend/src/components/cloud/CloudProviderTreeItem.vue,
|
||||
frontend/src/components/cloud/CloudFolderTreeItem.vue,
|
||||
frontend/src/api/client.js
|
||||
</files>
|
||||
<read_first>
|
||||
- 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
|
||||
</read_first>
|
||||
<behavior>
|
||||
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}`)
|
||||
}
|
||||
</behavior>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>Both cloud tree components created; getCloudFolders added to API client; Vite build passes</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Add Cloud Storage section to AppSidebar</name>
|
||||
<files>frontend/src/components/layout/AppSidebar.vue</files>
|
||||
<read_first>
|
||||
- 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
|
||||
</read_first>
|
||||
<behavior>
|
||||
- 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')
|
||||
</behavior>
|
||||
<action>
|
||||
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
|
||||
</action>
|
||||
<verify>
|
||||
<automated>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</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- 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
|
||||
</acceptance_criteria>
|
||||
<done>AppSidebar extended with Cloud Storage section; CloudProviderTreeItem renders active connections; Vite build passes</done>
|
||||
</task>
|
||||
|
||||
<task type="checkpoint:human-verify" gate="blocking">
|
||||
<what-built>
|
||||
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
|
||||
</what-built>
|
||||
<how-to-verify>
|
||||
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`
|
||||
</how-to-verify>
|
||||
<resume-signal>Type "approved" after verifying the UI and test suite, or describe any issues found.</resume-signal>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## 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) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
cd /Users/nik/Documents/Progamming/document_scanner && cd backend && pytest -v && cd ../frontend && npm run build 2>&1 | tail -5
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- 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
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/05-cloud-storage-backends/05-08-SUMMARY.md` when done
|
||||
</output>
|
||||
Reference in New Issue
Block a user