Files
bakery-ia/services/auth/app/api/auth.py
2025-07-26 18:46:52 +02:00

340 lines
12 KiB
Python

# services/auth/app/api/auth.py - UPDATED TO RETURN TOKENS FROM REGISTRATION
"""
Authentication API routes - Updated to return tokens directly from registration
Following industry best practices with unified token response format
"""
from fastapi import APIRouter, Depends, HTTPException, status, Request
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Optional
import structlog
from app.core.database import get_db
from app.schemas.auth import (
UserRegistration, UserLogin, TokenResponse,
RefreshTokenRequest, UserResponse, PasswordChange,
PasswordReset, TokenVerification
)
from app.services.auth_service import AuthService
from app.core.security import SecurityManager
from shared.monitoring.decorators import track_execution_time
logger = structlog.get_logger()
router = APIRouter(tags=["authentication"])
security = HTTPBearer(auto_error=False)
def get_metrics_collector(request: Request):
"""Get metrics collector from app state"""
return getattr(request.app.state, 'metrics_collector', None)
# ================================================================
# AUTHENTICATION ENDPOINTS
# ================================================================
@router.post("/register", response_model=TokenResponse)
@track_execution_time("registration_duration_seconds", "auth-service")
async def register(
user_data: UserRegistration,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""Register a new user and return tokens directly"""
metrics = get_metrics_collector(request)
try:
# Validate password strength
if not SecurityManager.validate_password(user_data.password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Password does not meet security requirements"
)
# Create user and generate tokens in one operation
result = await AuthService.register_user_with_tokens(
email=user_data.email,
password=user_data.password,
full_name=user_data.full_name,
db=db
)
# Record successful registration
if metrics:
metrics.increment_counter("registration_total", labels={"status": "success"})
logger.info(f"User registration with tokens successful: {user_data.email}")
return TokenResponse(**result)
except HTTPException as e:
if metrics:
metrics.increment_counter("registration_total", labels={"status": "failed"})
logger.warning(f"Registration failed for {user_data.email}: {e.detail}")
raise
except Exception as e:
if metrics:
metrics.increment_counter("registration_total", labels={"status": "error"})
logger.error(f"Registration error for {user_data.email}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Registration failed"
)
@router.post("/login", response_model=TokenResponse)
@track_execution_time("login_duration_seconds", "auth-service")
async def login(
login_data: UserLogin,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""Login user and return tokens"""
metrics = get_metrics_collector(request)
try:
# Check login attempts (rate limiting)
#attempts = await SecurityManager.get_login_attempts(login_data.email)
#if attempts >= 5:
# raise HTTPException(
# status_code=status.HTTP_429_TOO_MANY_REQUESTS,
# detail="Too many login attempts. Please try again later."
# )
# Attempt login
result = await AuthService.login(login_data.email, login_data.password, db)
# Clear login attempts on success
await SecurityManager.clear_login_attempts(login_data.email)
# Record successful login
if metrics:
metrics.increment_counter("login_success_total")
logger.info(f"Login successful for {login_data.email}")
return TokenResponse(**result)
except HTTPException as e:
# Increment login attempts on failure
await SecurityManager.increment_login_attempts(login_data.email)
# Record failed login
if metrics:
metrics.increment_counter("login_failure_total", labels={"reason": "auth_failed"})
logger.warning(f"Login failed for {login_data.email}: {e.detail}")
raise
except Exception as e:
# Record login error
if metrics:
metrics.increment_counter("login_failure_total", labels={"reason": "error"})
logger.error(f"Login error for {login_data.email}: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Login failed"
)
@router.post("/refresh", response_model=TokenResponse)
@track_execution_time("token_refresh_duration_seconds", "auth-service")
async def refresh_token(
refresh_data: RefreshTokenRequest,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""Refresh access token"""
metrics = get_metrics_collector(request)
try:
result = await AuthService.refresh_access_token(refresh_data.refresh_token, db)
# Record successful refresh
if metrics:
metrics.increment_counter("token_refresh_success_total")
logger.info("Token refresh successful")
return TokenResponse(**result)
except HTTPException as e:
if metrics:
metrics.increment_counter("token_refresh_failure_total")
logger.warning(f"Token refresh failed: {e.detail}")
raise
except Exception as e:
if metrics:
metrics.increment_counter("token_refresh_failure_total")
logger.error(f"Token refresh error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Token refresh failed"
)
@router.post("/verify", response_model=TokenVerification)
@track_execution_time("token_verification_duration_seconds", "auth-service")
async def verify_token(
credentials: HTTPAuthorizationCredentials = Depends(security),
request: Request = None
):
"""Verify access token"""
metrics = get_metrics_collector(request) if request else None
if not credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required"
)
try:
result = await AuthService.verify_user_token(credentials.credentials)
if metrics:
metrics.increment_counter("token_verify_success_total")
return TokenVerification(
valid=True,
user_id=result.get("user_id"),
email=result.get("email"),
exp=result.get("exp")
)
except HTTPException as e:
if metrics:
metrics.increment_counter("token_verify_failure_total")
raise
except Exception as e:
if metrics:
metrics.increment_counter("token_verify_failure_total")
logger.error(f"Token verification error: {e}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
@router.post("/logout")
@track_execution_time("logout_duration_seconds", "auth-service")
async def logout(
refresh_data: RefreshTokenRequest,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""Logout user by revoking refresh token"""
metrics = get_metrics_collector(request)
try:
success = await AuthService.logout(refresh_data.refresh_token, db)
if metrics:
status_label = "success" if success else "failed"
metrics.increment_counter("logout_total", labels={"status": status_label})
return {"message": "Logout successful" if success else "Logout failed"}
except Exception as e:
if metrics:
metrics.increment_counter("logout_total", labels={"status": "error"})
logger.error(f"Logout error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Logout failed"
)
# ================================================================
# PASSWORD MANAGEMENT ENDPOINTS
# ================================================================
@router.post("/change-password")
async def change_password(
password_data: PasswordChange,
credentials: HTTPAuthorizationCredentials = Depends(security),
request: Request = None,
db: AsyncSession = Depends(get_db)
):
"""Change user password"""
metrics = get_metrics_collector(request) if request else None
try:
if not credentials:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required"
)
# Verify current token
payload = await AuthService.verify_user_token(credentials.credentials)
user_id = payload.get("user_id")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token"
)
# Validate new password
if not SecurityManager.validate_password(password_data.new_password):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="New password does not meet security requirements"
)
# Change password logic would go here
# This is a simplified version - you'd need to implement the actual password change in AuthService
# Record password change
if metrics:
metrics.increment_counter("password_change_total", labels={"status": "success"})
logger.info(f"Password changed for user: {user_id}")
return {"message": "Password changed successfully"}
except HTTPException:
raise
except Exception as e:
# Record password change error
if metrics:
metrics.increment_counter("password_change_total", labels={"status": "error"})
logger.error(f"Password change error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Password change failed"
)
@router.post("/reset-password")
async def reset_password(
reset_data: PasswordReset,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""Request password reset"""
metrics = get_metrics_collector(request)
try:
# Password reset logic would go here
# This is a simplified version - you'd need to implement email sending, etc.
# Record password reset request
if metrics:
metrics.increment_counter("password_reset_total", labels={"status": "requested"})
logger.info(f"Password reset requested for: {reset_data.email}")
return {"message": "Password reset email sent if account exists"}
except Exception as e:
# Record password reset error
if metrics:
metrics.increment_counter("password_reset_total", labels={"status": "error"})
logger.error(f"Password reset error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Password reset failed"
)
# ================================================================
# HEALTH AND STATUS ENDPOINTS
# ================================================================
@router.get("/health")
async def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"service": "auth-service",
"version": "1.0.0"
}