` 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
+
+
+
diff --git a/.planning/phases/05-cloud-storage-backends/05-11-PLAN.md b/.planning/phases/05-cloud-storage-backends/05-11-PLAN.md
new file mode 100644
index 0000000..c62e282
--- /dev/null
+++ b/.planning/phases/05-cloud-storage-backends/05-11-PLAN.md
@@ -0,0 +1,265 @@
+---
+phase: 05-cloud-storage-backends
+plan: 11
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - backend/api/admin.py
+ - frontend/src/api/client.js
+ - frontend/src/components/admin/AdminUsersTab.vue
+ - backend/tests/test_admin.py
+autonomous: true
+requirements: [ADMIN-02, SEC-09]
+gap_closure: true
+
+must_haves:
+ truths:
+ - "Admin can permanently delete a non-admin user after entering their own admin password"
+ - "Backend verifies the admin password before executing the delete"
+ - "Delete purges cloud connections + MinIO objects + all DB rows (existing SEC-09 code runs)"
+ - "Frontend presents an inline confirmation panel with admin password field before calling DELETE"
+ - "Incorrect admin password returns 403 without deleting the user"
+ artifacts:
+ - path: "backend/api/admin.py"
+ provides: "UserDeleteConfirm Pydantic model; delete_user handler reads admin_password from body and verifies it"
+ - path: "frontend/src/api/client.js"
+ provides: "adminDeleteUser(id, adminPassword) calling DELETE /api/admin/users/{id}"
+ - path: "frontend/src/components/admin/AdminUsersTab.vue"
+ provides: "Inline delete confirmation panel with admin password field, mirroring confirmDeactivate pattern"
+ key_links:
+ - from: "frontend/src/components/admin/AdminUsersTab.vue"
+ to: "DELETE /api/admin/users/{id}"
+ via: "adminDeleteUser(id, adminPassword)"
+ - from: "backend/api/admin.py delete_user"
+ to: "services.auth.verify_password"
+ via: "verify_password(body.admin_password, admin.password_hash)"
+---
+
+
+Add admin hard-delete with password confirmation: a backend body model that verifies the admin's own password before permanent deletion, and a frontend inline confirmation panel with password field.
+
+Purpose: The backend delete endpoint exists and correctly purges all user data, but it accepts no authentication proof for the destructive action. There is also no frontend UI to trigger it.
+
+Output: Admin can initiate deletion from the Users tab, enter their password in an inline panel, and the backend verifies the password before deleting. Incorrect password is rejected with 403.
+
+
+
+@$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/admin.py:
+- Existing delete_user handler: DELETE /api/admin/users/{user_id}, status 204, no request body
+- Already purges cloud connections + MinIO objects, writes audit log (SEC-09 — do NOT change this logic)
+- Uses Depends(get_current_admin) → resolves to User ORM instance as `_admin`
+- verify_password not currently imported; services.auth exports it: `from services.auth import verify_password`
+- The handler must add: parse body as UserDeleteConfirm, call verify_password(body.admin_password, _admin.password_hash), raise 403 on failure
+
+From services/auth.py (existing pattern from admin.py imports):
+- `hash_password(plain: str) -> str`
+- `verify_password(plain: str, hashed: str) -> bool` — uses pwdlib Argon2
+
+From frontend/src/components/admin/AdminUsersTab.vue (confirmDeactivate pattern to mirror):
+- `confirmDeactivate = ref(null)` tracks which user ID is awaiting confirmation
+- `startDeactivate(id)` sets confirmDeactivate = id
+- Inline panel in | renders when `confirmDeactivate === user.id`
+- Panel has confirm + cancel buttons
+- Model to follow: add parallel state `confirmDelete = ref(null)`, `deletePassword = ref('')`, `deleteError = ref(null)`
+
+From frontend/src/api/client.js:
+- All admin functions follow: request(`/api/admin/users/${id}/...`, { method, headers, body })
+- DELETE with body: `request(\`/api/admin/users/${id}\`, { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ admin_password: adminPassword }) })`
+
+
+
+
+
+ Task 1: Backend — UserDeleteConfirm model + password verification in delete_user
+ backend/api/admin.py, backend/tests/test_admin.py
+
+ - DELETE /api/admin/users/{id} with correct admin_password in body returns 204 and user is deleted.
+ - DELETE /api/admin/users/{id} with wrong admin_password returns 403 {"detail": "Invalid admin password"} and user is NOT deleted.
+ - DELETE /api/admin/users/{id} with no body returns 422 (Pydantic validation).
+ - Cannot delete admin accounts (existing guard: 400 "Cannot delete admin accounts") — unchanged.
+ - Cannot delete non-existent user (existing guard: 404) — unchanged.
+ - Audit log entry written for successful delete (existing code) — unchanged.
+ - Cloud credentials purged before DB delete (existing SEC-09 code) — unchanged.
+
+
+ In backend/api/admin.py:
+ 1. Add `UserDeleteConfirm` Pydantic model in the Request models section:
+ ```python
+ class UserDeleteConfirm(BaseModel):
+ admin_password: str
+ ```
+ 2. Add `from services.auth import verify_password` to the existing imports from services.auth (currently imports `hash_password, revoke_all_refresh_tokens`).
+ 3. Modify the `delete_user` handler signature to accept the body:
+ - Change `async def delete_user(user_id, request, session, _admin)` to also accept `body: UserDeleteConfirm`.
+ - FastAPI will parse the JSON body automatically.
+ 4. Add password verification BEFORE any deletion logic (fail fast):
+ ```python
+ if not verify_password(body.admin_password, _admin.password_hash):
+ raise HTTPException(
+ status_code=status.HTTP_403_FORBIDDEN,
+ detail="Invalid admin password",
+ )
+ ```
+ 5. All existing deletion logic (cloud purge, MinIO purge, audit log, session.delete) is unchanged.
+
+ In backend/tests/test_admin.py, add three tests:
+ 1. `test_delete_user_correct_password` — create admin + regular user, call DELETE with correct admin password, assert 204, assert user no longer in GET /admin/users.
+ 2. `test_delete_user_wrong_password` — same setup, call DELETE with wrong password, assert 403, assert user still in GET /admin/users (not deleted).
+ 3. `test_delete_user_no_body` — call DELETE with no body (or empty body), assert 422.
+
+ Use the existing `_create_user_and_token(session, role="admin")` pattern from test_cloud.py (or the conftest admin_user fixture if available).
+
+
+ cd /Users/nik/Documents/Progamming/document_scanner/backend && python -m pytest tests/test_admin.py::test_delete_user_correct_password tests/test_admin.py::test_delete_user_wrong_password tests/test_admin.py::test_delete_user_no_body -v
+
+ Three tests pass. Delete with correct password returns 204. Delete with wrong password returns 403 and user survives. Delete with no body returns 422.
+
+
+
+ Task 2: Frontend — adminDeleteUser API function + inline delete confirmation panel
+ frontend/src/api/client.js, frontend/src/components/admin/AdminUsersTab.vue
+
+ ### 1. client.js — add adminDeleteUser
+ Export `adminDeleteUser(id, adminPassword)`:
+ ```javascript
+ export function adminDeleteUser(id, adminPassword) {
+ return request(`/api/admin/users/${id}`, {
+ method: 'DELETE',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ admin_password: adminPassword }),
+ })
+ }
+ ```
+
+ ### 2. AdminUsersTab.vue — add delete confirmation state
+ In ` |