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>
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 |
|
true |
|
true |
|
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.mdFrom 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) -> strverify_password(plain: str, hashed: str) -> bool— uses pwdlib Argon2
From backend/tests/test_admin_api.py:
admin_clientfixture 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 confirmationstartDeactivate(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 }) })`
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> |
<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>