14 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 | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07.1 | 02 | execute | 2 |
|
|
true |
|
|
Purpose: Closes the test coverage gap (CLAUDE.md testing protocol: every new behavior must have at least one test) and delivers the user-facing UX signal described in D-05.
Output: 3 new pytest tests in test_auth_api.py; toast in SettingsAccountTab.vue and TotpEnrollment.vue.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/phases/07.1-security-session-revocation-on-privilege-change/07.1-CONTEXT.md @.planning/phases/07.1-security-session-revocation-on-privilege-change/07.1-01-SUMMARY.md Task 1: Add tests for sessions_revoked behavior on all three endpoints backend/tests/test_auth_api.py<read_first>
backend/tests/test_auth_api.py — read lines 1-50 (imports, fixture helpers _register and _login) and lines 240-258 (test_change_password_success — the pattern to extend). These show how to register a user, obtain a token, and call change-password.
backend/tests/conftest.py — check the authed_client, auth_user, and db_session fixture definitions to understand what is available.
backend/api/auth.py — check the updated change_password, enable_totp, and disable_totp response shapes from Plan 01 (to match sessions_revoked key name exactly).
</read_first>
For each test, import pattern: use `from services import auth as auth_service` (already available in the test module via existing imports — verify before adding) and the `db_session: AsyncSession` fixture.
To simulate "other sessions": call `await auth_service.create_refresh_token(db_session, user.id)` to insert an additional token row. This is the canonical way to create a second session without running a full login flow (which requires the refresh cookie roundtrip).
After the privilege-change API call, query `select(RefreshToken).where(RefreshToken.user_id == user.id)` and inspect the rows:
- The row matching the token created by `create_refresh_token` (the "other session") must have `revoked=True`.
- If the `authed_client` fixture set a real refresh_token cookie, the calling session's row must have `revoked=False`. If the fixture does NOT set a cookie (verify by reading conftest), then `skip_token_hash` will be `None` and all tokens including the "other" one will be revoked — the test still asserts `sessions_revoked >= 1`.
For `test_enable_totp_revokes_other_sessions`: patch `services.auth.verify_totp` to return `True` so the TOTP code check is bypassed (same pattern as existing TOTP tests). Also patch `services.auth.store_backup_codes` to avoid real Argon2 hashing overhead.
Import `from sqlalchemy import select` and `from models import RefreshToken` for the DB assertion queries — check existing imports in the test file first to avoid duplicates.
Use `with patch(...)` context managers exactly as in the existing breach-check tests.
<acceptance_criteria>
- pytest tests/test_auth_api.py -k "revokes_other_sessions" -v reports 3 PASSED tests with no failures
- Each test asserts resp.json()["sessions_revoked"] >= 1
- Each test queries the DB and asserts the "other session" RefreshToken row has revoked=True
- Full test suite pytest -x -q still passes with no regressions
</acceptance_criteria>
<read_first>
frontend/src/components/settings/SettingsAccountTab.vue — read the full file (254 lines). Observe: changePassword() at line 193 calls api.changePassword(...) and sets passwordSuccess; disableTotp() at line 226 calls api.totpDisable(). Note there is no existing toast in this component — the toast pattern to follow is in SettingsView.vue (the oauthSuccessProvider inline HTML block at lines 6-28).
frontend/src/views/SettingsView.vue — read lines 1-30 (the OAuth success toast block). This is the exact inline HTML pattern to replicate in SettingsAccountTab.
frontend/src/components/auth/TotpEnrollment.vue — read lines 145-166 (confirmEnrollment() function that calls api.totpEnable()). Note the emit('enrolled') call at line 165 and the backupCodes data returned at line 150.
</read_first>
Add a new reactive ref `sessionRevokedToast = ref(false)` alongside the existing refs in the `<script setup>` block.
Add a toast HTML block in the `<template>`, positioned ABOVE the `<div class="space-y-6">` container, following the exact same structural pattern as the OAuth success toast in SettingsView.vue:
- `v-if="sessionRevokedToast"` condition
- `class="fixed top-4 right-4 z-50 ..."` positioning (same Tailwind classes)
- Green success icon (same SVG as the OAuth toast)
- Message text: "Other sessions have been terminated." (per D-05)
- Dismiss button that sets `sessionRevokedToast = false`
In the `changePassword()` function: capture the response from `api.changePassword(...)` as `const data = await api.changePassword(...)`. After the existing `passwordSuccess.value = 'Password updated.'` line, add:
```
if (data.sessions_revoked > 0) {
sessionRevokedToast.value = true
setTimeout(() => { sessionRevokedToast.value = false }, 5000)
}
```
In the `disableTotp()` function: capture the response as `const data = await api.totpDisable()`. After the existing `confirmDisable2fa.value = false` line, add:
```
if (data.sessions_revoked > 0) {
sessionRevokedToast.value = true
setTimeout(() => { sessionRevokedToast.value = false }, 5000)
}
```
### TotpEnrollment.vue changes
In `confirmEnrollment()` at the `api.totpEnable()` call: capture the response as `const data = await api.totpEnable(verifyCode.value)`. The existing code already uses `data.backup_codes` at line 150 — keep that. After the `backupCodes.value = data.backup_codes` line, add:
```
if (data.sessions_revoked > 0) {
sessionRevokedToast.value = true
setTimeout(() => { sessionRevokedToast.value = false }, 5000)
}
```
Add `const sessionRevokedToast = ref(false)` to the reactive state in `<script setup>`.
Add the same toast HTML block to TotpEnrollment's template, scoped to the component's root element (not fixed-position, since this is a component not a full view — use relative positioning: `class="mb-4 flex items-center gap-3 bg-white border border-green-200 rounded-xl px-5 py-4"` inside the component's template root). Show it `v-if="sessionRevokedToast"` with the text "Other sessions have been terminated." and a dismiss button.
Do NOT use a fixed-position toast inside TotpEnrollment (it is an embedded component, not a top-level view). Use inline alert style instead.
No new dependencies. No store changes. No route changes.
<acceptance_criteria>
- grep -n "sessions_revoked" frontend/src/components/settings/SettingsAccountTab.vue returns at least 2 lines (changePassword + disableTotp handlers)
- grep -n "sessions_revoked" frontend/src/components/auth/TotpEnrollment.vue returns at least 1 line (confirmEnrollment handler)
- grep -n "sessionRevokedToast" frontend/src/components/settings/SettingsAccountTab.vue returns at least 3 lines (ref declaration + v-if + setTimeout)
- grep -n "sessionRevokedToast" frontend/src/components/auth/TotpEnrollment.vue returns at least 3 lines (ref declaration + v-if + setTimeout)
- npm run build in the frontend directory exits with code 0
- "Other sessions have been terminated." text present in both files
</acceptance_criteria>
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| API response → frontend | sessions_revoked int from API response drives toast display — no user-supplied data rendered as HTML |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-7.1-04 | Information Disclosure | sessions_revoked toast | accept | Toast shows count-based boolean (>0), not the actual count or token details — no PII exposed |
| T-7.1-05 | Tampering | test fixture DB inserts | accept | Tests use internal auth_service.create_refresh_token — no external input path |
| T-7.1-SC | Tampering | npm/pip installs | accept | No new packages installed in this plan — no legitimacy gate required |
| </threat_model> |
cd /Users/nik/Documents/Progamming/document_scanner/backend && python -m pytest tests/test_auth_api.py -k "revokes_other_sessions" -v— 3 PASSEDcd /Users/nik/Documents/Progamming/document_scanner/backend && python -m pytest -x -q— zero failures (existing test count unchanged + 3 new passing)cd /Users/nik/Documents/Progamming/document_scanner/frontend && npm run build— exits 0grep -c "sessions_revoked" frontend/src/components/settings/SettingsAccountTab.vue— 2 or moregrep -c "sessions_revoked" frontend/src/components/auth/TotpEnrollment.vue— 1 or more
<success_criteria>
- 3 new tests in
test_auth_api.pynamedtest_*_revokes_other_sessions, all PASSING - Each test asserts
resp.json()["sessions_revoked"] >= 1 - Frontend
SettingsAccountTab.vueshows a 5-second "Other sessions have been terminated." toast afterchangePasswordanddisableTotpwhensessions_revoked > 0 - Frontend
TotpEnrollment.vueshows an inline "Other sessions have been terminated." alert afterenable_totpwhensessions_revoked > 0 - Full
pytest -x -qpasses with zero failures npm run buildexits 0 </success_criteria>