From 2d85dd3e9e83e1ca4e9726b58b96af7a49138abc Mon Sep 17 00:00:00 2001 From: Urtzi Alfaro Date: Mon, 21 Jul 2025 14:41:33 +0200 Subject: [PATCH] Imporve gateway auth for all services --- services/auth/app/api/users.py | 11 ++- services/data/app/api/traffic.py | 13 +++- services/data/app/api/weather.py | 34 +------- services/forecasting/app/api/forecast.py | 72 +++++++++++++++++ .../notification/app/api/notifications.py | 77 +++++++++++++++++++ services/tenant/app/api/tenants.py | 44 +++++------ 6 files changed, 188 insertions(+), 63 deletions(-) create mode 100644 services/forecasting/app/api/forecast.py create mode 100644 services/notification/app/api/notifications.py diff --git a/services/auth/app/api/users.py b/services/auth/app/api/users.py index 8521bea0..8e25eb5e 100644 --- a/services/auth/app/api/users.py +++ b/services/auth/app/api/users.py @@ -4,7 +4,7 @@ User management API routes from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.ext.asyncio import AsyncSession -from typing import List +from typing import Dict, Any import structlog from app.core.database import get_db @@ -14,12 +14,19 @@ from app.services.user_service import UserService from app.core.auth import get_current_user from app.models.users import User +# Import unified authentication from shared library +from shared.auth.decorators import ( + get_current_user_dep, + get_current_tenant_id_dep, + require_role # For admin-only endpoints +) + logger = structlog.get_logger() router = APIRouter() @router.get("/me", response_model=UserResponse) async def get_current_user_info( - current_user: User = Depends(get_current_user), + current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): """Get current user information""" diff --git a/services/data/app/api/traffic.py b/services/data/app/api/traffic.py index 3953ea1d..e031a2e7 100644 --- a/services/data/app/api/traffic.py +++ b/services/data/app/api/traffic.py @@ -5,7 +5,7 @@ from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession -from typing import List, Optional +from typing import List, Dict, Any from datetime import datetime, timedelta import structlog @@ -19,6 +19,11 @@ from app.schemas.external import ( DateRangeRequest ) +from shared.auth.decorators import ( + get_current_user_dep, + get_current_tenant_id_dep +) + router = APIRouter() traffic_service = TrafficService() logger = structlog.get_logger() @@ -27,7 +32,7 @@ logger = structlog.get_logger() async def get_current_traffic( latitude: float = Query(..., description="Latitude"), longitude: float = Query(..., description="Longitude"), - current_user: AuthInfo = Depends(get_current_user) + current_user: Dict[str, Any] = Depends(get_current_user_dep), ): """Get current traffic data for location""" try: @@ -72,7 +77,7 @@ async def get_historical_traffic( 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) + current_user: Dict[str, Any] = Depends(get_current_user_dep), ): """Get historical traffic data""" try: @@ -116,7 +121,7 @@ async def store_traffic_data( latitude: float = Query(..., description="Latitude"), longitude: float = Query(..., description="Longitude"), db: AsyncSession = Depends(get_db), - current_user: AuthInfo = Depends(get_current_user) + current_user: Dict[str, Any] = Depends(get_current_user_dep) ): """Store current traffic data to database""" try: diff --git a/services/data/app/api/weather.py b/services/data/app/api/weather.py index ba2e7992..aa5621b8 100644 --- a/services/data/app/api/weather.py +++ b/services/data/app/api/weather.py @@ -6,10 +6,9 @@ from typing import List, Optional, Dict, Any from datetime import datetime, date import structlog -from app.schemas.weather import ( +from app.schemas.external import ( WeatherDataResponse, - WeatherForecastResponse, - WeatherSummaryResponse + WeatherForecastResponse ) from app.services.weather_service import WeatherService from app.services.messaging import publish_weather_updated @@ -136,35 +135,6 @@ async def get_weather_history( logger.error("Failed to get weather history", error=str(e)) raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}") -@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), -): - """Get weather summary for tenant's location""" - try: - logger.debug("Getting weather summary", - location_id=location_id, - days=days, - tenant_id=tenant_id) - - weather_service = WeatherService() - - # 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 - - 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, diff --git a/services/forecasting/app/api/forecast.py b/services/forecasting/app/api/forecast.py new file mode 100644 index 00000000..e66ccd09 --- /dev/null +++ b/services/forecasting/app/api/forecast.py @@ -0,0 +1,72 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, BackgroundTasks +from typing import List, Optional, Dict, Any +from datetime import datetime, date +import structlog + +from app.schemas.forecast import ( + ForecastRequest, + ForecastResponse, + BatchForecastRequest, + ForecastPerformanceResponse +) +from app.services.forecast_service import ForecastService +from app.services.messaging import publish_forecast_generated + +# Import unified authentication +from shared.auth.decorators import ( + get_current_user_dep, + get_current_tenant_id_dep +) + +router = APIRouter(prefix="/forecasts", tags=["forecasting"]) +logger = structlog.get_logger() + +@router.post("/generate", response_model=ForecastResponse) +async def generate_forecast( + request: ForecastRequest, + background_tasks: BackgroundTasks, + tenant_id: str = Depends(get_current_tenant_id_dep), + current_user: Dict[str, Any] = Depends(get_current_user_dep), +): + """Generate forecast for products""" + try: + logger.info("Generating forecast", + tenant_id=tenant_id, + user_id=current_user["user_id"], + products=len(request.products) if request.products else "all") + + forecast_service = ForecastService() + + # Ensure products belong to tenant + if request.products: + valid_products = await forecast_service.validate_products( + tenant_id, request.products + ) + if len(valid_products) != len(request.products): + raise HTTPException( + status_code=400, + detail="Some products not found or not accessible" + ) + + # Generate forecast + forecast = await forecast_service.generate_forecast( + tenant_id=tenant_id, + request=request, + user_id=current_user["user_id"] + ) + + # Publish event + background_tasks.add_task( + publish_forecast_generated, + forecast_id=forecast.id, + tenant_id=tenant_id, + user_id=current_user["user_id"] + ) + + return forecast + + except HTTPException: + raise + except Exception as e: + logger.error("Failed to generate forecast", error=str(e)) + raise HTTPException(status_code=500, detail=str(e)) diff --git a/services/notification/app/api/notifications.py b/services/notification/app/api/notifications.py new file mode 100644 index 00000000..b4534f72 --- /dev/null +++ b/services/notification/app/api/notifications.py @@ -0,0 +1,77 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from typing import List, Optional, Dict, Any +import structlog + +from app.schemas.notification import ( + NotificationCreate, + NotificationResponse, + NotificationPreferences, + NotificationHistory +) +from app.services.notification_service import NotificationService + +# Import unified authentication +from shared.auth.decorators import ( + get_current_user_dep, + get_current_tenant_id_dep, + require_role +) + +router = APIRouter(prefix="/notifications", tags=["notifications"]) +logger = structlog.get_logger() + +@router.post("/send", response_model=NotificationResponse) +async def send_notification( + notification: NotificationCreate, + tenant_id: str = Depends(get_current_tenant_id_dep), + current_user: Dict[str, Any] = Depends(get_current_user_dep), +): + """Send notification to users""" + try: + logger.info("Sending notification", + tenant_id=tenant_id, + sender_id=current_user["user_id"], + type=notification.type) + + notification_service = NotificationService() + + # Ensure notification is scoped to tenant + notification.tenant_id = tenant_id + notification.sender_id = current_user["user_id"] + + # Check permissions + if notification.broadcast and current_user.get("role") not in ["admin", "manager"]: + raise HTTPException( + status_code=403, + detail="Only admins and managers can send broadcast notifications" + ) + + result = await notification_service.send_notification(notification) + + return result + + except HTTPException: + raise + except Exception as e: + logger.error("Failed to send notification", error=str(e)) + raise HTTPException(status_code=500, detail=str(e)) + +@router.get("/preferences", response_model=NotificationPreferences) +async def get_notification_preferences( + tenant_id: str = Depends(get_current_tenant_id_dep), + current_user: Dict[str, Any] = Depends(get_current_user_dep), +): + """Get user's notification preferences""" + try: + notification_service = NotificationService() + + preferences = await notification_service.get_user_preferences( + user_id=current_user["user_id"], + tenant_id=tenant_id + ) + + return preferences + + except Exception as e: + logger.error("Failed to get preferences", error=str(e)) + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/services/tenant/app/api/tenants.py b/services/tenant/app/api/tenants.py index 3c91fc72..3ebea92c 100644 --- a/services/tenant/app/api/tenants.py +++ b/services/tenant/app/api/tenants.py @@ -14,24 +14,26 @@ from app.schemas.tenants import ( TenantUpdate, TenantMemberResponse ) from app.services.tenant_service import TenantService -from shared.auth.decorators import require_authentication, get_current_user, get_current_tenant_id +# Import unified authentication +from shared.auth.decorators import ( + get_current_user_dep, + get_current_tenant_id_dep, + require_role +) logger = structlog.get_logger() router = APIRouter() @router.post("/tenants/register", response_model=TenantResponse) -@require_authentication async def register_bakery( bakery_data: BakeryRegistration, - request: Request, + current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): - """Register a new bakery/tenant""" - user = get_current_user(request) try: - result = await TenantService.create_bakery(bakery_data, user["user_id"], db) - logger.info(f"Bakery registered: {bakery_data.name} by {user['email']}") + result = await TenantService.create_bakery(bakery_data, current_user["user_id"], db) + logger.info(f"Bakery registered: {bakery_data.name} by {current_user['email']}") return result except Exception as e: @@ -64,12 +66,10 @@ async def verify_tenant_access( @require_authentication async def get_user_tenants( user_id: str, - request: Request, + current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): - """Get all tenants accessible by user""" - current_user = get_current_user(request) - + # Users can only see their own tenants if current_user["user_id"] != user_id: raise HTTPException( @@ -92,14 +92,12 @@ async def get_user_tenants( @require_authentication async def get_tenant( tenant_id: str, - request: Request, + current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): - """Get tenant details""" - user = get_current_user(request) - + # Verify user has access to tenant - access = await TenantService.verify_user_access(user["user_id"], tenant_id, db) + access = await TenantService.verify_user_access(current_user["user_id"], tenant_id, db) if not access.has_access: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, @@ -120,14 +118,12 @@ async def get_tenant( async def update_tenant( tenant_id: str, update_data: TenantUpdate, - request: Request, + current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): - """Update tenant information""" - user = get_current_user(request) - + try: - result = await TenantService.update_tenant(tenant_id, update_data, user["user_id"], db) + result = await TenantService.update_tenant(tenant_id, update_data, current_user["user_id"], db) return result except HTTPException: @@ -145,12 +141,10 @@ async def add_team_member( tenant_id: str, user_id: str, role: str, - request: Request, + current_user: Dict[str, Any] = Depends(get_current_user_dep), db: AsyncSession = Depends(get_db) ): - """Add a team member to tenant""" - current_user = get_current_user(request) - + try: result = await TenantService.add_team_member( tenant_id, user_id, role, current_user["user_id"], db