refactor(09-05): CODE-09 purge — Phase 8 backend sub-packages (WHAT comments removed)

- api/admin/users.py: removed module docstring + 7 WHAT function docstrings
- api/admin/quotas.py: removed module docstring + 2 WHAT function docstrings
- api/admin/ai.py: removed module docstring + 3 WHAT inline comments; security invariant docstrings preserved
- api/admin/shared.py: unchanged (all comments are WHY — constraint notes)
- api/documents/upload.py: removed 4 WHAT inline comments; T-03-05/T-03-06 WHY notes preserved
- api/documents/content.py: removed WHAT function docstring + WHAT inline comment
- api/documents/crud.py: removed 7 WHAT inline comments; D-16 + security constraint docstrings preserved
- api/auth/shared.py: removed module docstring + 1 WHAT inline comment
- api/auth/tokens.py: removed module docstring + 8 WHAT function docstrings/comments; family-revocation + SEC-02 WHY notes preserved
- api/auth/totp.py: removed module docstring + 2 WHAT function docstrings + 2 WHAT inline comments
- api/auth/password.py: removed module docstring + 2 WHAT function docstrings + 3 WHAT inline comments
- All NO-prefix anchor comments and security-invariant WHY comments preserved
- pytest: 413 passed, 1 failed (pre-existing ModuleNotFoundError unrelated to purge)
This commit is contained in:
curo1305
2026-06-12 16:59:46 +02:00
parent 9ddd599897
commit 6d1c02f703
10 changed files with 8 additions and 268 deletions
+4 -70
View File
@@ -1,17 +1,3 @@
"""
Auth API — token-issuing endpoints.
Handles:
POST /register — new user registration with HIBP check
POST /login — login with optional TOTP/backup-code second factor
POST /refresh — rotate refresh token (httpOnly cookie in/out)
POST /logout — revoke current refresh token, clear cookie
POST /logout-all — revoke all refresh tokens for current user
GET /me — return current user profile
GET /me/quota — return current user quota
GET /me/preferences — return current user preferences
PATCH /me/preferences — update current user preferences
"""
from __future__ import annotations
import hashlib
@@ -51,27 +37,17 @@ async def register(
body: RegisterRequest,
session: AsyncSession = Depends(get_db),
):
"""Register a new user account.
- Validates password strength (min 12 chars, upper, lower, digit, special)
- Checks HIBP k-anonymity API for breached passwords
- Hashes password with Argon2
- Inserts User + Quota rows in a single transaction
"""
# Password strength check
try:
auth_service.validate_password_strength(body.password)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc))
# HIBP breach check
if await auth_service.check_hibp(body.password):
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="This password has appeared in a data breach. Choose a different password.",
)
# Duplicate email/handle check
result = await session.execute(
select(User).where(
(User.email == str(body.email)) | (User.handle == body.handle)
@@ -83,7 +59,6 @@ async def register(
detail="Email or handle already in use",
)
# Create user and quota
user_id = uuid.uuid4()
new_user = User(
id=user_id,
@@ -132,23 +107,11 @@ async def login(
response: Response,
session: AsyncSession = Depends(get_db),
):
"""Authenticate a user and issue tokens.
Per-account rate limiting (SEC-02): checks Redis counter keyed by email
BEFORE any DB lookup to prevent enumeration timing attacks.
Three login flows:
1. No TOTP enabled: password → tokens
2. TOTP enabled, no code provided: requires_totp = True (challenge)
3. TOTP enabled, totp_code provided: verify TOTP → tokens
4. TOTP enabled, backup_code provided (no totp_code): verify backup → tokens
"""
# Per-account rate limiting (SEC-02)
# Per-account rate limiting (SEC-02): Redis counter keyed by email, checked before any DB lookup
redis_client = request.app.state.redis
rate_key = f"login_attempts:{body.email}"
count = await redis_client.incr(rate_key)
if count == 1:
# Set TTL only on first increment (15-minute window)
await redis_client.expire(rate_key, 900)
if count > 10:
raise HTTPException(
@@ -156,11 +119,9 @@ async def login(
detail="Too many login attempts. Try again in 15 minutes.",
)
# Look up user by email
result = await session.execute(select(User).where(User.email == str(body.email)))
user: Optional[User] = result.scalar_one_or_none()
# IP extraction for audit log (used in both success and failure paths)
_ip = get_client_ip(request)
# Verify password (anti-enumeration: same error regardless of whether user exists)
@@ -180,7 +141,6 @@ async def login(
detail="Incorrect email or password",
)
# Active check
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -191,10 +151,8 @@ async def login(
if user.password_must_change:
return {"requires_password_change": True, "user_id": str(user.id)}
# TOTP second-factor dispatch
if user.totp_enabled:
if body.totp_code is None and body.backup_code is None:
# Challenge: prompt for second factor
return {"requires_totp": True}
if body.totp_code is not None:
@@ -223,7 +181,6 @@ async def login(
ip_address=_ip,
)
# Issue tokens
access_token = auth_service.create_access_token(
str(user.id),
user.role,
@@ -266,12 +223,7 @@ async def refresh_token(
response: Response,
session: AsyncSession = Depends(get_db),
):
"""Rotate the refresh token.
Reads the refresh_token httpOnly cookie; on success issues a new access
token and rotates the refresh cookie.
On token reuse (revoked token presented), revokes entire family and raises 401.
"""
# Token reuse (revoked token presented) revokes entire family — family revocation security contract
raw_token = request.cookies.get("refresh_token")
if not raw_token:
raise HTTPException(
@@ -292,7 +244,6 @@ async def refresh_token(
detail="Invalid or expired refresh token",
) from exc
# Look up user for response body
user = await session.get(User, uuid.UUID(user_id_str))
if user is None or not user.is_active:
raise HTTPException(
@@ -300,7 +251,6 @@ async def refresh_token(
detail="User not found or deactivated",
)
# Set new refresh cookie
_set_refresh_cookie(response, new_raw)
access_token = auth_service.create_access_token(
@@ -325,7 +275,6 @@ async def refresh_token(
@router.post("/logout")
async def logout(request: Request, response: Response, session: AsyncSession = Depends(get_db)):
"""Revoke current refresh token and clear the cookie."""
import hashlib as _hashlib
_ip = get_client_ip(request)
@@ -365,7 +314,6 @@ async def logout_all(
session: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Sign out of all devices: revoke all refresh tokens for current user."""
_ip = get_client_ip(request)
count = await auth_service.revoke_all_refresh_tokens(session, current_user.id)
# D-13: sign-out-all event
@@ -387,7 +335,6 @@ async def logout_all(
@router.get("/me")
async def get_me(current_user: User = Depends(get_current_user)):
"""Return the current user's profile (requires valid Bearer token)."""
return _user_dict(current_user)
@@ -398,11 +345,6 @@ async def get_my_quota(
current_user: User = Depends(get_current_user),
session: AsyncSession = Depends(get_db),
):
"""Return the current user's quota usage (STORE-04).
Returns {"used_bytes": int, "limit_bytes": int} for the sidebar quota bar.
Quota row is created at registration (100 MB default — STORE-01).
"""
q = await session.get(Quota, current_user.id)
if q is None:
raise HTTPException(status_code=404, detail="Quota not found")
@@ -415,11 +357,7 @@ async def get_my_quota(
async def get_my_preferences(
current_user: User = Depends(get_current_user),
):
"""Return the current user's PDF open mode preference (D-10).
Both regular users and admins can read their own preferences.
Falls back to 'in_app' if the column is absent (migration not yet run).
"""
# Falls back to 'in_app' if the column is absent (migration not yet run)
try:
pdf_open_mode = current_user.pdf_open_mode
except AttributeError:
@@ -435,11 +373,7 @@ async def update_my_preferences(
session: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""Update the current user's PDF open mode preference (D-10).
Both regular users and admins can update their own preferences.
Pydantic Literal["in_app", "new_tab"] enforces strict allowlist (T-04-05-05).
"""
# Pydantic Literal["in_app", "new_tab"] enforces strict allowlist (T-04-05-05)
user = await session.get(User, current_user.id)
if user is None:
raise HTTPException(status_code=404, detail="User not found")