Adds auth module

This commit is contained in:
Urtzi Alfaro
2025-07-17 21:25:27 +02:00
parent 654d1c2fe8
commit efca9a125a
19 changed files with 1169 additions and 406 deletions

View File

@@ -15,9 +15,6 @@ class Settings(BaseSettings):
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
# CORS settings - FIXED: Remove List[str] type and parse manually
CORS_ORIGINS: str = "http://localhost:3000,http://localhost:3001"
# Service URLs
AUTH_SERVICE_URL: str = "http://auth-service:8000"
TRAINING_SERVICE_URL: str = "http://training-service:8000"
@@ -26,6 +23,19 @@ class Settings(BaseSettings):
TENANT_SERVICE_URL: str = "http://tenant-service:8000"
NOTIFICATION_SERVICE_URL: str = "http://notification-service:8000"
# Service Discovery
CONSUL_URL: str = os.getenv("CONSUL_URL", "http://consul:8500")
ENABLE_SERVICE_DISCOVERY: bool = os.getenv("ENABLE_SERVICE_DISCOVERY", "false").lower() == "true"
# CORS
CORS_ORIGINS: str = os.getenv("CORS_ORIGINS", "http://localhost:3000,http://localhost:3001")
@property
def CORS_ORIGINS_LIST(self) -> List[str]:
"""Get CORS origins as list"""
return [origin.strip() for origin in self.CORS_ORIGINS.split(",")]
# Redis settings
REDIS_URL: str = "redis://redis:6379/6"
@@ -37,11 +47,6 @@ class Settings(BaseSettings):
JWT_SECRET_KEY: str = "your-secret-key-change-in-production"
JWT_ALGORITHM: str = "HS256"
@property
def CORS_ORIGINS_LIST(self) -> List[str]:
"""Parse CORS origins from string to list"""
return [origin.strip() for origin in self.CORS_ORIGINS.split(",") if origin.strip()]
@property
def SERVICES(self) -> Dict[str, str]:
"""Service registry"""

View File

@@ -1,122 +1,65 @@
"""
Service discovery for microservices
Service discovery for API Gateway
"""
import asyncio
import logging
from typing import Dict, List, Optional
import httpx
import redis.asyncio as redis
from datetime import datetime, timedelta
from typing import Optional, Dict
import json
from app.core.config import settings
logger = logging.getLogger(__name__)
class ServiceDiscovery:
"""Service discovery and health checking"""
"""Service discovery client"""
def __init__(self):
self.redis_client = redis.from_url(settings.REDIS_URL)
self.services = settings.SERVICES
self.health_check_interval = 30 # seconds
self.health_check_task = None
async def initialize(self):
"""Initialize service discovery"""
logger.info("Initializing service discovery")
self.consul_url = settings.CONSUL_URL if hasattr(settings, 'CONSUL_URL') else None
self.service_cache: Dict[str, str] = {}
# Start health check task
self.health_check_task = asyncio.create_task(self._health_check_loop())
# Initial health check
await self._check_all_services()
async def cleanup(self):
"""Cleanup service discovery"""
if self.health_check_task:
self.health_check_task.cancel()
try:
await self.health_check_task
except asyncio.CancelledError:
pass
await self.redis_client.close()
async def get_service_url(self, service_name: str) -> Optional[str]:
"""Get service URL"""
return self.services.get(service_name)
async def get_healthy_services(self) -> List[str]:
"""Get list of healthy services"""
healthy_services = []
"""Get service URL from service discovery"""
for service_name in self.services:
is_healthy = await self._is_service_healthy(service_name)
if is_healthy:
healthy_services.append(service_name)
# Return cached URL if available
if service_name in self.service_cache:
return self.service_cache[service_name]
return healthy_services
async def _health_check_loop(self):
"""Continuous health check loop"""
while True:
# Try Consul if enabled
if self.consul_url and getattr(settings, 'ENABLE_SERVICE_DISCOVERY', False):
try:
await self._check_all_services()
await asyncio.sleep(self.health_check_interval)
except asyncio.CancelledError:
break
url = await self._get_from_consul(service_name)
if url:
self.service_cache[service_name] = url
return url
except Exception as e:
logger.error(f"Health check error: {e}")
await asyncio.sleep(self.health_check_interval)
logger.warning(f"Failed to get {service_name} from Consul: {e}")
# Fall back to environment variables
return self._get_from_env(service_name)
async def _check_all_services(self):
"""Check health of all services"""
for service_name, service_url in self.services.items():
try:
is_healthy = await self._check_service_health(service_url)
await self._update_service_health(service_name, is_healthy)
except Exception as e:
logger.error(f"Health check failed for {service_name}: {e}")
await self._update_service_health(service_name, False)
async def _check_service_health(self, service_url: str) -> bool:
"""Check individual service health"""
async def _get_from_consul(self, service_name: str) -> Optional[str]:
"""Get service URL from Consul"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(f"{service_url}/health")
return response.status_code == 200
response = await client.get(
f"{self.consul_url}/v1/health/service/{service_name}?passing=true"
)
if response.status_code == 200:
services = response.json()
if services:
service = services[0]
address = service['Service']['Address']
port = service['Service']['Port']
return f"http://{address}:{port}"
except Exception as e:
logger.warning(f"Service health check failed: {e}")
return False
logger.error(f"Consul query failed: {e}")
return None
async def _update_service_health(self, service_name: str, is_healthy: bool):
"""Update service health status in Redis"""
try:
key = f"service_health:{service_name}"
value = {
"healthy": is_healthy,
"last_check": datetime.now(datetime.timezone.utc).isoformat(),
"url": self.services[service_name]
}
await self.redis_client.hset(key, mapping=value)
await self.redis_client.expire(key, 300) # 5 minutes TTL
except Exception as e:
logger.error(f"Failed to update service health for {service_name}: {e}")
async def _is_service_healthy(self, service_name: str) -> bool:
"""Check if service is healthy from Redis cache"""
try:
key = f"service_health:{service_name}"
health_data = await self.redis_client.hgetall(key)
if not health_data:
return False
return health_data.get(b'healthy', b'false').decode() == 'True'
except Exception as e:
logger.error(f"Failed to check service health for {service_name}: {e}")
return False
def _get_from_env(self, service_name: str) -> Optional[str]:
"""Get service URL from environment variables"""
env_var = f"{service_name.upper().replace('-', '_')}_SERVICE_URL"
return getattr(settings, env_var, None)