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 11 execute 1
backend/api/admin.py
frontend/src/api/client.js
frontend/src/components/admin/AdminUsersTab.vue
backend/tests/test_admin_api.py
true
ADMIN-02
SEC-09
true
truths artifacts key_links
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
path provides
backend/api/admin.py UserDeleteConfirm Pydantic model; delete_user handler reads admin_password from body and verifies it
path provides
frontend/src/api/client.js adminDeleteUser(id, adminPassword) calling DELETE /api/admin/users/{id}
path provides
frontend/src/components/admin/AdminUsersTab.vue Inline delete confirmation panel with admin password field, mirroring confirmDeactivate pattern
from to via
frontend/src/components/admin/AdminUsersTab.vue DELETE /api/admin/users/{id} adminDeleteUser(id, adminPassword)
from to via
backend/api/admin.py delete_user services.auth.verify_password 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.

<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/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 backend/tests/test_admin_api.py:

  • admin_client fixture at line 71 returns (client, admin, session) tuple
  • Admin user plaintext password: "AdminPass1!Secret"
  • Use this fixture for all three new tests — do NOT recreate admin users manually

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_api.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_api.py, add three tests using the existing `admin_client` fixture (line 71, returns `(client, admin, session)`, admin password is "AdminPass1!Secret"):
1. `test_delete_user_correct_password` — use admin_client fixture, create a regular user, call DELETE with `{"admin_password": "AdminPass1!Secret"}`, assert 204, assert user no longer in GET /admin/users.
2. `test_delete_user_wrong_password` — same setup, call DELETE with `{"admin_password": "WrongPass!"}`, 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.
cd /Users/nik/Documents/Progamming/document_scanner/backend && python -m pytest tests/test_admin_api.py::test_delete_user_correct_password tests/test_admin_api.py::test_delete_user_wrong_password tests/test_admin_api.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 `<script setup>`, add alongside the existing `confirmDeactivate` state:
- `const confirmDelete = ref(null)` — holds the user ID awaiting delete confirmation
- `const deletePassword = ref('')` — the admin password input
- `const deleteError = ref(null)` — error message for wrong password

Add functions:
- `startDelete(id)`: sets `confirmDelete.value = id`, clears `deletePassword.value` and `deleteError.value`, and sets `confirmDeactivate.value = null` (cannot have both panels open at once).
- `cancelDelete()`: sets `confirmDelete.value = null`, clears password + error.
- `confirmDoDelete(id)`: sets `pendingAction[id] = true`, calls `await api.adminDeleteUser(id, deletePassword.value)`, on success removes the user from `users.value` and calls `cancelDelete()`. On error, sets `deleteError.value = e.message`. Always clears `pendingAction[id]` in finally.

### 3. AdminUsersTab.vue — template: Delete button in action column
In the `<template v-else-if="user.is_active">` block (the normal active user actions), add a Delete button after the Deactivate button:
```html
<span class="text-gray-300">·</span>
<button
  @click="startDelete(user.id)"
  class="text-red-800 hover:text-red-900 text-sm font-semibold"
>
  Delete
</button>
```
For deactivated users (the `<template v-else>` block), also add a Delete button after Reactivate, using the same markup.

