Files
curo1305 67edc19a36 docs(05): add UAT, UI-SPEC, deferred items, debug notes; refine plans 09-11
Plan refinements: Vitest tests added to 09/10 must-haves, explicit
mock_flow two-tuple pattern in 10, test_admin_api.py fixture usage in 11.
New artifacts: UAT checklist, UI-SPEC, deferred-items, debug investigation
for cloud-doc-operations-fail.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-30 11:57:54 +02:00

15 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
frontend/src/components/settings/__tests__/SettingsCloudTab.test.js
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
handleConnect on OAuth providers calls initiateOAuth and navigates to the returned URL (verified by Vitest)
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
path provides
frontend/src/components/settings/__tests__/SettingsCloudTab.test.js Vitest test asserting handleConnect calls initiateOAuth and sets window.location.href
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 flow object. Set the mock explicitly: `mock_flow.authorization_url.return_value = ("https://accounts.google.com/test", "state123")` — do not use a generic "returns a URL" — this exact two-tuple assignment is required because `flow.authorization_url(...)` returns `(url, state)` and the handler unpacks both values.
- 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 + Vitest test frontend/src/components/settings/SettingsCloudTab.vue, frontend/src/components/cloud/CloudCredentialModal.vue, frontend/src/components/ui/ConfirmBlock.vue, frontend/src/api/client.js, frontend/src/components/settings/__tests__/SettingsCloudTab.test.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">`.

### 7. SettingsCloudTab.test.js — add Vitest test for handleConnect OAuth flow
The file `frontend/src/components/settings/__tests__/SettingsCloudTab.test.js` already exists. Add a new test asserting the corrected OAuth connect flow:
- Mock `initiateOAuth` from `../../../../api/client.js` using `vi.mock`: `vi.mock('../../../../api/client.js', () => ({ initiateOAuth: vi.fn() }))`.
- In the test, set `initiateOAuth.mockResolvedValue({ url: 'https://accounts.google.com/o/oauth2/auth?state=xyz' })`.
- Assign `window.location = { href: '' }` (use `Object.defineProperty` or `vi.stubGlobal` to make `window.location.href` writable in the test environment).
- Mount `SettingsCloudTab` with a stubbed auth store that has a valid accessToken.
- Find a Connect button for a provider with `key === 'google_drive'` and trigger a click event simulating `handleConnect({ key: 'google_drive' })`.
- Assert: `initiateOAuth` was called with `'google_drive'`.
- Assert: `window.location.href` was set to `'https://accounts.google.com/o/oauth2/auth?state=xyz'` (the exact URL returned by the mock — confirming the component navigates to `data.url` and not directly to `/api/cloud/...`).
- Use `afterEach(() => vi.restoreAllMocks())`.
cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run test -- --reporter=verbose --run src/components/settings/__tests__/SettingsCloudTab.test.js 2>&1 | tail -20 && npm run build 2>&1 | tail -5 Vitest test for handleConnect OAuth flow passes. 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 test -- --run src/components/settings/__tests__/SettingsCloudTab.test.js` — Vitest test for handleConnect OAuth flow passes - `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; mock uses explicit mock_flow.authorization_url.return_value = ("https://accounts.google.com/test", "state123") two-tuple
  • handleConnect in SettingsCloudTab uses fetch() + initiateOAuth(); no window.location.href to /api path
  • Vitest test asserts initiateOAuth called with provider key and window.location.href set to returned URL
  • 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