Files
kite/.planning/phases/02-users-authentication/02-06-PLAN.md
T
curo1305 eaa3399ec0 docs: add shared module map to CLAUDE.md, SECURITY.md, planning artifacts
- CLAUDE.md: add Code Standards section with backend and frontend shared
  module maps, component architecture rules, duplication checklist, and
  no-dead-code enforcement rule
- SECURITY.md: Phase 02 + 03 security audit results (all threats CLOSED)
- .planning: update milestone audit, config, and add plan/UAT files for
  phases 01, 02-06, and 06.2-05

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-02 16:10:59 +02:00

17 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, gap_closure, source_doc, must_haves
phase plan type wave depends_on files_modified autonomous requirements gap_closure source_doc must_haves
02-users-authentication 06 execute 1
02-05
backend/api/admin.py
backend/tests/test_admin_api.py
frontend/src/router/index.js
frontend/src/App.vue
frontend/src/views/SettingsView.vue
frontend/src/components/auth/TotpEnrollment.vue
frontend/package.json
true
AUTH-03
AUTH-04
AUTH-05
SEC-01
SEC-03
ADMIN-01
ADMIN-07
true 02-UAT.md
truths artifacts key_links
Admin can create a new user via POST /api/admin/users without HTTP 500
Login, register, and password-reset pages show AuthLayout only — no sidebar, no user identity footer
After logout the sidebar is gone — the user lands on the login page with AuthLayout
Non-admin user navigating to /admin is redirected to /
TOTP enrollment step 1 shows a scannable QR image, not a text link
TOTP enrollment option is accessible from a tab within /settings (Account tab)
path provides contains
backend/api/admin.py create_user handler with await session.flush() before write_audit_log() await session.flush()
path provides contains
backend/tests/test_admin_api.py regression test confirming audit_log FK ordering is safe test_create_user_writes_audit_log
path provides contains
frontend/src/router/index.js meta: { layout: 'auth' } on auth routes; meta: { requiresAdmin: true } on /admin; beforeEach role guard requiresAdmin
path provides contains
frontend/src/App.vue Layout-aware root — renders AuthLayout for auth routes, app shell for all others AuthLayout
path provides contains
frontend/src/views/SettingsView.vue Account tab that embeds AccountView content (2FA, change password, sign-out-all) account
path provides contains
frontend/src/components/auth/TotpEnrollment.vue QR image rendered from qrUri using qrcode library QRCode
path provides contains
frontend/package.json qrcode package installed qrcode
from to via pattern
frontend/src/App.vue frontend/src/layouts/AuthLayout.vue v-if route.meta.layout === 'auth' conditional import AuthLayout
from to via pattern
frontend/src/router/index.js frontend/src/stores/auth.js beforeEach reads authStore.user?.role requiresAdmin.*role
from to via pattern
frontend/src/components/auth/TotpEnrollment.vue qrcode import QRCode from 'qrcode'; QRCode.toDataURL(qrUri.value) toDataURL
Close five UAT gaps discovered in Phase 02 that block critical auth flows from passing.

Purpose: Phase 02 is marked complete in STATE.md, but the UAT revealed a blocker (admin 500) and four major issues (sidebar on auth pages, orphaned account view, missing admin route guard, missing QR code). These gaps prevent TOTP enrollment, leaks user identity on public pages, and allows non-admins to reach the admin panel. This plan fixes all five gaps.

Output: Five concrete fixes — one backend single-line verification + regression test, and four frontend changes — that make UAT tests 4, 6, 7, 9, and 14 pass.

Note: GAP 1 (admin create_user HTTP 500) was already fixed during plan 02-04 execution. The await session.flush() is present at admin.py:247 and a regression test test_create_user_writes_audit_log exists in test_admin_api.py. Task 1 below verifies the fix is correct and confirms the test passes rather than making any code change.

<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/02-users-authentication/02-05-SUMMARY.md From frontend/src/stores/auth.js: accessToken ref(null) — JWT, memory only user ref(null) — { id, handle, email, role, totp_enabled } refresh() async function — uses httpOnly cookie; called in beforeEach on page reload

From frontend/src/layouts/AuthLayout.vue: Template:

← renders the auth page card
Note: AuthLayout already contains its own — App.vue must NOT add a second one when AuthLayout is active.

From frontend/src/App.vue (current): Unconditionally renders + — no layout switch. Import: import AppSidebar from './components/layout/AppSidebar.vue'

From frontend/src/router/index.js (current): Auth routes have only meta: { public: true } — no layout hint. /admin route has no meta at all. beforeEach: checks accessToken only; never reads user.role.

From frontend/src/components/auth/TotpEnrollment.vue: step ref: 'setup' | 'verify' | 'backup-codes' qrUri ref: provisioning_uri from api.totpSetup() — valid otpauth:// URI In 'verify' step, currently renders text link. qrcode is not installed (confirmed: package.json has no qrcode entry).