### 4. AdminUsersTab.vue — template: inline delete confirmation panel
Replace the existing `<div v-if="confirmDeactivate === user.id">` inline panel pattern: add a second conditional panel for delete below it (as a sibling `<div>` within the `<td>`):
```html
<div v-if="confirmDelete === user.id" class="space-y-2">
  <p class="text-xs text-red-700 font-semibold">
    Permanently delete <span class="font-bold">{{ user.email }}</span>?
    This will erase all their documents, cloud connections, and quota data. This cannot be undone.
  </p>
  <div>
    <label class="block text-xs text-gray-700 mb-1 font-semibold">Your admin password to confirm</label>
    <input
      v-model="deletePassword"
      type="password"
      autocomplete="current-password"
      placeholder="Admin password"
      class="block w-full rounded-lg px-2 py-1.5 text-xs border border-red-300 focus:outline-none focus:ring-2 focus:ring-red-500 focus:border-red-500 transition-colors"
      @keydown.enter.prevent="confirmDoDelete(user.id)"
    />
  </div>
  <p v-if="deleteError" class="text-xs text-red-600">{{ deleteError }}</p>
  <div class="flex items-center gap-2">
    <button
      @click="confirmDoDelete(user.id)"
      :disabled="pendingAction[user.id] || !deletePassword"
      class="text-red-700 hover:text-red-800 text-sm font-semibold disabled:opacity-50"
    >
      <span v-if="pendingAction[user.id]" class="flex items-center gap-1">
        <span class="animate-spin rounded-full border-2 border-current border-t-transparent w-3 h-3"></span>
        Deleting…
      </span>
      <span v-else>Delete permanently</span>
    </button>
    <span class="text-gray-300">·</span>
    <button @click="cancelDelete" class="text-gray-500 hover:text-gray-700 text-sm">
      Cancel
    </button>
  </div>
</div>
```
The delete panel and deactivate panel are mutually exclusive: `startDelete` clears `confirmDeactivate`, and `startDeactivate` should also clear `confirmDelete` (add `confirmDelete.value = null` to `startDeactivate`).
cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5 Frontend build passes with zero errors. AdminUsersTab has Delete button and inline confirmation panel with password field. adminDeleteUser exported from client.js.

<threat_model>

Trust Boundaries

Boundary Description
DELETE /api/admin/users/{id} body Admin password sent in JSON body — verified server-side with Argon2 before any deletion
admin password in transit Sent over HTTPS (production); never stored, logged, or returned in any response

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-05-11-01 Elevation of Privilege DELETE /admin/users/{id} mitigate Requires get_current_admin (admin role) AND correct admin password verification via pwdlib Argon2 — two-factor confirmation
T-05-11-02 Information Disclosure Wrong password error message mitigate 403 "Invalid admin password" — does not confirm whether user exists (user existence already checked first but 403 is returned regardless to prevent oracle)
T-05-11-03 Tampering admin_password in request body mitigate Pydantic UserDeleteConfirm validates presence; verify_password uses constant-time Argon2 comparison (pwdlib)
T-05-11-04 Repudiation User deletion audit trail mitigate write_audit_log("admin.user_deleted") written before session.delete — existing code preserved unchanged
T-05-11-05 Denial of Service Repeated wrong-password delete attempts accept Admin endpoints already rate-limited at application level; admin accounts are trusted actors
T-05-11-SC Tampering npm/pip installs mitigate No new packages installed in this plan
</threat_model>
After both tasks complete: - `pytest backend/tests/test_admin_api.py::test_delete_user_correct_password backend/tests/test_admin_api.py::test_delete_user_wrong_password backend/tests/test_admin_api.py::test_delete_user_no_body -v` - `npm run build` — zero errors - Full pytest suite: `pytest -v` — zero new failures - Manual: open Admin panel → Users tab, confirm Delete button visible per user row - Manual: click Delete, enter correct admin password → user removed from list - Manual: click Delete, enter wrong password → error message shown, user not removed - Security: verify admin_password not present in any audit log entry

<success_criteria>

  • UserDeleteConfirm model added to admin.py
  • delete_user verifies admin password via verify_password before proceeding
  • Wrong password returns 403 without deleting user
  • adminDeleteUser(id, adminPassword) exported from client.js
  • AdminUsersTab has Delete button on active and deactivated rows
  • Inline password confirmation panel appears on Delete click, mutually exclusive with deactivate panel
  • Three new backend tests pass; full test suite has zero new failures; frontend build clean </success_criteria>
Create `.planning/phases/05-cloud-storage-backends/05-11-SUMMARY.md` when done