- api/auth.py login + refresh call sites pass User-Agent and Accept-Language
headers to create_access_token (D-05 — fgp binding at token issuance)
- test_auth_fgp.py: promote all 4 xfail stubs to real assertions (FGP-01..04)
- conftest.py: add _TEST_USER_AGENT constant; configure async_client to send
consistent User-Agent; update auth_user/second_auth_user/admin_user fixtures
to bind fgp to _TEST_USER_AGENT so tokens validate correctly in tests
- test_auth_deps.py: import _TEST_USER_AGENT; update auth_client fixture and
all create_access_token calls to use the constant
- test_cloud.py: update _create_user_and_token to bind fgp to _TEST_USER_AGENT
- test_documents.py: update 3 inline create_access_token calls to pass user_agent
- test_security_headers.py: import _TEST_USER_AGENT; update headers_client +
token creation to use the constant
- Version bump: backend 0.1.2 → 0.1.3, frontend 0.1.2 → 0.1.3
- [Rule 1 - Bug] Fix httpx default User-Agent vs empty-string fgp mismatch in
test infrastructure: 10 tests were failing due to fgp check rejecting tokens
created with fgp="" when client sent "python-httpx/X.Y.Z"
Two fixes from manual SLA validation (06-LOCUST-01):
1. backend/api/auth.py: concurrent registrations with same email/handle caused
UniqueViolation to bubble as 500. Now wraps session.commit() in try/except
IntegrityError → 409 Conflict. Regression covered by existing auth API tests.
2. backend/load_tests/locustfile.py: rewrote from single-shared-account to
pre-authenticated unique accounts. New @events.test_start listener creates
50 accounts in 9-user batches (65 s pause between batches to stay under
10 req/min IP rate limit). Added min-request guard to SLA check so a
run-time-consumed-by-setup result never silently "passes".
Run command updated to --run-time 12m.
Known open issue: _account_key in services/rate_limiting.py falls back to IP
when request.state.current_user is not yet set (rate limiter key_func runs
before handler body). Fix: extract sub from JWT directly. See next session.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace raw email in auth.login_failed metadata_ with a 16-char
SHA-256 prefix (attempted_email_hash). Update AuditLogTab.vue to
display the hash field instead. This removes plaintext email PII from
the unencrypted audit_logs JSONB column as required by CLAUDE.md.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- D-11: replace get_client_ip body with trusted-proxy CIDR check (127/8, 172.16/12,
192.168/16, ::1/128); untrusted peers always return their own IP, preventing XFF
spoofing by external callers
- D-12: create services/rate_limiting.py with _account_key() keyed by user.id
(falls back to peer IP when no authenticated user in request.state); exports
account_limiter = Limiter(key_func=_account_key)
- Wire: auth.py switches from get_remote_address to get_client_ip; main.py imports
account_limiter; all 9 documents.py endpoints and 7 cloud.py endpoints decorated
@account_limiter.limit("100/minute") with request.state.current_user = current_user
as the first handler statement (A1 ordering invariant)
- Promote all 8 xfail stubs to real assertions; add autouse fixture in conftest.py
to reset MemoryStorage between tests preventing cross-test 429 contamination
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add backend/ai/utils.py — parse_classification, parse_suggestions, strip_code_fences
shared by all AI providers; removes duplicated private functions from
anthropic_provider.py and openai_provider.py
- Add backend/deps/utils.py — get_client_ip, parse_uuid request-parsing helpers;
removes local _ip() variants from admin.py, auth.py, shares.py, folders.py
- Add backend/storage/exceptions.py — canonical CloudConnectionError definition;
all routers and backends import from here instead of redefining
- Move validate_password_strength to backend/services/auth.py; removes duplicated
_validate_password_strength from admin.py and auth.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- auth.py: store attempted_email in metadata_ and link user_id when the account exists (wrong password case); previously logged no PII at all
- AuditLogTab: Email column falls back to metadata_.attempted_email in amber with "(attempted)" label when no confirmed user_email is available
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add PreferencesUpdate Pydantic model with Literal['in_app', 'new_tab'] validation
- Add GET /api/auth/me/preferences — returns current pdf_open_mode
- Add PATCH /api/auth/me/preferences — validates + stores + returns updated value
- Both endpoints use get_current_user (admin can set own prefs, D-10)
- Add 7 preference tests: default GET, in_app, new_tab, invalid 422, persist,
and two unauthenticated 401 tests
Includes planning artifacts (03-CONTEXT, 03-DISCUSSION-LOG, 03-02-SUMMARY),
integration test script, MinIO/auth/docker fixes, and local dev account reference.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>