W-04: Replace inline lockout logic in totp.py (3 occurrences of
manual failed_login_count/locked_until manipulation) with shared
session service calls: check_account_lockout, record_failed_login,
record_successful_login. Also fix TOTP replay prevention to use
flush() not commit() for atomicity with session creation.
S-1: Add "Set up" action button to the post-login passkey prompt
toast, navigating to /settings?tab=security (already supported by
SettingsPage search params).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Extract _create_db_session, _set_session_cookie, _check_account_lockout,
_record_failed_login, and _record_successful_login from auth.py into
services/session.py. Update totp.py to use shared service instead of
its duplicate _create_full_session (which lacked session cap enforcement).
Also fixes:
- auth/status N+1 query (2 sequential queries -> single JOIN)
- Rename verify_password route to verify_password_endpoint (shadow fix)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 fixes:
- W-01: Add start_period: 30s to backend healthcheck for migration window
- W-03: Narrow .dockerignore *.md to specific files (preserve alembic/README)
Phase 2 fixes:
- C-01: Wrap Argon2id calls in totp.py (disable, regenerate, backup verify,
backup store) — missed in initial AC-2 pass
- S-01: Extract async wrappers (ahash_password, averify_password,
averify_password_with_upgrade) into services/auth.py, refactor all
callers to use them instead of manual run_in_executor boilerplate
- W-01: Fix ntfy dedup regression — commit per category instead of per-user
to preserve dedup records if a later category fails
Phase 4 fixes:
- C-01: Fix optimistic drag-and-drop cache key to include date range
- C-02: Replace toISOString() with format() to avoid UTC date shift in
visible range calculation
- W-02: Initialize visibleRange from current month to eliminate unscoped
first fetch + immediate refetch
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Nginx already forwards X-Forwarded-For and X-Real-IP, but the backend
read request.client.host directly — always returning 172.18.0.x. Added
get_client_ip() helper to audit service; updated all 13 call sites.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add max_length constraints to all string fields in request schemas,
matching DB column limits (title:255, description:5000, etc.)
- Add min_length=1 to required name/title fields
- Add ConfigDict(extra="forbid") to all request schemas to reject
unknown fields (prevents silent field injection)
- Add Path(ge=1, le=2147483647) to all integer path parameters across
all routers to prevent integer overflow → 500 errors
- Add max_length to TOTP inline schemas (code:6, mfa_token:256, etc.)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- backend: add POST /auth/totp/enforce-setup and /auth/totp/enforce-confirm
endpoints that operate on mfa_enforce_token (not session cookie), generate
TOTP secret/QR/backup codes, verify confirmation code, enable TOTP, clear
mfa_enforce_pending flag, and issue a full session cookie
- frontend: expand LockScreen to five modes — login, first-run setup,
open registration, TOTP challenge, MFA enforcement setup (QR -> verify ->
backup codes), and forced password change; all modes share AmbientBackground
and the existing card layout; registration visible only when
authStatus.registration_open is true
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>