--- phase: 05-cloud-storage-backends plan: 10 type: execute wave: 1 depends_on: [] files_modified: - backend/api/cloud.py - frontend/src/components/settings/SettingsCloudTab.vue - frontend/src/components/cloud/CloudCredentialModal.vue - frontend/src/components/ui/ConfirmBlock.vue - backend/tests/test_cloud.py autonomous: true requirements: [CLOUD-01, CLOUD-04] gap_closure: true must_haves: truths: - "Clicking Connect on Google Drive or OneDrive initiates OAuth without a 401" - "Nextcloud custom endpoint is preserved when re-editing an existing connection" - "Edit button appears on ERROR-state Nextcloud/WebDAV rows" - "Disconnect confirmation text renders fully within its container without overflow" artifacts: - path: "backend/api/cloud.py" provides: "oauth_initiate returns JSON {url} (200) instead of RedirectResponse (302)" - path: "frontend/src/components/settings/SettingsCloudTab.vue" provides: "handleConnect uses fetch() + Bearer header; Edit button in ERROR template block" - path: "frontend/src/components/cloud/CloudCredentialModal.vue" provides: "watch handler detects custom endpoint on edit and repopulates showAdvanced + customEndpoint" - path: "frontend/src/components/ui/ConfirmBlock.vue" provides: "break-words on message paragraph" key_links: - from: "frontend/src/components/settings/SettingsCloudTab.vue" to: "/api/cloud/oauth/initiate/{provider}" via: "fetch() with Authorization header → data.url → window.location.href" - from: "backend/api/cloud.py" to: "oauth_initiate handler" via: "returns JSONResponse({url: authorization_url}) status 200" --- Fix four cloud settings UI gaps: OAuth initiate 401, Nextcloud custom endpoint lost on edit, missing Edit button on ERROR rows, and confirmation text overflow. Purpose: Users cannot connect OAuth providers (Google Drive/OneDrive) due to a bare browser navigation that carries no auth header. Nextcloud connections with custom endpoints lose their configuration on re-edit. ERROR-state connections cannot be edited without removing first. Output: OAuth flow initiates correctly, Nextcloud edit round-trips custom endpoint, ERROR rows have Edit button, confirmation text wraps. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/05-cloud-storage-backends/05-UAT.md From backend/api/cloud.py (current oauth_initiate): - Route: GET /api/cloud/oauth/initiate/{provider} - Currently returns: RedirectResponse(url=authorization_url, status_code=302) - Requires: Depends(get_regular_user) - State token stored in Redis; OAuth URL constructed from google/onedrive SDKs - Change: return JSONResponse({"url": authorization_url}) with status 200 From frontend/src/components/settings/SettingsCloudTab.vue (current handleConnect): - OAUTH_PROVIDERS = new Set(['google_drive', 'onedrive']) - Currently: window.location.href = `/api/cloud/oauth/initiate/${provider.key}` - Change: fetch with Authorization header, then window.location.href = data.url From frontend/src/api/client.js: - The `request()` function is JSON-only (calls res.json()). For OAuth initiate, we want JSON back (the {url} object), so `request()` can be used directly. - Export: `initiateOAuth(provider)` → calls request(`/api/cloud/oauth/initiate/${provider}`) From frontend/src/components/cloud/CloudCredentialModal.vue (watch handler, lines 191-209): - Nextcloud edit: extracts only hostname with regex match[1], clears customEndpoint - Bug: if stored server_url does NOT match /remote.php/dav/files/{username}/ pattern, the custom endpoint is silently lost - Fix: compare resolvedServerUrl (auto-constructed) to existing.server_url; if they differ, populate customEndpoint with the stored URL and set showAdvanced=true From frontend/src/components/settings/SettingsCloudTab.vue (ERROR template, lines 88-96): - Only has Remove button - Add Edit button identical to the one in the ACTIVE block (same v-if guard: !OAUTH_PROVIDERS.has(provider.key)) From frontend/src/components/ui/ConfirmBlock.vue: - The message

