5 plans across 5 waves covering AUTH-01..08, SEC-01..03/05..07, ADMIN-01..05/07. Includes security hardening (Origin validation, per-account rate limiting, TOTP replay prevention, refresh token family revocation with security alert), TOTP + backup code login, and admin panel frontend. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
18 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 02-users-authentication | 05 | execute | 5 |
|
|
false |
|
|
Purpose: This is the final wave plan — it wires all the admin API endpoints (Plan 04) into working Vue components with the exact UI-SPEC visual contract. Output: AdminView.vue, three admin tab components, AppSidebar.vue update with admin link and user footer.
<execution_context> @/Users/nik/.claude/get-shit-done/workflows/execute-plan.md @/Users/nik/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/02-users-authentication/02-CONTEXT.md @.planning/phases/02-users-authentication/02-PATTERNS.md @.planning/phases/02-users-authentication/02-UI-SPEC.md @.planning/phases/02-users-authentication/02-04-SUMMARY.md From frontend/src/api/client.js (Plan 02 exports):adminListUsers() → GET /api/admin/users → { items: [{ id, handle, email, role, is_active, totp_enabled, ai_provider, ai_model, created_at }] } adminCreateUser(body) → POST /api/admin/users → 201 { id, handle, email, role, created_at } adminDeactivateUser(id) → PATCH /api/admin/users/{id}/status { is_active: false } adminReactivateUser(id) → PATCH /api/admin/users/{id}/status { is_active: true } adminResetUserPassword(id) → POST /api/admin/users/{id}/password-reset → 202 adminUpdateQuota(id, limitBytes) → PATCH /api/admin/users/{id}/quota { limit_bytes: limitBytes } adminUpdateAiConfig(id, provider, model) → PATCH /api/admin/users/{id}/ai-config { ai_provider, ai_model }
From frontend/src/stores/auth.js (Plan 02):
const user = ref(null) // { id, handle, email, role, totp_enabled } function logout() // clears accessToken and user
From frontend/src/components/layout/AppSidebar.vue (current — must be extended, not recreated):
<script setup> import { useTopicsStore } from '../../stores/topics.js' const topicsStore = useTopicsStore() </script>UI-SPEC admin panel visual contract: Tab strip: flex border-b border-gray-200 mb-6 Active tab: px-4 py-2 text-sm font-semibold text-indigo-600 border-b-2 border-indigo-600 Inactive tab: px-4 py-2 text-sm font-semibold text-gray-500 hover:text-gray-700 border-b-2 border-transparent Table: bg-white rounded-xl border border-gray-200 overflow-hidden divide-y divide-gray-200 Deactivate link: text-red-600 hover:text-red-700 text-sm Other actions: text-indigo-600 hover:text-indigo-700 text-sm Create button: bg-indigo-600 hover:bg-indigo-700 text-white text-sm font-semibold px-4 py-2 rounded-lg
After Plans 02, 03, and 04 complete, the full auth and admin backend is live. This checkpoint verifies the backend before building the admin frontend against it. 1. `docker compose up` — confirm all services start cleanly 2. `curl -s http://localhost:8000/health` → { status: "ok", ... } 3. Register a user: `curl -s -X POST http://localhost:8000/api/auth/register -H 'Content-Type: application/json' -d '{"handle":"testuser","email":"test@test.com","password":"TestPass12!"}'` → 201 4. Login: `curl -s -X POST http://localhost:8000/api/auth/login -H 'Content-Type: application/json' -d '{"email":"test@test.com","password":"TestPass12!"}' -v` → 200 + Set-Cookie header containing "HttpOnly" and "SameSite=Strict" 5. Try GET /api/admin/users without admin token → 403 6. Check docker compose logs for admin bootstrap: should see "Admin bootstrap" log line or "ADMIN_EMAIL not set" warning Type "backend verified" or describe any issues found Task 2: Admin tab components and AdminView frontend/src/views/AdminView.vue, frontend/src/components/admin/AdminUsersTab.vue, frontend/src/components/admin/AdminQuotasTab.vue, frontend/src/components/admin/AdminAiConfigTab.vue - frontend/src/views/SettingsView.vue (tabbed sections pattern — v-if on activeTab, button tab strip, card layout) - frontend/src/views/TopicsView.vue (list + action pattern for AdminUsersTab) - .planning/phases/02-users-authentication/02-UI-SPEC.md (Admin View section — table structure, tab strip classes, row states, create user panel, quota inline edit, AI config table) - .planning/phases/02-users-authentication/02-PATTERNS.md (AdminUsersTab, AdminQuotasTab, AdminAiConfigTab, AdminView sections) - .planning/phases/02-users-authentication/02-UI-SPEC.md (Copywriting Contract — admin entries, loading states) frontend/src/views/AdminView.vue: Heading "Admin panel" (text-2xl font-semibold text-gray-900). Horizontal tab strip using UI-SPEC tab classes. Tabs: "Users" | "Quotas" | "AI Config". v-if switch: AdminUsersTab, AdminQuotasTab, AdminAiConfigTab. Import all three components. No guard needed here — /admin route is protected by router guard; AdminView only renders when admin user is authenticated.frontend/src/components/admin/AdminUsersTab.vue: On mounted, call adminListUsers() and store in users ref. Table columns per UI-SPEC: Email | Role | Status | Created | Actions. Role badge: same indigo/gray badge styles as AccountView. Status badge: active=bg-green-100 text-green-700, deactivated=bg-gray-100 text-gray-500. Actions column: for active rows "Reset password" · "Deactivate"; for deactivated rows "Reactivate". Action link classes per UI-SPEC.
Deactivate flow: clicking "Deactivate" replaces the row's action cell with an inline confirmation showing the user email ("Deactivate [email]? They will lose access immediately. Their data is preserved." + "Deactivate" / "Keep account" buttons). On confirm: call adminDeactivateUser(id), update row is_active=false. No modal — inline replacement per UI-SPEC.
Create user: "Create user" button top-right of table. On click: show inline panel above table (not a modal) with: email input, role selector (User/Admin dropdown), password field (pre-generated 12-char random password, read-only, with copy button) — executor should generate the temp password client-side via crypto.getRandomValues to produce a strong alphanumeric string. On submit: adminCreateUser({ handle: email.split('@')[0], email, password, role }) then prepend to users list. Copywriting: "Create user" CTA.
Loading states: per UI-SPEC loading table — row-level spinner (animate-spin rounded-full border-2 border-current border-t-transparent w-4 h-4 inline) in the action column while deactivate/reset operations are in flight; pointer-events-none on the row.
Empty state: if users.length === 0 after load: heading "No users yet" + body "Create the first user account to get started." — center in table area.
frontend/src/components/admin/AdminQuotasTab.vue: On mounted, call adminListUsers(). Table columns: Email | Used | Limit | Usage % | Actions. Used and Limit displayed in MB (Math.round(bytes / 1048576)) + " MB" suffix. Usage %: Math.round(used/limit * 100) + "%". Actions: "Edit" button per row. On "Edit": the Limit cell becomes an input type="number" min="1" step="1" (value in MB) + Save/Cancel buttons. On save: adminUpdateQuota(id, newMB * 1048576). If response.warning is true, show inline warning below input: text-xs text-amber-600 "New limit is below current usage (X MB). Existing documents will not be deleted, but uploads will be blocked." Still apply the update. Loading: spinner inline in save button + disabled.
frontend/src/components/admin/AdminAiConfigTab.vue: On mounted, call adminListUsers(). Table columns: Email | AI Provider | AI Model | Actions. Provider: dropdown <select> with options: "openai", "anthropic", "ollama", "lmstudio" (hardcoded list matching DEFAULT_SETTINGS providers in config.py). Model: text input (or dropdown if provider selected — for MVP, use a text input pre-filled with user's current ai_model or empty). "Save" button per row. On save: adminUpdateAiConfig(id, provider, model). Success: row briefly shows green "Saved" text for 1.5s. Loading: spinner in Save button + disabled.
No impersonation action, link, or UI element exists in any of these components.
cd /Users/nik/Documents/Progamming/document_scanner/frontend && node -e "const fs = require('fs'); ['src/views/AdminView.vue','src/components/admin/AdminUsersTab.vue','src/components/admin/AdminQuotasTab.vue','src/components/admin/AdminAiConfigTab.vue'].forEach(f => { const c = fs.readFileSync(f,'utf8'); if (c.includes('impersonate') || c.includes('login-as') || c.includes('loginAs')) throw new Error('Impersonation found in ' + f); }); console.log('No impersonation in admin components');"
cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5
- frontend/src/views/AdminView.vue contains "Admin panel" heading and three tab references (AdminUsersTab, AdminQuotasTab, AdminAiConfigTab)
- AdminUsersTab.vue contains "adminListUsers" and "adminDeactivateUser" and "adminResetUserPassword"
- AdminUsersTab.vue contains "Deactivate" and "Reset password" and "Reactivate" action text
- AdminUsersTab.vue contains "No users yet" (empty state)
- AdminQuotasTab.vue contains "adminUpdateQuota" and "MB" and "warning"
- AdminAiConfigTab.vue contains "adminUpdateAiConfig"
- grep -c "impersonate\|loginAs\|login-as" frontend/src/components/admin/AdminUsersTab.vue returns 0
- npm run build exits 0
Admin panel frontend delivered: Users tab with create/deactivate/reset, Quotas tab with inline edit and warning, AI Config tab with provider/model dropdowns. No impersonation UI exists.
Task 3: AppSidebar.vue — admin link and user identity footer
frontend/src/components/layout/AppSidebar.vue
- frontend/src/components/layout/AppSidebar.vue (full file — extend in-place, understand existing nav-link structure before modifying)
- frontend/src/stores/auth.js (useAuthStore — user.role, logout() function signature)
- .planning/phases/02-users-authentication/02-UI-SPEC.md (Sidebar Updates section — admin link spec, user identity footer layout and class spec)
- .planning/phases/02-users-authentication/02-PATTERNS.md (AppSidebar.vue section — import pattern, admin link template, user footer template)
Extend AppSidebar.vue in-place (do not recreate the file). Read the full current file first, then apply targeted additions.
Script block: add `import { useAuthStore } from '../../stores/auth.js'` and `const authStore = useAuthStore()` alongside the existing topicsStore import.
Admin link: add a router-link to "/admin" inside the nav section, above the Settings link. Conditional: v-if="authStore.user?.role === 'admin'". Icon: inline SVG shield (heroicon stroke style, w-4 h-4). Label: "Admin". Apply same nav-link scoped class as other nav items. Active state: :class="{ 'nav-link-active': $route.path === '/admin' }".
User identity footer: add below the settings link, inside the sidebar's bottom section. Layout per UI-SPEC:
div.flex.items-center.gap-3.px-4.py-3.border-t.border-gray-100
div (avatar circle): initials from authStore.user?.email (first char uppercase). Classes: bg-indigo-100 text-indigo-700 text-xs font-semibold rounded-full w-8 h-8 flex items-center justify-center shrink-0
span (email): authStore.user?.email. Classes: text-xs text-gray-600 truncate flex-1
button (sign-out icon): @click="authStore.logout(); $router.push('/login')". aria-label="Sign out". Icon: inline SVG arrow-right-on-rectangle (w-4 h-4). Classes: text-gray-400 hover:text-gray-600
The entire footer block is conditionally rendered: v-if="authStore.user".
Do not modify any existing nav links, styles, or the topicsStore import. Only add the new elements.
cd /Users/nik/Documents/Progamming/document_scanner/frontend && node -e "const fs = require('fs'); const c = fs.readFileSync('src/components/layout/AppSidebar.vue','utf8'); if (!c.includes('useAuthStore')) throw new Error('No useAuthStore'); if (!c.includes('role.*admin') && !c.includes('admin.*role')) throw new Error('No admin role check'); if (!c.includes('aria-label')) throw new Error('No aria-label on sign-out'); console.log('Sidebar OK');"
cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5
- AppSidebar.vue contains "useAuthStore" import
- AppSidebar.vue contains v-if with 'admin' role check for the admin link — grep -c "admin" frontend/src/components/layout/AppSidebar.vue returns at least 2
- AppSidebar.vue contains "aria-label" (sign-out button accessibility)
- AppSidebar.vue contains "border-t border-gray-100" (footer separator)
- AppSidebar.vue contains authStore.logout() call
- The existing nav links, topicsStore reference, and scoped styles are preserved unchanged
- npm run build exits 0
Sidebar updated with conditional admin link and user identity footer including initials avatar, email display, and sign-out button. Existing nav links untouched.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| browser→admin API | Admin Bearer token required for all admin API calls from the frontend |
| admin UI→user data | Admin can view user metadata; UI must never render password_hash or credentials_enc |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-02-30 | Elevation of Privilege | Admin link visible to non-admin users | mitigate | v-if="authStore.user?.role === 'admin'" on the sidebar link; security enforced at API layer regardless |
| T-02-31 | Elevation of Privilege | Admin UI impersonation | mitigate | No "Log in as user" button, link, or action in any admin component; acceptance criteria grep confirms zero occurrences |
| T-02-32 | Information Disclosure | Admin panel renders sensitive user data | mitigate | Admin API response shape excludes password_hash/credentials_enc; UI only binds to safe fields (email, role, is_active, ai_provider, ai_model, limit_bytes, used_bytes) |
| T-02-33 | Tampering | Inline deactivation without confirmation | mitigate | Deactivate action shows inline confirmation with user email before calling API; prevents accidental mass-deactivation |
| T-02-34 | Denial of Service | Admin creates unlimited users | accept | No rate limit on admin user creation — admin is a trusted role; acceptable risk for single-tenant deployment |
| </threat_model> |
<success_criteria>
- Admin panel renders with correct tab navigation and visual design matching UI-SPEC
- All three admin tabs functional: user CRUD, quota inline edit with warning, AI config per-user
- Sidebar admin link appears only for role='admin' users
- Sidebar user identity footer shows initials, email, and sign-out button
- No impersonation UI exists anywhere in admin components
- npm run build exits 0 </success_criteria>