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>
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
---
|
||||
status: testing
|
||||
phase: 01-infrastructure-foundation
|
||||
source: 01-01-SUMMARY.md, 01-02-SUMMARY.md, 01-03-SUMMARY.md, 01-04-SUMMARY.md, 01-05-SUMMARY.md
|
||||
started: 2026-05-31T00:00:00Z
|
||||
updated: 2026-05-31T00:00:00Z
|
||||
---
|
||||
|
||||
## Current Test
|
||||
|
||||
<!-- OVERWRITE each test - shows where we are -->
|
||||
|
||||
number: 1
|
||||
name: Cold Start Smoke Test
|
||||
expected: |
|
||||
Kill any running containers. Run `docker compose down -v` to clear all volumes and state.
|
||||
Then run `docker compose up --build -d`. All 5 services (postgres, minio, redis, backend,
|
||||
celery-worker) should come up as `Up (healthy)` with no errors. Hit `GET /health` and get
|
||||
back `{"status":"ok","checks":{"postgres":"ok","minio":"ok"}}` — live data from a fresh start.
|
||||
awaiting: user response
|
||||
|
||||
## Tests
|
||||
|
||||
### 1. Cold Start Smoke Test
|
||||
expected: Kill any running containers. Run `docker compose down -v` to clear all volumes and state. Then run `docker compose up --build -d`. All 5 services (postgres, minio, redis, backend, celery-worker) should come up as `Up (healthy)` with no errors. Hit `GET /health` and get back `{"status":"ok","checks":{"postgres":"ok","minio":"ok"}}` — live data from a fresh start.
|
||||
result: [pending]
|
||||
|
||||
### 2. Database Migration Applies Cleanly
|
||||
expected: Run `cd backend && alembic upgrade head`. It should exit 0 with output `Running upgrade -> 0001`. All 11 tables (users, quotas, refresh_tokens, folders, documents, topics, document_topics, shares, audit_log, cloud_connections, groups) should be present in PostgreSQL.
|
||||
result: [pending]
|
||||
|
||||
### 3. Health Endpoint Reports OK
|
||||
expected: `GET /health` (or `curl http://localhost:8000/health`) returns HTTP 200 with `{"status":"ok","checks":{"postgres":"ok","minio":"ok"}}`. Both postgres and minio checks show "ok".
|
||||
result: [pending]
|
||||
|
||||
### 4. Document Upload Stored in PostgreSQL + MinIO
|
||||
expected: Upload any text or PDF file via `POST /documents` (multipart form, field `file`). Response contains a UUID `id` and `original_name` matching the filename. Checking PostgreSQL shows one row in `documents` with `object_key` starting with `null-user/`. Checking the MinIO `docuvault` bucket shows the object is present.
|
||||
result: [pending]
|
||||
|
||||
### 5. Celery Background Task Processes Upload
|
||||
expected: After uploading a document, Celery should automatically pick up the `extract_and_classify` task. Within a few seconds, `docker compose logs celery-worker` shows `Task tasks.document_tasks.extract_and_classify[...] succeeded`. No `FAILED` task entries appear.
|
||||
result: [pending]
|
||||
|
||||
### 6. Document Delete Removes from Both Stores
|
||||
expected: `DELETE /documents/{id}` (using the UUID from the upload) returns `{"success": true}`. The document row is gone from PostgreSQL. The object is gone from the MinIO `docuvault` bucket.
|
||||
result: [pending]
|
||||
|
||||
### 7. MinIO Object Key Contains No Original Filename
|
||||
expected: After uploading a file named something recognizable (e.g. `invoice-q3.pdf`), inspect the object key in MinIO. The key should be in the form `null-user/{uuid}/{uuid}.pdf` — the word "invoice", "q3", or any part of the original filename must NOT appear in the key.
|
||||
result: [pending]
|
||||
|
||||
## Summary
|
||||
|
||||
total: 7
|
||||
passed: 0
|
||||
issues: 0
|
||||
pending: 7
|
||||
skipped: 0
|
||||
blocked: 0
|
||||
|
||||
## Gaps
|
||||
|
||||
[none yet]
|
||||
@@ -0,0 +1,340 @@
|
||||
---
|
||||
phase: 02-users-authentication
|
||||
plan: "06"
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: ["02-05"]
|
||||
files_modified:
|
||||
- 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
|
||||
autonomous: true
|
||||
requirements: [AUTH-03, AUTH-04, AUTH-05, SEC-01, SEC-03, ADMIN-01, ADMIN-07]
|
||||
gap_closure: true
|
||||
source_doc: 02-UAT.md
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "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)"
|
||||
artifacts:
|
||||
- path: "backend/api/admin.py"
|
||||
provides: "create_user handler with await session.flush() before write_audit_log()"
|
||||
contains: "await session.flush()"
|
||||
- path: "backend/tests/test_admin_api.py"
|
||||
provides: "regression test confirming audit_log FK ordering is safe"
|
||||
contains: "test_create_user_writes_audit_log"
|
||||
- path: "frontend/src/router/index.js"
|
||||
provides: "meta: { layout: 'auth' } on auth routes; meta: { requiresAdmin: true } on /admin; beforeEach role guard"
|
||||
contains: "requiresAdmin"
|
||||
- path: "frontend/src/App.vue"
|
||||
provides: "Layout-aware root — renders AuthLayout for auth routes, app shell for all others"
|
||||
contains: "AuthLayout"
|
||||
- path: "frontend/src/views/SettingsView.vue"
|
||||
provides: "Account tab that embeds AccountView content (2FA, change password, sign-out-all)"
|
||||
contains: "account"
|
||||
- path: "frontend/src/components/auth/TotpEnrollment.vue"
|
||||
provides: "QR image rendered from qrUri using qrcode library"
|
||||
contains: "QRCode"
|
||||
- path: "frontend/package.json"
|
||||
provides: "qrcode package installed"
|
||||
contains: "qrcode"
|
||||
key_links:
|
||||
- from: "frontend/src/App.vue"
|
||||
to: "frontend/src/layouts/AuthLayout.vue"
|
||||
via: "v-if route.meta.layout === 'auth' conditional import"
|
||||
pattern: "AuthLayout"
|
||||
- from: "frontend/src/router/index.js"
|
||||
to: "frontend/src/stores/auth.js"
|
||||
via: "beforeEach reads authStore.user?.role"
|
||||
pattern: "requiresAdmin.*role"
|
||||
- from: "frontend/src/components/auth/TotpEnrollment.vue"
|
||||
to: "qrcode"
|
||||
via: "import QRCode from 'qrcode'; QRCode.toDataURL(qrUri.value)"
|
||||
pattern: "toDataURL"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/02-users-authentication/02-05-SUMMARY.md
|
||||
</context>
|
||||
|
||||
<interfaces>
|
||||
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: <div class="min-h-screen bg-gray-50 flex items-center justify-center">
|
||||
<router-view /> ← renders the auth page card
|
||||
</div>
|
||||
Note: AuthLayout already contains its own <router-view /> — App.vue must NOT add
|
||||
a second one when AuthLayout is active.
|
||||
|
||||
From frontend/src/App.vue (current):
|
||||
Unconditionally renders <AppSidebar /> + <router-view /> — 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 <a :href="qrUri"> 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 <SettingsPreferencesTab />
|
||||
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).
|
||||
</interfaces>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Verify backend fix + regression test for admin create_user (GAP 1)</name>
|
||||
<files>backend/api/admin.py, backend/tests/test_admin_api.py</files>
|
||||
<action>
|
||||
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 239–260. 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.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest tests/test_admin_api.py::test_create_user_writes_audit_log -v</automated>
|
||||
</verify>
|
||||
<done>test_create_user_writes_audit_log passes; session.flush() confirmed present before write_audit_log() in create_user handler</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Auth route layout switching + admin role guard (GAPs 2, 3, 4)</name>
|
||||
<files>frontend/src/router/index.js, frontend/src/App.vue</files>
|
||||
<action>
|
||||
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'`
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- 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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: AccountView merged into SettingsView as Account tab + QR code in TotpEnrollment (GAPs 3 and 5)</name>
|
||||
<files>frontend/src/views/SettingsView.vue, frontend/src/components/auth/TotpEnrollment.vue, frontend/package.json</files>
|
||||
<action>
|
||||
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).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | tail -5</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- 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.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<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>
|
||||
|
||||
<verification>
|
||||
Run all checks from the project root:
|
||||
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/02-users-authentication/02-06-SUMMARY.md` when done.
|
||||
</output>
|
||||
@@ -0,0 +1,404 @@
|
||||
---
|
||||
phase: "06.2"
|
||||
plan: "05"
|
||||
type: execute
|
||||
wave: 3
|
||||
depends_on:
|
||||
- "06.2-04"
|
||||
files_modified:
|
||||
- frontend/src/views/AccountView.vue
|
||||
- frontend/src/components/admin/AdminUsersTab.vue
|
||||
- frontend/src/views/CloudFolderView.vue
|
||||
- frontend/src/components/admin/AuditLogTab.vue
|
||||
autonomous: true
|
||||
gap_closure: true
|
||||
requirements:
|
||||
- SHARE-03
|
||||
- ADMIN-06
|
||||
must_haves:
|
||||
truths:
|
||||
- "User can see their own @handle in Account settings — enabling them to share their handle with others for document sharing"
|
||||
- "Admin can see each user's handle in the Users tab — enabling handle lookup for support and sharing"
|
||||
- "Cloud folder browser shows an actionable error when no cloud connection exists — directing user to Settings"
|
||||
- "Audit log entries display @alice style handles (@ prefix present)"
|
||||
- "Export CSV button shows active filter count when filters are set — user understands export scope before clicking"
|
||||
- "Clear filters button in Audit Log tab resets all filters and re-fetches unfiltered data"
|
||||
artifacts:
|
||||
- path: "frontend/src/views/AccountView.vue"
|
||||
provides: "Handle row in Account information section"
|
||||
contains: "authStore.user?.handle"
|
||||
- path: "frontend/src/components/admin/AdminUsersTab.vue"
|
||||
provides: "Handle column in users table"
|
||||
contains: "user.handle"
|
||||
- path: "frontend/src/views/CloudFolderView.vue"
|
||||
provides: "Actionable no-connection error message"
|
||||
contains: "Settings"
|
||||
- path: "frontend/src/components/admin/AuditLogTab.vue"
|
||||
provides: "@ prefix on handles, Clear filters button, active filter count indicator"
|
||||
contains: "clearFilters"
|
||||
key_links:
|
||||
- from: "AccountView.vue"
|
||||
to: "authStore.user"
|
||||
via: "authStore.user?.handle (already present in /api/auth/me response)"
|
||||
pattern: "authStore.user\\?.handle"
|
||||
- from: "AdminUsersTab.vue user row"
|
||||
to: "adminListUsers() response"
|
||||
via: "user.handle (backend returns handle in GET /api/admin/users)"
|
||||
pattern: "user\\.handle"
|
||||
- from: "AuditLogTab.vue entry.user_handle"
|
||||
to: "rendered cell"
|
||||
via: "template expression with @ prefix"
|
||||
pattern: "'@' \\+ entry.user_handle"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Close the four UAT-diagnosed gaps from 06.2-UAT.md: (1) user handle invisible in account settings and admin user list, (2) cloud folder browser shows unhelpful error when no connection exists, (3) audit log handle entries missing @ prefix, (4) CSV export gives no indication of active filters and no way to clear them.
|
||||
|
||||
Purpose: These gaps block real usage — users cannot share documents because handles are invisible, cloud storage is unusable with no diagnostic guidance, audit logs look wrong without @ prefixes, and CSV exports silently export filtered (possibly empty) data.
|
||||
|
||||
Output: Four targeted frontend changes across four files. No backend changes required.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/Users/nik/Documents/Progamming/document_scanner/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-UAT.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@/Users/nik/Documents/Progamming/document_scanner/.planning/ROADMAP.md
|
||||
@/Users/nik/Documents/Progamming/document_scanner/.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-CONTEXT.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Extracted from codebase — executor needs no further exploration. -->
|
||||
|
||||
From frontend/src/views/AccountView.vue (Account information section, lines 8-24):
|
||||
<!-- Currently renders email and role only -->
|
||||
<section class="bg-white border border-gray-200 rounded-xl p-6">
|
||||
<h3 class="font-semibold text-gray-800 mb-4">Account information</h3>
|
||||
<div class="space-y-2 text-sm text-gray-700">
|
||||
<div><span class="text-gray-500">Email:</span> {{ authStore.user?.email }}</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-gray-500">Role:</span>
|
||||
<span class="inline-flex items-center px-2 py-1 rounded text-xs font-semibold" ...>
|
||||
{{ authStore.user?.role }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<!-- authStore.user shape (from GET /api/auth/me): { id, email, handle, role, totp_enabled } -->
|
||||
<!-- authStore.user?.handle is already available — just not rendered -->
|
||||
|
||||
From frontend/src/components/admin/AdminUsersTab.vue (table head, lines 113-119):
|
||||
<thead>
|
||||
<tr class="bg-gray-50 text-left">
|
||||
<th ...>Email</th>
|
||||
<th ...>Role</th>
|
||||
<th ...>Status</th>
|
||||
<th ...>Created</th>
|
||||
<th ...>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<!-- user object shape returned by adminListUsers(): { id, handle, email, role, is_active, totp_enabled, created_at } -->
|
||||
<!-- user.handle is present in the API response (confirmed: admin.py line 63 returns "handle") -->
|
||||
|
||||
From frontend/src/views/CloudFolderView.vue (load function, lines 126-137):
|
||||
async function load() {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
try {
|
||||
const data = await api.getCloudFolders(provider.value, folderId.value ?? 'root')
|
||||
items.value = data.items ?? []
|
||||
} catch (e) {
|
||||
error.value = e.message || 'Failed to load folder contents'
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
<!-- Error rendered in template (line 36-39): -->
|
||||
<div v-else-if="error" class="text-sm text-red-500 py-8 text-center">
|
||||
{{ error }}
|
||||
<button @click="load" class="ml-2 text-indigo-600 hover:underline">Retry</button>
|
||||
</div>
|
||||
<!-- Backend returns 404 with detail "No active connection" when no cloud provider connected -->
|
||||
<!-- getCloudFolders() throws; e.message is whatever the request() wrapper extracts from the response -->
|
||||
|
||||
From frontend/src/components/admin/AuditLogTab.vue (relevant section):
|
||||
<!-- Line 95 — current handle cell (no @ prefix): -->
|
||||
<td class="px-4 py-3 text-sm text-gray-700">{{ entry.user_handle || entry.user_id || '—' }}</td>
|
||||
|
||||
<!-- Filters reactive object (lines 188-193): -->
|
||||
const filters = reactive({
|
||||
start: '',
|
||||
end: '',
|
||||
user_handle: '',
|
||||
event_type: '',
|
||||
})
|
||||
|
||||
<!-- Filter bar (lines 4-62): Apply filters button at line 44, Export CSV button at line 51 -->
|
||||
<!-- applyFilters() (line 220): resets page to 1, calls fetchLog() -->
|
||||
<!-- No clearFilters() function exists yet -->
|
||||
<!-- No active filter count indicator exists yet near Export CSV button -->
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Show handle in AccountView and AdminUsersTab</name>
|
||||
<files>frontend/src/views/AccountView.vue, frontend/src/components/admin/AdminUsersTab.vue</files>
|
||||
<action>
|
||||
CHANGE 1 — frontend/src/views/AccountView.vue
|
||||
|
||||
In the "Account information" section (lines 8-24), add a Username row immediately after the Email row and before the Role row. The new row follows the same pattern as the Email row:
|
||||
|
||||
<div><span class="text-gray-500">Username:</span> @{{ authStore.user?.handle }}</div>
|
||||
|
||||
Place this line between the email div and the role div. The @ is a literal character prepended to the handle value so users immediately recognise it as their sharing handle. No script changes needed — authStore.user?.handle is already available.
|
||||
|
||||
CHANGE 2 — frontend/src/components/admin/AdminUsersTab.vue
|
||||
|
||||
Add a "Handle" column to the users table so admins can look up other users' handles.
|
||||
|
||||
In the `<thead>` row (after the Email th and before the Role th), add:
|
||||
<th class="px-4 py-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Handle</th>
|
||||
|
||||
In the `<tbody>` rows (after the email `<td>` and before the role `<td>`), add:
|
||||
<td class="px-4 py-3 text-gray-500 font-mono text-xs">{{ user.handle ? '@' + user.handle : '—' }}</td>
|
||||
|
||||
No script changes needed — adminListUsers() already returns handle in the user object (confirmed in backend/api/admin.py line 63).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -n "authStore.user?.handle" /Users/nik/Documents/Progamming/document_scanner/frontend/src/views/AccountView.vue && grep -n "user\.handle" /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/admin/AdminUsersTab.vue | grep -v "handle:"</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- `grep "authStore.user?.handle" frontend/src/views/AccountView.vue` returns a match in the template section
|
||||
- `grep "user\.handle" frontend/src/components/admin/AdminUsersTab.vue` returns a match in both thead and tbody
|
||||
- `cd frontend && npm run build 2>&1 | grep -i "error" | grep -v "^>"` returns no output
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Actionable cloud connection error and audit log @ prefix</name>
|
||||
<files>frontend/src/views/CloudFolderView.vue, frontend/src/components/admin/AuditLogTab.vue</files>
|
||||
<action>
|
||||
CHANGE 1 — frontend/src/views/CloudFolderView.vue
|
||||
|
||||
Replace the generic error handler in the `load()` function with one that distinguishes "no connection" from a general error. The backend returns a response whose error detail contains "No active connection" (or HTTP 404) when no cloud provider is connected.
|
||||
|
||||
Replace the catch block in load():
|
||||
|
||||
} catch (e) {
|
||||
const msg = e.message || ''
|
||||
if (msg.toLowerCase().includes('no active connection') || msg.includes('404') || msg.toLowerCase().includes('not found')) {
|
||||
error.value = 'No cloud provider connected. Go to Settings to connect a cloud storage account.'
|
||||
} else {
|
||||
error.value = msg || 'Failed to load folder contents.'
|
||||
}
|
||||
}
|
||||
|
||||
Also update the error template block (lines 36-39) to add a Settings link. Replace the existing error div with:
|
||||
|
||||
<div v-else-if="error" class="text-sm text-red-500 py-8 text-center">
|
||||
<p>{{ error }}</p>
|
||||
<div class="flex items-center justify-center gap-3 mt-2">
|
||||
<router-link
|
||||
to="/settings"
|
||||
class="text-indigo-600 hover:underline text-sm"
|
||||
>
|
||||
Go to Settings
|
||||
</router-link>
|
||||
<button @click="load" class="text-indigo-600 hover:underline text-sm">
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Confirm `router-link` is usable here — `useRouter` and `useRoute` are already imported from 'vue-router' in the script setup.
|
||||
|
||||
CHANGE 2 — frontend/src/components/admin/AuditLogTab.vue
|
||||
|
||||
Change the user handle cell (line 95) from:
|
||||
{{ entry.user_handle || entry.user_id || '—' }}
|
||||
to:
|
||||
{{ entry.user_handle ? '@' + entry.user_handle : (entry.user_id || '—') }}
|
||||
|
||||
This is a template-only one-liner change. No script changes required.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -n "No cloud provider connected" /Users/nik/Documents/Progamming/document_scanner/frontend/src/views/CloudFolderView.vue && grep -n "'@' + entry.user_handle" /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/admin/AuditLogTab.vue</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- `grep "No cloud provider connected" frontend/src/views/CloudFolderView.vue` returns a match
|
||||
- `grep "Go to Settings" frontend/src/views/CloudFolderView.vue` returns a match
|
||||
- `grep "'@' + entry.user_handle" frontend/src/components/admin/AuditLogTab.vue` returns a match
|
||||
- `grep "entry.user_handle || entry.user_id" frontend/src/components/admin/AuditLogTab.vue` returns NO match (old pattern gone)
|
||||
- `cd frontend && npm run build 2>&1 | grep -i "error" | grep -v "^>"` returns no output
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Clear filters button and active filter count indicator in AuditLogTab</name>
|
||||
<files>frontend/src/components/admin/AuditLogTab.vue</files>
|
||||
<action>
|
||||
Add two UX improvements to AuditLogTab.vue to make the CSV export scope transparent.
|
||||
|
||||
CHANGE 1 — Add clearFilters() function in the script setup section:
|
||||
|
||||
Add the following function after the existing applyFilters() function:
|
||||
|
||||
function clearFilters() {
|
||||
filters.start = ''
|
||||
filters.end = ''
|
||||
filters.user_handle = ''
|
||||
filters.event_type = ''
|
||||
page.value = 1
|
||||
fetchLog()
|
||||
}
|
||||
|
||||
Also add a computed property (or inline expression) for active filter count. Add this computed after the clearFilters() function:
|
||||
|
||||
import { computed } from 'vue' // add computed to the existing vue import if not present
|
||||
|
||||
const activeFilterCount = computed(() => {
|
||||
let count = 0
|
||||
if (filters.start) count++
|
||||
if (filters.end) count++
|
||||
if (filters.user_handle) count++
|
||||
if (filters.event_type) count++
|
||||
return count
|
||||
})
|
||||
|
||||
NOTE: `computed` must be added to the existing `import { ref, reactive, onMounted }` line at the top of the script. Change it to `import { ref, reactive, onMounted, computed }`.
|
||||
|
||||
CHANGE 2 — Add "Clear filters" button to filter bar in the template:
|
||||
|
||||
In the filter bar (the `<div class="flex flex-wrap gap-3 mb-4 items-end">` block), add a "Clear filters" button immediately after the existing "Apply filters" button. Only show it when filters are active:
|
||||
|
||||
<button
|
||||
v-if="activeFilterCount > 0"
|
||||
@click="clearFilters"
|
||||
class="border border-gray-300 text-gray-500 text-sm px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Clear filters
|
||||
</button>
|
||||
|
||||
CHANGE 3 — Add active filter count indicator near Export CSV button:
|
||||
|
||||
Wrap the existing Export CSV button in a relative container and add a badge showing the active filter count when non-zero. Replace the standalone Export CSV button block with:
|
||||
|
||||
<div class="relative inline-flex flex-col items-start gap-1">
|
||||
<button
|
||||
@click="exportCsv"
|
||||
:disabled="exportingCsv"
|
||||
class="border border-gray-300 text-gray-700 text-sm px-4 py-2 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
|
||||
>
|
||||
<span v-if="exportingCsv" class="flex items-center gap-1">
|
||||
<span class="animate-spin rounded-full border-2 border-current border-t-transparent w-3 h-3"></span>
|
||||
Exporting…
|
||||
</span>
|
||||
<span v-else>Export CSV</span>
|
||||
</button>
|
||||
<span
|
||||
v-if="activeFilterCount > 0"
|
||||
class="text-xs text-amber-600"
|
||||
>
|
||||
{{ activeFilterCount }} filter{{ activeFilterCount !== 1 ? 's' : '' }} active
|
||||
</span>
|
||||
</div>
|
||||
|
||||
The amber text "N filter(s) active" sits directly below the Export CSV button so users see at a glance that the download will be scoped. The existing `<p v-if="exportError">` block remains unchanged immediately after this new wrapper div.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>grep -n "clearFilters\|activeFilterCount\|Clear filters\|filters active" /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/admin/AuditLogTab.vue | head -15</automated>
|
||||
</verify>
|
||||
<done>
|
||||
- `grep "clearFilters" frontend/src/components/admin/AuditLogTab.vue` returns at least 2 matches (definition + @click binding)
|
||||
- `grep "activeFilterCount" frontend/src/components/admin/AuditLogTab.vue` returns at least 3 matches (computed definition + v-if + template text)
|
||||
- `grep "Clear filters" frontend/src/components/admin/AuditLogTab.vue` returns a match in the template
|
||||
- `grep "filters active" frontend/src/components/admin/AuditLogTab.vue` returns a match
|
||||
- `grep "computed" frontend/src/components/admin/AuditLogTab.vue` returns a match in the import line
|
||||
- `cd frontend && npm run build 2>&1 | grep -i "error" | grep -v "^>"` returns no output
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
## Trust Boundaries
|
||||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| AccountView → authStore.user | handle is read from in-memory Pinia store — never from localStorage; no user-supplied input involved |
|
||||
| CloudFolderView → error message | error text originates from backend API response; rendered via Vue template auto-escaping (no innerHTML) — XSS risk mitigated |
|
||||
| AuditLogTab → entry.user_handle | handle value from API response rendered via Vue template auto-escaping — no innerHTML |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-06.2-05-01 | Information Disclosure | Handle visible in AccountView | accept | Handle is already a public-within-platform identifier (used as share target); displaying it to the owning user is expected and correct |
|
||||
| T-06.2-05-02 | Information Disclosure | Handle visible in AdminUsersTab | accept | Admin already has access to email; handle is lower-sensitivity than email; admin-only endpoint already enforces get_current_admin |
|
||||
| T-06.2-05-03 | XSS | Cloud error message rendered from API response | mitigate | Vue template auto-escaping prevents XSS; the error string is interpolated via {{ }} not v-html — no raw HTML injection possible |
|
||||
| T-06.2-05-04 | XSS | @ + entry.user_handle rendered in table | mitigate | String concatenation in Vue template expression is auto-escaped — not v-html |
|
||||
| T-06.2-05-SC | Tampering | npm/pip/cargo installs | accept | No new packages installed in this plan — frontend-only template and script changes only |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
After all three tasks complete:
|
||||
|
||||
Build check (no errors):
|
||||
```
|
||||
cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build 2>&1 | grep -i "error" | grep -v "^>" | head -10
|
||||
```
|
||||
Expected: no output.
|
||||
|
||||
Gap 1 — Handle in AccountView:
|
||||
```
|
||||
grep -n "authStore.user?.handle" /Users/nik/Documents/Progamming/document_scanner/frontend/src/views/AccountView.vue
|
||||
```
|
||||
Expected: match in template section.
|
||||
|
||||
Gap 1 — Handle in AdminUsersTab:
|
||||
```
|
||||
grep -n "user\.handle" /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/admin/AdminUsersTab.vue
|
||||
```
|
||||
Expected: at least 2 matches (thead + tbody).
|
||||
|
||||
Gap 2 — Cloud actionable error:
|
||||
```
|
||||
grep -n "No cloud provider connected\|Go to Settings" /Users/nik/Documents/Progamming/document_scanner/frontend/src/views/CloudFolderView.vue
|
||||
```
|
||||
Expected: 2 matches.
|
||||
|
||||
Gap 3 — Audit log @ prefix:
|
||||
```
|
||||
grep -n "'@' + entry.user_handle" /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/admin/AuditLogTab.vue
|
||||
```
|
||||
Expected: 1 match.
|
||||
|
||||
Gap 4 — Clear filters + filter count:
|
||||
```
|
||||
grep -c "clearFilters\|activeFilterCount" /Users/nik/Documents/Progamming/document_scanner/frontend/src/components/admin/AuditLogTab.vue
|
||||
```
|
||||
Expected: 5 or more matches total.
|
||||
|
||||
Backend test suite unaffected (no backend changes):
|
||||
```
|
||||
cd /Users/nik/Documents/Progamming/document_scanner/backend && pytest -x -q 2>&1 | tail -5
|
||||
```
|
||||
Expected: exits 0, same pass count as before this plan.
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Account settings page shows the user's own @handle in the Account information section
|
||||
- Admin Users tab includes a Handle column showing @handle for every user row
|
||||
- Cloud folder browser shows "No cloud provider connected. Go to Settings to connect a cloud storage account." (with a Settings link) when backend returns a no-connection error
|
||||
- Audit log table renders @alice style handles (@ prefix present on all non-null handles)
|
||||
- AuditLogTab has a "Clear filters" button (visible only when at least one filter is active) that resets all filters and re-fetches
|
||||
- Export CSV button area shows "N filter(s) active" in amber text when one or more filters are set
|
||||
- `npm run build` exits 0 with no errors
|
||||
- Backend pytest suite still passes (no regressions — this plan touches only frontend files)
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
Create `.planning/phases/06.2-close-v1-sharing-cloud-delete-csv-export-gaps/06.2-05-SUMMARY.md` when done.
|
||||
</output>
|
||||
Reference in New Issue
Block a user