Files
kite/.planning/phases/05-cloud-storage-backends/05-10-PLAN.md
T
curo1305 f006c00d49 docs(05): create UAT gap closure plans 09-11
Three new plans address all 6 diagnosed gaps from 05-UAT.md:

- 05-09: cloud document open (fetch+Blob URL), re-analyze (cloud-aware
  Celery task), and edit (PATCH /api/documents/{id})
- 05-10: OAuth initiate JSON response fix, Nextcloud custom endpoint
  edit round-trip, Edit button on ERROR rows, confirmation text overflow
- 05-11: admin hard-delete with admin-password confirmation (backend
  UserDeleteConfirm model + frontend inline panel)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 10:39:47 +02:00

13 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, gap_closure, must_haves
phase plan type wave depends_on files_modified autonomous requirements gap_closure must_haves
05-cloud-storage-backends 10 execute 1
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
true
CLOUD-01
CLOUD-04
true
truths artifacts key_links
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
path provides
backend/api/cloud.py oauth_initiate returns JSON {url} (200) instead of RedirectResponse (302)
path provides
frontend/src/components/settings/SettingsCloudTab.vue handleConnect uses fetch() + Bearer header; Edit button in ERROR template block
path provides
frontend/src/components/cloud/CloudCredentialModal.vue watch handler detects custom endpoint on edit and repopulates showAdvanced + customEndpoint
path provides
frontend/src/components/ui/ConfirmBlock.vue break-words on message paragraph
from to via
frontend/src/components/settings/SettingsCloudTab.vue /api/cloud/oauth/initiate/{provider} fetch() with Authorization header → data.url → window.location.href
from to via
backend/api/cloud.py oauth_initiate handler 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.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 `<!-- ERROR -->` template block (currently lines 88-96), add an Edit button before the Remove button, mirroring the ACTIVE block exactly:
```html
<button
  v-if="!OAUTH_PROVIDERS.has(provider.key)"
  @click="handleEdit(provider)"
  class="text-sm px-3 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 text-gray-700 transition-colors"
>
  Edit
</button>
```

### 4. SettingsCloudTab.vue — fix confirmation wrapper overflow
Add `w-full overflow-hidden` to the outer `<div>` that wraps the `<ConfirmBlock>` 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 `<p class="text-sm text-gray-700">` to `<p class="text-sm text-gray-700 break-words">`.
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.

<threat_model>

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
</threat_model>
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

<success_criteria>

  • 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 </success_criteria>
Create `.planning/phases/05-cloud-storage-backends/05-10-SUMMARY.md` when done