# [DEF:backend.src.api.routes.profile:Module] # # @TIER: CRITICAL # @SEMANTICS: api, profile, preferences, self-service, account-lookup # @PURPOSE: Exposes self-scoped profile preference endpoints and environment-based Superset account lookup. # @LAYER: API # @RELATION: DEPENDS_ON -> backend.src.services.profile_service # @RELATION: DEPENDS_ON -> backend.src.dependencies.get_current_user # @RELATION: DEPENDS_ON -> backend.src.core.database.get_db # # @INVARIANT: Endpoints are self-scoped and never mutate another user preference. # @UX_STATE: ProfileLoad -> Returns stable ProfilePreferenceResponse for authenticated user. # @UX_STATE: Saving -> Validation errors map to actionable 422 details. # @UX_STATE: LookupLoading -> Returns success/degraded Superset lookup payload. # @UX_FEEDBACK: Stable status/message/warning payloads support profile page feedback. # @UX_RECOVERY: Lookup degradation keeps manual username save path available. # [SECTION: IMPORTS] from typing import Optional from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from ...core.database import get_db from ...core.logger import logger, belief_scope from ...dependencies import get_config_manager, get_current_user from ...models.auth import User from ...schemas.profile import ( ProfilePreferenceResponse, ProfilePreferenceUpdateRequest, SupersetAccountLookupRequest, SupersetAccountLookupResponse, ) from ...services.profile_service import ( EnvironmentNotFoundError, ProfileAuthorizationError, ProfileService, ProfileValidationError, ) # [/SECTION] router = APIRouter(prefix="/api/profile", tags=["profile"]) # [DEF:_get_profile_service:Function] # @PURPOSE: Build profile service for current request scope. # @PRE: db session and config manager are available. # @POST: Returns a ready ProfileService instance. def _get_profile_service(db: Session, config_manager) -> ProfileService: return ProfileService(db=db, config_manager=config_manager) # [/DEF:_get_profile_service:Function] # [DEF:get_preferences:Function] # @PURPOSE: Get authenticated user's dashboard filter preference. # @PRE: Valid JWT and authenticated user context. # @POST: Returns preference payload for current user only. @router.get("/preferences", response_model=ProfilePreferenceResponse) async def get_preferences( current_user: User = Depends(get_current_user), db: Session = Depends(get_db), config_manager=Depends(get_config_manager), ): with belief_scope("profile.get_preferences", f"user_id={current_user.id}"): logger.reason("[REASON] Resolving current user preference") service = _get_profile_service(db, config_manager) return service.get_my_preference(current_user) # [/DEF:get_preferences:Function] # [DEF:update_preferences:Function] # @PURPOSE: Update authenticated user's dashboard filter preference. # @PRE: Valid JWT and valid request payload. # @POST: Persists normalized preference for current user or raises validation/authorization errors. @router.patch("/preferences", response_model=ProfilePreferenceResponse) async def update_preferences( payload: ProfilePreferenceUpdateRequest, current_user: User = Depends(get_current_user), db: Session = Depends(get_db), config_manager=Depends(get_config_manager), ): with belief_scope("profile.update_preferences", f"user_id={current_user.id}"): service = _get_profile_service(db, config_manager) try: logger.reason("[REASON] Attempting preference save") return service.update_my_preference(current_user=current_user, payload=payload) except ProfileValidationError as exc: logger.reflect("[REFLECT] Preference validation failed") raise HTTPException(status_code=422, detail=exc.errors) from exc except ProfileAuthorizationError as exc: logger.explore("[EXPLORE] Cross-user mutation guard blocked request") raise HTTPException(status_code=403, detail=str(exc)) from exc # [/DEF:update_preferences:Function] # [DEF:lookup_superset_accounts:Function] # @PURPOSE: Lookup Superset account candidates in selected environment. # @PRE: Valid JWT, authenticated context, and environment_id query parameter. # @POST: Returns success or degraded lookup payload with stable shape. @router.get("/superset-accounts", response_model=SupersetAccountLookupResponse) async def lookup_superset_accounts( environment_id: str = Query(...), search: Optional[str] = Query(default=None), page_index: int = Query(default=0, ge=0), page_size: int = Query(default=20, ge=1, le=100), sort_column: str = Query(default="username"), sort_order: str = Query(default="desc"), current_user: User = Depends(get_current_user), db: Session = Depends(get_db), config_manager=Depends(get_config_manager), ): with belief_scope( "profile.lookup_superset_accounts", f"user_id={current_user.id}, environment_id={environment_id}", ): service = _get_profile_service(db, config_manager) lookup_request = SupersetAccountLookupRequest( environment_id=environment_id, search=search, page_index=page_index, page_size=page_size, sort_column=sort_column, sort_order=sort_order, ) try: logger.reason("[REASON] Executing Superset account lookup") return service.lookup_superset_accounts( current_user=current_user, request=lookup_request, ) except EnvironmentNotFoundError as exc: logger.explore("[EXPLORE] Lookup request references unknown environment") raise HTTPException(status_code=404, detail=str(exc)) from exc # [/DEF:lookup_superset_accounts:Function] # [/DEF:backend.src.api.routes.profile:Module]