""" Cache settings and status endpoint — Phase 14. Owner-scoped endpoints for the byte cache. Admin users are blocked by get_regular_user (T-14-01, T-14-06). Route family: GET /analysis/cache — current cache usage and user settings PATCH /analysis/cache/settings — update analysis preferences and cache limit Security invariants (T-14-02, T-14-08): - object_key, credentials_enc, and raw provider URLs must never appear in any response body. - All queries are scoped to current_user.id — no cross-user access is possible. """ from __future__ import annotations from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Request, status from sqlalchemy import func, select from sqlalchemy.ext.asyncio import AsyncSession from api.cloud.schemas import CacheStatusOut, CacheSettingsUpdateRequest from db.models import CloudByteCacheEntry, User from deps.auth import get_regular_user from deps.db import get_db from services.cloud_analysis_settings import ( DEFAULT_MAX_CACHE_LIMIT_BYTES, get_or_create_user_analysis_settings, update_user_analysis_settings, ) from services.rate_limiting import account_limiter router = APIRouter() @router.get("/analysis/cache", response_model=CacheStatusOut) @account_limiter.limit("120/minute") async def get_cache_status( request: Request, session: AsyncSession = Depends(get_db), current_user: User = Depends(get_regular_user), ) -> CacheStatusOut: """Return cache usage totals and analysis settings for the current user. Returns aggregate byte/count totals (no object_key or credentials). Blocked for admin accounts (T-14-01). """ request.state.current_user = current_user # Aggregate active (non-evicted) cache entries for this user result = await session.execute( select( func.count(CloudByteCacheEntry.id).label("entry_count"), func.coalesce(func.sum(CloudByteCacheEntry.size_bytes), 0).label("total_bytes"), ).where( CloudByteCacheEntry.user_id == current_user.id, CloudByteCacheEntry.evicted_at.is_(None), ) ) row = result.one() entry_count = int(row.entry_count) total_bytes = int(row.total_bytes) # Fetch or create settings (never fails — creates defaults on first access) settings = await get_or_create_user_analysis_settings( session, user_id=current_user.id ) return CacheStatusOut( entry_count=entry_count, total_bytes=total_bytes, cache_limit_bytes=settings.cloud_cache_limit_bytes, tier_cap_bytes=DEFAULT_MAX_CACHE_LIMIT_BYTES, analysis_progress_detail=settings.analysis_progress_detail, analysis_failure_behavior=settings.analysis_failure_behavior, ) @router.patch("/analysis/cache/settings", response_model=CacheStatusOut) @account_limiter.limit("30/minute") async def update_cache_settings( request: Request, body: CacheSettingsUpdateRequest, session: AsyncSession = Depends(get_db), current_user: User = Depends(get_regular_user), ) -> CacheStatusOut: """Update analysis preferences and/or the user's cache byte limit. Only provided (non-None) fields are updated. Enum and bounds validation is performed by the service layer (raises ValueError; we re-raise as 422). Blocked for admin accounts (T-14-01). """ request.state.current_user = current_user try: await update_user_analysis_settings( session, user_id=current_user.id, analysis_progress_detail=body.analysis_progress_detail, analysis_failure_behavior=body.analysis_failure_behavior, cloud_cache_limit_bytes=body.cloud_cache_limit_bytes, max_cache_limit_bytes=DEFAULT_MAX_CACHE_LIMIT_BYTES, ) except ValueError as exc: raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, detail=str(exc), ) from exc await session.commit() # Return fresh status after update result = await session.execute( select( func.count(CloudByteCacheEntry.id).label("entry_count"), func.coalesce(func.sum(CloudByteCacheEntry.size_bytes), 0).label("total_bytes"), ).where( CloudByteCacheEntry.user_id == current_user.id, CloudByteCacheEntry.evicted_at.is_(None), ) ) row = result.one() settings = await get_or_create_user_analysis_settings( session, user_id=current_user.id ) return CacheStatusOut( entry_count=int(row.entry_count), total_bytes=int(row.total_bytes), cache_limit_bytes=settings.cloud_cache_limit_bytes, tier_cap_bytes=DEFAULT_MAX_CACHE_LIMIT_BYTES, analysis_progress_detail=settings.analysis_progress_detail, analysis_failure_behavior=settings.analysis_failure_behavior, )