From df7c6e1e00ba5d390424fc9e623353abe4394bf3 Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Mon, 21 Jul 2025 13:09:30 +0200 Subject: [PATCH] Imporve gateway auth for weather.py --- services/data/app/api/weather.py | 229 +++++++++++++++++-------------- 1 file changed, 127 insertions(+), 102 deletions(-) diff --git a/services/data/app/api/weather.py b/services/data/app/api/weather.py index 724f0659..ba2e7992 100644 --- a/services/data/app/api/weather.py +++ b/services/data/app/api/weather.py @@ -1,55 +1,68 @@ -# ================================================================ -# services/data/app/api/weather.py - FIXED VERSION -# ================================================================ -"""Weather data API endpoints with improved error handling""" +# services/data/app/api/weather.py - UPDATED WITH UNIFIED AUTH +"""Weather data API endpoints with unified authentication""" -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.ext.asyncio import AsyncSession -from typing import List, Optional -from datetime import datetime, timedelta +from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks +from typing import List, Optional, Dict, Any +from datetime import datetime, date import structlog -from app.core.database import get_db -from app.core.auth import get_current_user, AuthInfo +from app.schemas.weather import ( + WeatherDataResponse, + WeatherForecastResponse, + WeatherSummaryResponse +) from app.services.weather_service import WeatherService from app.services.messaging import publish_weather_updated -from app.schemas.external import ( - WeatherDataResponse, - WeatherForecastResponse, - LocationRequest, - DateRangeRequest + +# Import unified authentication from shared library +from shared.auth.decorators import ( + get_current_user_dep, + get_current_tenant_id_dep ) -router = APIRouter() -weather_service = WeatherService() +router = APIRouter(prefix="/weather", tags=["weather"]) logger = structlog.get_logger() @router.get("/current", response_model=WeatherDataResponse) async def get_current_weather( latitude: float = Query(..., description="Latitude"), longitude: float = Query(..., description="Longitude"), - current_user: AuthInfo = Depends(get_current_user) + tenant_id: str = Depends(get_current_tenant_id_dep), + current_user: Dict[str, Any] = Depends(get_current_user_dep), ): - """Get current weather for location""" + """Get current weather data for location""" try: - logger.debug("API: Getting current weather", lat=latitude, lon=longitude) + logger.debug("Getting current weather", + lat=latitude, + lon=longitude, + tenant_id=tenant_id, + user_id=current_user["user_id"]) + weather_service = WeatherService() weather = await weather_service.get_current_weather(latitude, longitude) if not weather: - logger.warning("No weather data available", lat=latitude, lon=longitude) raise HTTPException(status_code=404, detail="Weather data not available") - logger.debug("Successfully returning weather data", temp=weather.temperature) + # Publish event + try: + await publish_weather_updated({ + "type": "current_weather_requested", + "tenant_id": tenant_id, + "latitude": latitude, + "longitude": longitude, + "requested_by": current_user["user_id"], + "timestamp": datetime.utcnow().isoformat() + }) + except Exception as e: + logger.warning("Failed to publish weather event", error=str(e)) + return weather except HTTPException: - # Re-raise HTTP exceptions raise except Exception as e: - logger.error("Unexpected error in weather API", error=str(e)) - import traceback - logger.error("Weather API traceback", traceback=traceback.format_exc()) + logger.error("Failed to get current weather", error=str(e)) raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") @router.get("/forecast", response_model=List[WeatherForecastResponse]) @@ -57,127 +70,139 @@ async def get_weather_forecast( latitude: float = Query(..., description="Latitude"), longitude: float = Query(..., description="Longitude"), days: int = Query(7, description="Number of forecast days", ge=1, le=14), - current_user: AuthInfo = Depends(get_current_user) + tenant_id: str = Depends(get_current_tenant_id_dep), + current_user: Dict[str, Any] = Depends(get_current_user_dep), ): """Get weather forecast for location""" try: - logger.debug("API: Getting weather forecast", lat=latitude, lon=longitude, days=days) + logger.debug("Getting weather forecast", + lat=latitude, + lon=longitude, + days=days, + tenant_id=tenant_id) + weather_service = WeatherService() forecast = await weather_service.get_weather_forecast(latitude, longitude, days) if not forecast: - logger.warning("No forecast data available", lat=latitude, lon=longitude) raise HTTPException(status_code=404, detail="Weather forecast not available") - # Publish event (with error handling) + # Publish event try: await publish_weather_updated({ "type": "forecast_requested", + "tenant_id": tenant_id, "latitude": latitude, "longitude": longitude, "days": days, + "requested_by": current_user["user_id"], "timestamp": datetime.utcnow().isoformat() }) - except Exception as pub_error: - logger.warning("Failed to publish weather forecast event", error=str(pub_error)) - # Continue processing - event publishing failure shouldn't break the API + except Exception as e: + logger.warning("Failed to publish forecast event", error=str(e)) - logger.debug("Successfully returning forecast data", count=len(forecast)) return forecast except HTTPException: - # Re-raise HTTP exceptions raise except Exception as e: - logger.error("Unexpected error in weather forecast API", error=str(e)) - import traceback - logger.error("Weather forecast API traceback", traceback=traceback.format_exc()) + logger.error("Failed to get weather forecast", error=str(e)) raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") -@router.get("/historical", response_model=List[WeatherDataResponse]) -async def get_historical_weather( +@router.get("/history", response_model=List[WeatherDataResponse]) +async def get_weather_history( + start_date: date = Query(..., description="Start date"), + end_date: date = Query(..., description="End date"), latitude: float = Query(..., description="Latitude"), longitude: float = Query(..., description="Longitude"), - start_date: datetime = Query(..., description="Start date"), - end_date: datetime = Query(..., description="End date"), - db: AsyncSession = Depends(get_db), - current_user: AuthInfo = Depends(get_current_user) + tenant_id: str = Depends(get_current_tenant_id_dep), + current_user: Dict[str, Any] = Depends(get_current_user_dep), ): """Get historical weather data""" try: - # Validate date range - if end_date <= start_date: - raise HTTPException(status_code=400, detail="End date must be after start date") + logger.debug("Getting weather history", + start_date=start_date, + end_date=end_date, + tenant_id=tenant_id) - if (end_date - start_date).days > 365: - raise HTTPException(status_code=400, detail="Date range cannot exceed 365 days") - - historical_data = await weather_service.get_historical_weather( - latitude, longitude, start_date, end_date, db + weather_service = WeatherService() + history = await weather_service.get_weather_history( + latitude, longitude, start_date, end_date ) - # Publish event (with error handling) - try: - await publish_weather_updated({ - "type": "historical_requested", - "latitude": latitude, - "longitude": longitude, - "start_date": start_date.isoformat(), - "end_date": end_date.isoformat(), - "records_count": len(historical_data), - "timestamp": datetime.utcnow().isoformat() - }) - except Exception as pub_error: - logger.warning("Failed to publish historical weather event", error=str(pub_error)) - # Continue processing + return history - return historical_data - - except HTTPException: - # Re-raise HTTP exceptions - raise except Exception as e: - logger.error("Unexpected error in historical weather API", error=str(e)) + logger.error("Failed to get weather history", error=str(e)) raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") -@router.post("/store") -async def store_weather_data( - latitude: float = Query(..., description="Latitude"), - longitude: float = Query(..., description="Longitude"), - db: AsyncSession = Depends(get_db), - current_user: AuthInfo = Depends(get_current_user) +@router.get("/summary", response_model=WeatherSummaryResponse) +async def get_weather_summary( + location_id: Optional[str] = Query(None, description="Location ID"), + days: int = Query(30, description="Number of days to summarize"), + tenant_id: str = Depends(get_current_tenant_id_dep), + current_user: Dict[str, Any] = Depends(get_current_user_dep), ): - """Store current weather data to database""" + """Get weather summary for tenant's location""" try: - # Get current weather data - weather = await weather_service.get_current_weather(latitude, longitude) + logger.debug("Getting weather summary", + location_id=location_id, + days=days, + tenant_id=tenant_id) - if not weather: - raise HTTPException(status_code=404, detail="No weather data to store") + weather_service = WeatherService() - # Convert to dict for storage - weather_dict = { - "date": weather.date, - "temperature": weather.temperature, - "precipitation": weather.precipitation, - "humidity": weather.humidity, - "wind_speed": weather.wind_speed, - "pressure": weather.pressure, - "description": weather.description, - "source": weather.source - } + # If no location_id provided, use tenant's default location + if not location_id: + # This would typically fetch from tenant service + location_id = tenant_id # Simplified for example - success = await weather_service.store_weather_data( - latitude, longitude, weather_dict, db + summary = await weather_service.get_weather_summary(location_id, days) + + return summary + + except Exception as e: + logger.error("Failed to get weather summary", error=str(e)) + raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") + +@router.post("/sync") +async def sync_weather_data( + background_tasks: BackgroundTasks, + force: bool = Query(False, description="Force sync even if recently synced"), + tenant_id: str = Depends(get_current_tenant_id_dep), + current_user: Dict[str, Any] = Depends(get_current_user_dep), +): + """Manually trigger weather data synchronization""" + try: + logger.info("Weather sync requested", + tenant_id=tenant_id, + user_id=current_user["user_id"], + force=force) + + weather_service = WeatherService() + + # Check if user has permission to sync (could be admin only) + if current_user.get("role") not in ["admin", "manager"]: + raise HTTPException( + status_code=403, + detail="Insufficient permissions to sync weather data" + ) + + # Schedule background sync + background_tasks.add_task( + weather_service.sync_weather_data, + tenant_id=tenant_id, + force=force ) - if success: - return {"status": "success", "message": "Weather data stored successfully"} - else: - raise HTTPException(status_code=500, detail="Failed to store weather data") - + return { + "message": "Weather sync initiated", + "status": "processing", + "initiated_by": current_user["user_id"] + } + except HTTPException: raise except Exception as e: - logger.error("Error storing weather data", error=str(e)) + logger.error("Failed to initiate weather sync", error=str(e)) raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") \ No newline at end of file