From frontend/src/views/SettingsView.vue: Tabs array: [{ id: 'preferences' }, { id: 'ai' }, { id: 'cloud' }] activeTab ref defaults to 'preferences'. Pattern: v-if="activeTab === 'preferences'" renders AccountView content to merge: Account info section, 2FA section (TotpEnrollment), Change password form, Sessions (sign-out-all). All logic lives in AccountView.vue script setup — reuse the same components (TotpEnrollment, ConfirmBlock, PasswordStrengthBar).

Task 1: Verify backend fix + regression test for admin create_user (GAP 1) backend/api/admin.py, backend/tests/test_admin_api.py Confirm the fix is present and the regression test passes. Do not change any code unless the verification below fails.
Step 1 — Verify the flush is present: Read backend/api/admin.py lines 239260. Confirm `await session.flush()` appears after `session.add(quota)` and before the `write_audit_log()` call. If the line is missing, add it immediately after `session.add(quota)` (single-line change, matching the comment pattern already used in bootstrap_admin and auth/register).

Step 2 — Verify the regression test exists: Read backend/tests/test_admin_api.py. Confirm `test_create_user_writes_audit_log` exists and checks that POST /api/admin/users returns 201 AND that an audit_log row with event_type='admin.user_created' exists. If the test is absent or only checks status_code without asserting the audit log row, add or extend it.

Step 3 — Run the test: `cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest tests/test_admin_api.py::test_create_user_writes_audit_log -v`. It must pass.
cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest tests/test_admin_api.py::test_create_user_writes_audit_log -v test_create_user_writes_audit_log passes; session.flush() confirmed present before write_audit_log() in create_user handler Task 2: Auth route layout switching + admin role guard (GAPs 2, 3, 4) frontend/src/router/index.js, frontend/src/App.vue Three changes, two files:
--- frontend/src/router/index.js ---

Change 1 — Add layout hint to all four auth routes. Set `meta: { public: true, layout: 'auth' }` on:
  - /login
  - /register
  - /password-reset
  - /password-reset/confirm

Change 2 — Add admin guard to /admin route. Change:
  `{ path: '/admin', component: () => import('../views/AdminView.vue') }`
to:
  `{ path: '/admin', component: () => import('../views/AdminView.vue'), meta: { requiresAdmin: true } }`

Change 3 — Extend beforeEach to check requiresAdmin. The existing guard ends after the try/catch. After that block, add:

  if (to.meta.requiresAdmin && authStore.user?.role !== 'admin') {
    return { path: '/' }
  }

This runs after the silent refresh attempt (so authStore.user is populated) and before the route renders. Do not alter any existing logic — append only.

--- frontend/src/App.vue ---

Replace the entire file content with a layout-aware version:

- Import both AuthLayout and AppSidebar.
- Import useRoute from vue-router.
- In the template: use a v-if/v-else on `route.meta.layout === 'auth'`:
  - When true: render `<AuthLayout />` only. AuthLayout already contains its own
    `<router-view />` — do NOT add another router-view here.
  - When false (all other routes): render the original app shell:
    `<div class="flex h-screen overflow-hidden"><AppSidebar /><main class="flex-1 overflow-y-auto"><router-view /></main></div>`
- Keep the existing onMounted topicsStore.fetchTopics() call.
- AuthLayout is a local component; import it as:
    `import AuthLayout from './layouts/AuthLayout.vue'`
cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5 - Build exits 0. - Vite output contains no "component not found" or "missing import" warnings. - frontend/src/App.vue imports AuthLayout and uses route.meta.layout conditionally. - frontend/src/router/index.js has meta.layout:'auth' on all four auth routes and meta.requiresAdmin:true on /admin with the role check in beforeEach. Task 3: AccountView merged into SettingsView as Account tab + QR code in TotpEnrollment (GAPs 3 and 5) frontend/src/views/SettingsView.vue, frontend/src/components/auth/TotpEnrollment.vue, frontend/package.json Three changes:
--- frontend/package.json ---

Add `"qrcode": "^1.5.4"` to the `dependencies` section (not devDependencies — it is a runtime library). Run `npm install` in the frontend directory after editing.

--- frontend/src/components/auth/TotpEnrollment.vue ---

In the 'verify' step block, replace the `<a :href="qrUri">` link block with a QR image. Use the qrcode library to generate a data URL:

1. Add an import at the top of the script setup block:
     `import QRCode from 'qrcode'`

2. Add a ref for the QR data URL:
     `const qrDataUrl = ref('')`

3. In startSetup(), after setting `qrUri.value = data.provisioning_uri`, generate the QR image:
     `qrDataUrl.value = await QRCode.toDataURL(qrUri.value, { width: 200, margin: 1 })`

4. In the 'verify' step template, replace the entire `<div class="bg-white border...">` block
   (the one containing the `<a :href="qrUri">` link) with:
     `<img v-if="qrDataUrl" :src="qrDataUrl" alt="TOTP QR code" class="w-48 h-48 rounded-xl border border-gray-200" />`
   Keep the manual secret display section (the `<code>` block) immediately below the image
   so users who cannot scan still have the fallback.

The QRCode.toDataURL call returns a Promise<string> with a data:image/png;base64,... URL.
The img tag renders it inline without any server round-trip.

