--- phase: "12" plan: "06" subsystem: cloud-navigation tags: [cloud, navigation, multi-account, uuid, security] dependency_graph: requires: ["12-05"] provides: [connection-id-native-lifecycle, uuid-only-navigation, multi-account-settings] affects: [backend/api/cloud/connections.py, frontend/src/components/cloud, frontend/src/components/settings] tech_stack: added: [] patterns: [connection-ID-based-routing, owner-scoped-credential-update, always-insert-pattern] key_files: created: - frontend/src/components/cloud/__tests__/CloudProviderTreeItem.test.js - frontend/src/components/cloud/__tests__/CloudFolderTreeItem.test.js - frontend/src/components/layout/__tests__/AppSidebar.test.js modified: - backend/api/cloud/connections.py - backend/tests/test_cloud.py - frontend/src/api/cloud.js - frontend/src/api/utils.js - frontend/src/components/cloud/CloudCredentialModal.vue - frontend/src/components/cloud/CloudProviderTreeItem.vue - frontend/src/components/cloud/CloudFolderTreeItem.vue - frontend/src/components/layout/AppSidebar.vue - frontend/src/components/settings/SettingsCloudTab.vue - backend/main.py - frontend/package.json decisions: - "Always INSERT new connection rows (never upsert by provider) to allow same-provider multi-account" - "PUT /connections/{id}/credentials preserves existing password when field omitted" - "CloudFolderTreeItem uses connectionId prop instead of provider slug everywhere" - "Cloud Storage sidebar link active only on exact /cloud route" metrics: duration: "~30 minutes" completed: "2026-06-21" tasks_completed: 3 files_changed: 14 --- # Phase 12 Plan 06: Cloud Connection Navigation and Multi-Account Support Summary **One-liner:** UUID-only cloud navigation with same-provider multi-account independence, PUT credential-update endpoint, and route-aware sidebar/breadcrumb selection. ## What Was Built ### Task 1: Connection-ID native lifecycle **Backend:** - Replaced `_upsert_cloud_connection` (upsert by provider) with `_insert_cloud_connection` (always INSERT new UUID row). Two POSTs for Nextcloud now create two independent rows with distinct UUIDs and independent encrypted credentials. - Added `PUT /api/cloud/connections/{id}/credentials` endpoint: owner-scoped (404 on wrong owner), merges submitted fields with existing credentials (password omitted = preserve existing), re-runs `validate_cloud_url` and health-check before committing (T-12-06-04 SSRF gate), audit-logged, credential-free response. - Backend tests: `test_same_provider_connections_are_independent` proves two distinct UUIDs; `test_credential_update_wrong_owner_returns_404` covers T-12-06-01 IDOR. **Frontend:** - `CloudCredentialModal.submit`: calls `updateWebDavCredentials(existing.id, ...)` on edit (targeting the specific UUID), `connectWebDav(...)` only for new connections. - `SettingsCloudTab`: replaced `connectionFor(provider)` (first-match) with `connectionsFor(provider)` (all matches), renders each as an independent named row with its own Rename/Edit/Remove actions, plus an always-visible "Add account" / "Connect" button per provider. - `utils.js request()`: FastAPI validation arrays (`detail: [{loc, msg}]`) now summarised into a concise `field: message` string instead of bare `HTTP 422`. - New API export `updateWebDavCredentials` in `cloud.js`. ### Task 2: UUID-only navigation and sidebar repair - `CloudProviderTreeItem`: uses `getCloudFoldersByConnectionId(connection.id, '')` (never `getCloudFolders(provider, ...)`), routes to `/cloud/{connection.id}/root`, exposes `isActive` computed from exact route prefix match. - `CloudFolderTreeItem`: accepts `connectionId` prop (replaces `provider` slug), uses `getCloudFoldersByConnectionId(connectionId, folder.id)` for children, routes to `/cloud/{connectionId}/{folder.id}`, `isActive` = exact path match. - `AppSidebar`: Cloud Storage nav-link active only on `$route.path === '/cloud'` (exact), not `startsWith` — prevents generic highlight while inside a connection. - `CloudFolderView` was already connection-ID based (prior plan); breadcrumbs already use `connectionRoot` prop. ### Task 3: Full suites, build, version, docs - All 516 backend tests pass (7 xfailed, 17 skipped — pre-existing). - All 334 frontend tests across 42 files pass. - Production build clean (610 ms). - npm audit: 0 vulnerabilities. - Version bumped 0.2.1 → 0.2.2 in `backend/main.py` and `frontend/package.json`. ## Verification Against UAT Failures | UAT Symptom | Fixed by | |---|---| | POST Nextcloud 201 → sidebar browse → 422 (UUID endpoint received provider slug) | CloudProviderTreeItem now passes `connection.id` not `connection.provider` | | Two same-provider connections not possible (upsert merged them) | `_insert_cloud_connection` always creates a new row | | Settings showed only first Nextcloud account | `connectionsFor()` renders all accounts per provider | | Editing one connection could affect another same-provider row | `PUT /connections/{id}/credentials` is owner + ID scoped | | Bare HTTP 422 shown to user on validation failure | `utils.js` now formats FastAPI validation arrays into field messages | ## Deviations from Plan None — plan executed exactly as written. ## Known Stubs None — all cloud navigation uses real connection IDs from API responses. ## Threat Flags None — new `PUT /connections/{id}/credentials` endpoint follows the same security pattern as existing endpoints: owner scope via `_get_owned_connection`, SSRF validation, health-check, credential-free response, audit log. ## Self-Check: PASSED - `backend/api/cloud/connections.py` — modified, `_insert_cloud_connection` present - `PUT /connections/{id}/credentials` endpoint registered in router - `frontend/src/components/cloud/CloudProviderTreeItem.vue` — uses `getCloudFoldersByConnectionId` - `frontend/src/components/cloud/CloudFolderTreeItem.vue` — uses `connectionId` prop - All commits verified: 731b65e, 057b499, c85e4ab