diff --git a/.planning/phases/12-cloud-resource-foundation/12-06-SUMMARY.md b/.planning/phases/12-cloud-resource-foundation/12-06-SUMMARY.md new file mode 100644 index 0000000..18fd37b --- /dev/null +++ b/.planning/phases/12-cloud-resource-foundation/12-06-SUMMARY.md @@ -0,0 +1,104 @@ +--- +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