--- frontend/src/views/SettingsView.vue ---

Add an "Account" tab to SettingsView that embeds the AccountView content directly.

1. Add the tab entry to the tabs array (between 'preferences' and 'ai', or append after 'cloud' — append at the end is fine):
     `{ id: 'account', label: 'Account' }`

2. Add a new tab panel below the existing three:
     `<SettingsAccountTab v-if="activeTab === 'account'" />`

3. Create the new component file at:
     `frontend/src/components/settings/SettingsAccountTab.vue`

   This component contains exactly the content from AccountView.vue:
     - The four sections (Account information, Two-factor authentication, Change password, Sessions)
     - All script setup logic (changePassword, disableTotp, onTotpEnrolled, signOutAll, all refs)
     - All imports (useAuthStore, api, PasswordStrengthBar, TotpEnrollment, ConfirmBlock, AppSpinner)
   Remove the outer `<div class="p-8 max-w-2xl mx-auto">` wrapper and `<h2>Account settings</h2>`
   heading — SettingsView already provides the page chrome.

4. In SettingsView.vue script setup, add the import:
     `import SettingsAccountTab from '../components/settings/SettingsAccountTab.vue'`

5. Update the /account route in frontend/src/router/index.js to redirect to settings:
     `{ path: '/account', redirect: '/settings' }`
   (Remove the lazy import of AccountView from the route — the view is now embedded in Settings.)
   This ensures any bookmark or back-navigation to /account silently lands on /settings.

Do NOT delete AccountView.vue — leave it in place (the redirect makes it unreachable from the router, not deleted from disk).
cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5 - Build exits 0. - frontend/package.json contains "qrcode" in dependencies. - frontend/src/components/auth/TotpEnrollment.vue imports QRCode and renders an img tag in the verify step. - frontend/src/views/SettingsView.vue has an Account tab rendering SettingsAccountTab. - frontend/src/components/settings/SettingsAccountTab.vue exists with 2FA, change password, and sign-out-all sections. - /account route redirects to /settings.

<threat_model>

Trust Boundaries

Boundary Description
router guard Unauthenticated or non-admin client navigates directly to /admin
layout selection Auth page accidentally renders app shell leaking user identity

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-02-GAP-01 Elevation of Privilege router/index.js beforeEach mitigate requiresAdmin meta + role check; if authStore.user?.role !== 'admin' → redirect to /
T-02-GAP-02 Information Disclosure App.vue AppSidebar mitigate Conditional layout: auth routes render AuthLayout only; sidebar absent on all public routes
T-02-GAP-03 Tampering admin.py create_user flush order accept Already mitigated: await session.flush() present before write_audit_log(); regression test confirms FK ordering
T-02-GAP-SC Tampering npm install qrcode mitigate qrcode@1.5.x is the canonical npm package (weekly downloads 20M+); no server dependency; LEGITIMACY: VERIFIED
</threat_model>
Run all checks from the project root:
# Backend regression test (GAP 1 fix confirmed)
cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest tests/test_admin_api.py -v -k "create_user"

# Full backend suite — zero failures
cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest -v

# Frontend build — exits 0
cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build

# Frontend test suite — exits 0
cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm test

# Confirm layout guard is wired
grep -n "layout.*auth\|AuthLayout" /Users/nik/Documents/Progamming/document_scanner/frontend/src/App.vue
grep -n "layout.*auth" /Users/nik/Documents/Progamming/document_scanner/frontend/src/router/index.js | wc -l
# expect 4 (login, register, password-reset, password-reset/confirm)

# Confirm admin route guard
grep -n "requiresAdmin\|role.*admin" /Users/nik/Documents/Progamming/document_scanner/frontend/src/router/index.js

# Confirm QR library installed
grep "qrcode" /Users/nik/Documents/Progamming/document_scanner/frontend/package.json

# Confirm QR image rendered (not a link)
grep -n "toDataURL\|qrDataUrl\|img.*qr" /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/auth/TotpEnrollment.vue

# Confirm Account tab in SettingsView
grep -n "account\|SettingsAccountTab" /Users/nik/Documents/Progamming/document_scanner/frontend/src/views/SettingsView.vue

<success_criteria>

  1. pytest tests/test_admin_api.py::test_create_user_writes_audit_log passes — confirms audit_log FK ordering is correct under PostgreSQL
  2. Visiting /login, /register, /password-reset renders AuthLayout (no sidebar, no user identity) — confirmed by App.vue v-if on route.meta.layout
  3. Non-admin authenticated user navigating to /admin is redirected to / — confirmed by beforeEach requiresAdmin check
  4. SettingsView has an Account tab containing TotpEnrollment, change password form, and sign-out-all; /account redirects to /settings
  5. TotpEnrollment 'verify' step renders an <img> tag sourced from QRCode.toDataURL(qrUri) — no <a href="otpauth://..."> link in production path
  6. pytest -v in backend passes with zero failures
  7. npm run build in frontend exits 0
  8. npm test in frontend exits 0 </success_criteria>
Create `.planning/phases/02-users-authentication/02-06-SUMMARY.md` when done.