has class "text-sm text-gray-700" — add "break-words" to it - The wrapper div in SettingsCloudTab.vue (lines 100-113) needs "w-full overflow-hidden" on the outer div Task 1: Fix OAuth initiate — return JSON URL instead of 302 redirect backend/api/cloud.py, backend/tests/test_cloud.py - GET /api/cloud/oauth/initiate/google_drive with valid Bearer token returns 200 JSON {url: "https://accounts.google.com/..."}. - GET /api/cloud/oauth/initiate/onedrive with valid Bearer token returns 200 JSON {url: "https://login.microsoftonline.com/..."}. - GET /api/cloud/oauth/initiate/invalid_provider returns 400 (unchanged). - GET /api/cloud/oauth/initiate/google_drive with no auth returns 401 or 403 (unchanged behavior — get_regular_user enforces this). In backend/api/cloud.py, change the `oauth_initiate` handler: - Remove `response_class=RedirectResponse` from the `@router.get` decorator. - Replace the two `return RedirectResponse(url=authorization_url, status_code=302)` statements (one for google_drive, one for onedrive) with `from fastapi.responses import JSONResponse` (already imported via fastapi) and return `JSONResponse({"url": authorization_url})`. - The state token generation, Redis storage, and authorization URL construction are unchanged. - Update the return type annotation if present. In backend/tests/test_cloud.py, add test `test_oauth_initiate_returns_json_url`: - Creates a regular user + token. - Mocks Redis setex (the app state redis client). - For google_drive provider: mocks `google_auth_oauthlib.flow.Flow.from_client_config` to return a mock whose `authorization_url` returns ("https://accounts.google.com/test", "state123"). - Calls GET /api/cloud/oauth/initiate/google_drive with Bearer header. - Asserts response.status_code == 200. - Asserts response.json()["url"].startswith("https://accounts.google.com/"). - Also adds `test_oauth_initiate_requires_auth`: calls without token, asserts 401 or 403. cd /Users/nik/Documents/Progamming/document_scanner/backend && python -m pytest tests/test_cloud.py::test_oauth_initiate_returns_json_url tests/test_cloud.py::test_oauth_initiate_requires_auth -v Both tests pass. oauth_initiate returns 200 JSON {url}. No RedirectResponse in handler. Task 2: Frontend OAuth fetch, Nextcloud edit fix, Edit on ERROR, text overflow frontend/src/components/settings/SettingsCloudTab.vue, frontend/src/components/cloud/CloudCredentialModal.vue, frontend/src/components/ui/ConfirmBlock.vue, frontend/src/api/client.js ### 1. client.js — add initiateOAuth helper Export `initiateOAuth(provider)` that calls `request(`/api/cloud/oauth/initiate/${provider}`)`. This uses the existing `request()` helper which injects the Bearer header and handles 401 → refresh → retry. ### 2. SettingsCloudTab.vue — fix handleConnect for OAuth providers Replace the current `window.location.href = /api/cloud/oauth/initiate/${provider.key}` line in `handleConnect` with: ``` const data = await initiateOAuth(provider.key) window.location.href = data.url ``` Import `initiateOAuth` from `../../api/client.js`. Wrap in try/catch; on error show a toast or set a reactive `oauthError` ref displayed above the provider list. ### 3. SettingsCloudTab.vue — add Edit button to ERROR template block In the `` template block (currently lines 88-96), add an Edit button before the Remove button, mirroring the ACTIVE block exactly: ```html ``` ### 4. SettingsCloudTab.vue — fix confirmation wrapper overflow Add `w-full overflow-hidden` to the outer `

` that wraps the `` component (the div at the current line starting `v-if="confirmRemoveId === connectionFor(provider.key)?.id"`). This div needs to break out of the flex row — it is already rendered outside the `flex items-center` row as a sibling block, so adding `w-full` and `overflow-hidden` is sufficient. ### 5. CloudCredentialModal.vue — fix Nextcloud edit custom endpoint pre-population In the watch handler on `props.show`, after the existing logic that sets `serverBase.value` by extracting the hostname: - Compute what the auto-constructed URL would be using the extracted hostname and `existing.connection_username`: `const autoUrl = match ? \`${match[1]}/remote.php/dav/files/${encodeURIComponent(existing.connection_username ?? '')}/\` : ''` - If `existing.server_url !== autoUrl` (the stored URL doesn't match the auto-constructed pattern) AND `existing.server_url` has a path beyond just the hostname, then: - Set `showAdvanced.value = true` - Set `customEndpoint.value = existing.server_url` - Otherwise leave `showAdvanced.value = false` and `customEndpoint.value = ''` (existing behavior). ### 6. ConfirmBlock.vue — add break-words to message paragraph Change `

` to `

`. cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5 Frontend build passes with zero errors. All four UI changes are applied: OAuth uses fetch, ERROR rows have Edit button, Nextcloud watch handler preserves custom endpoint, ConfirmBlock message has break-words. ## Trust Boundaries | Boundary | Description | |----------|-------------| | frontend→/api/cloud/oauth/initiate | Now goes through fetch() with Bearer header instead of bare browser navigation | | OAuth URL returned to frontend | URL is generated by the backend OAuth library and stored state in Redis — frontend only receives the URL string | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-05-10-01 | Spoofing | oauth_initiate auth | mitigate | get_regular_user still enforced — only authenticated users receive the OAuth URL | | T-05-10-02 | Information Disclosure | OAuth URL in JSON response | accept | URL is a standard OAuth authorization URL with CSRF state token — no credentials in the URL | | T-05-10-03 | Tampering | OAuth state token | mitigate | State token generated server-side (secrets.token_urlsafe(32)), stored in Redis with TTL 1800, single-use (deleted in callback) — unchanged from original design | | T-05-10-04 | Spoofing | Nextcloud custom endpoint re-edit | accept | Pre-populated values come from encrypted DB credentials decrypted server-side and returned as server_url field — not user-alterable before display | | T-05-10-SC | Tampering | npm/pip installs | mitigate | No new packages installed in this plan | After both tasks complete: - `pytest backend/tests/test_cloud.py::test_oauth_initiate_returns_json_url backend/tests/test_cloud.py::test_oauth_initiate_requires_auth -v` - `npm run build` — zero errors - Manual: click Connect on Google Drive — browser navigates to accounts.google.com (not localhost 401) - Manual: edit Nextcloud connection with custom endpoint — Advanced section opens with endpoint pre-filled - Manual: connection in ERROR state — Edit button is visible - Manual: disconnect confirmation text wraps within its container - oauth_initiate returns 200 JSON {url} (not 302 redirect) - Two new pytest tests pass for OAuth initiate - handleConnect in SettingsCloudTab uses fetch() + initiateOAuth(); no window.location.href to /api path - ERROR status template block has Edit button for non-OAuth providers - CloudCredentialModal watch handler repopulates customEndpoint and showAdvanced when stored URL differs from auto-constructed pattern - ConfirmBlock message paragraph has break-words class - Full frontend build and pytest suite have zero new failures Create `.planning/phases/05-cloud-storage-backends/05-10-SUMMARY.md` when done