Files
kite/.planning/phases/02-users-authentication/02-05-PLAN.md
T
curo1305 16584ade00 docs(02): create phase 2 plan — Users & Authentication
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>
2026-05-22 19:13:44 +02:00

269 lines
18 KiB
Markdown

---
phase: 02-users-authentication
plan: 05
type: execute
wave: 5
depends_on:
- 02-02
- 02-03
- 02-04
files_modified:
- 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/components/layout/AppSidebar.vue
autonomous: false
requirements:
- ADMIN-01
- ADMIN-02
- ADMIN-03
- ADMIN-04
- ADMIN-05
- ADMIN-07
must_haves:
truths:
- "Admin panel is accessible at /admin and visible in sidebar only for role='admin' users"
- "Admin can view all users in a table with email, role, status, and action links"
- "Admin can create a user with email, temporary password, and role via an inline form above the table"
- "Admin can deactivate a user account with inline confirmation showing the user email"
- "Admin can reset a user password, sending an email via the backend"
- "Admin can edit a user's storage quota inline with a warning when new limit is below current usage"
- "Admin can assign AI provider and model per user from dropdown selectors"
- "Admin panel link in sidebar is absent for non-admin users"
- "No impersonation action or UI exists anywhere"
artifacts:
- path: "frontend/src/views/AdminView.vue"
provides: "Admin panel with horizontal tab strip: Users | Quotas | AI Config"
- path: "frontend/src/components/admin/AdminUsersTab.vue"
provides: "User table with create form, deactivate/reactivate/reset-password actions"
- path: "frontend/src/components/admin/AdminQuotasTab.vue"
provides: "Quota inline-edit table with usage warning"
- path: "frontend/src/components/admin/AdminAiConfigTab.vue"
provides: "AI provider/model dropdown per user with save"
- path: "frontend/src/components/layout/AppSidebar.vue"
provides: "Admin nav link (conditional) + user identity footer with sign-out button"
key_links:
- from: "frontend/src/views/AdminView.vue"
to: "frontend/src/components/admin/AdminUsersTab.vue"
via: "v-if on activeTab"
pattern: "AdminUsersTab"
- from: "frontend/src/components/layout/AppSidebar.vue"
to: "useAuthStore().user?.role"
via: "v-if conditional admin link"
pattern: "role.*admin"
---
<objective>
Deliver the complete admin panel frontend. After this plan, an admin user can manage all user accounts, quotas, and AI configurations from the web UI. The sidebar shows the admin link only for admin users and the user identity footer enables sign-out.
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.
</objective>
<execution_context>
@/Users/nik/.claude/get-shit-done/workflows/execute-plan.md
@/Users/nik/.claude/get-shit-done/templates/summary.md
</execution_context>
<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
</context>
<interfaces>
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
</interfaces>
<tasks>
<task type="checkpoint:human-verify" gate="blocking">
<what-built>
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.
</what-built>
<how-to-verify>
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
</how-to-verify>
<resume-signal>Type "backend verified" or describe any issues found</resume-signal>
</task>
<task type="auto">
<name>Task 2: Admin tab components and AdminView</name>
<files>
frontend/src/views/AdminView.vue,
frontend/src/components/admin/AdminUsersTab.vue,
frontend/src/components/admin/AdminQuotasTab.vue,
frontend/src/components/admin/AdminAiConfigTab.vue
</files>
<read_first>
- 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)
</read_first>
<action>
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.
</action>
<verify>
<automated>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');"</automated>
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>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.</done>
</task>
<task type="auto">
<name>Task 3: AppSidebar.vue — admin link and user identity footer</name>
<files>
frontend/src/components/layout/AppSidebar.vue
</files>
<read_first>
- 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)
</read_first>
<action>
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.
</action>
<verify>
<automated>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');"</automated>
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5</automated>
</verify>
<acceptance_criteria>
- 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
</acceptance_criteria>
<done>Sidebar updated with conditional admin link and user identity footer including initials avatar, email display, and sign-out button. Existing nav links untouched.</done>
</task>
</tasks>
<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>
<verification>
1. Log in as a regular user → sidebar does NOT show "Admin" link
2. Log in as admin user → sidebar shows "Admin" link navigating to /admin
3. At /admin, Users tab shows user table with Email, Role, Status, Created, Actions columns
4. Create a user via admin panel → user appears in table
5. Deactivate a user → inline confirmation appears with email; on confirm, row status badge changes to "Deactivated"
6. Quotas tab: click Edit on a row → limit cell becomes input; enter value below current usage → warning text appears after save
7. AI Config tab: change provider dropdown for a user → Save → "Saved" confirmation
8. npm run build exits 0
9. grep -c "impersonate\|loginAs\|login-as" frontend/src/components/admin/AdminUsersTab.vue returns 0
</verification>
<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>
<output>
Create `.planning/phases/02-users-authentication/02-05-SUMMARY.md` when done.
</output>