Initial microservices setup from artifacts

This commit is contained in:
Urtzi Alfaro
2025-07-17 13:09:24 +02:00
commit 347ff51bd7
200 changed files with 9559 additions and 0 deletions

View File

View File

View File

@@ -0,0 +1,124 @@
"""
Authentication API routes
"""
from fastapi import APIRouter, Depends, HTTPException, status, Request
from sqlalchemy.ext.asyncio import AsyncSession
import logging
from app.core.database import get_db
from app.schemas.auth import UserRegistration, UserLogin, TokenResponse, RefreshTokenRequest, UserResponse
from app.services.auth_service import AuthService
from app.core.security import security_manager
logger = logging.getLogger(__name__)
router = APIRouter()
@router.post("/register", response_model=UserResponse)
async def register(
user_data: UserRegistration,
db: AsyncSession = Depends(get_db)
):
"""Register a new user"""
try:
return await AuthService.register_user(user_data, db)
except HTTPException:
raise
except Exception as e:
logger.error(f"Registration error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Registration failed"
)
@router.post("/login", response_model=TokenResponse)
async def login(
login_data: UserLogin,
request: Request,
db: AsyncSession = Depends(get_db)
):
"""User login"""
try:
ip_address = request.client.host
user_agent = request.headers.get("user-agent", "")
return await AuthService.login_user(login_data, db, ip_address, user_agent)
except HTTPException:
raise
except Exception as e:
logger.error(f"Login error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Login failed"
)
@router.post("/refresh", response_model=TokenResponse)
async def refresh_token(
refresh_data: RefreshTokenRequest,
db: AsyncSession = Depends(get_db)
):
"""Refresh access token"""
try:
return await AuthService.refresh_token(refresh_data.refresh_token, db)
except HTTPException:
raise
except Exception as e:
logger.error(f"Token refresh error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Token refresh failed"
)
@router.post("/verify")
async def verify_token(
request: Request,
db: AsyncSession = Depends(get_db)
):
"""Verify access token"""
try:
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required"
)
token = auth_header.split(" ")[1]
return await AuthService.verify_token(token, db)
except HTTPException:
raise
except Exception as e:
logger.error(f"Token verification error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Token verification failed"
)
@router.post("/logout")
async def logout(
request: Request,
db: AsyncSession = Depends(get_db)
):
"""User logout"""
try:
auth_header = request.headers.get("Authorization")
if not auth_header or not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header required"
)
token = auth_header.split(" ")[1]
user_data = await AuthService.verify_token(token, db)
await AuthService.logout_user(user_data["user_id"], db)
return {"message": "Logged out successfully"}
except HTTPException:
raise
except Exception as e:
logger.error(f"Logout error: {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Logout failed"
)

View File

View File

@@ -0,0 +1,47 @@
"""
Authentication service configuration
"""
import os
from pydantic import BaseSettings
class Settings(BaseSettings):
"""Application settings"""
# Basic settings
APP_NAME: str = "Authentication Service"
VERSION: str = "1.0.0"
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
# Database settings
DATABASE_URL: str = os.getenv("DATABASE_URL", "postgresql+asyncpg://auth_user:auth_pass123@auth-db:5432/auth_db")
# Redis settings
REDIS_URL: str = os.getenv("REDIS_URL", "redis://redis:6379/0")
# JWT settings
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "your-super-secret-jwt-key")
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
JWT_ACCESS_TOKEN_EXPIRE_MINUTES: int = int(os.getenv("JWT_ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
JWT_REFRESH_TOKEN_EXPIRE_DAYS: int = int(os.getenv("JWT_REFRESH_TOKEN_EXPIRE_DAYS", "7"))
# Password settings
PASSWORD_MIN_LENGTH: int = 8
PASSWORD_REQUIRE_UPPERCASE: bool = True
PASSWORD_REQUIRE_LOWERCASE: bool = True
PASSWORD_REQUIRE_NUMBERS: bool = True
PASSWORD_REQUIRE_SYMBOLS: bool = False
# Security settings
BCRYPT_ROUNDS: int = 12
MAX_LOGIN_ATTEMPTS: int = 5
LOCKOUT_DURATION_MINUTES: int = 30
# RabbitMQ settings
RABBITMQ_URL: str = os.getenv("RABBITMQ_URL", "amqp://bakery:forecast123@rabbitmq:5672/")
class Config:
env_file = ".env"
settings = Settings()

View File

@@ -0,0 +1,12 @@
"""
Database configuration for auth service
"""
from shared.database.base import DatabaseManager
from app.core.config import settings
# Initialize database manager
database_manager = DatabaseManager(settings.DATABASE_URL)
# Alias for convenience
get_db = database_manager.get_db

View File

@@ -0,0 +1,153 @@
"""
Security utilities for authentication service
"""
import bcrypt
import re
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
import redis.asyncio as redis
from fastapi import HTTPException, status
import logging
from app.core.config import settings
from shared.auth.jwt_handler import JWTHandler
logger = logging.getLogger(__name__)
# Initialize JWT handler
jwt_handler = JWTHandler(settings.JWT_SECRET_KEY, settings.JWT_ALGORITHM)
# Redis client for session management
redis_client = redis.from_url(settings.REDIS_URL)
class SecurityManager:
"""Security utilities for authentication"""
@staticmethod
def hash_password(password: str) -> str:
"""Hash password using bcrypt"""
salt = bcrypt.gensalt(rounds=settings.BCRYPT_ROUNDS)
return bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
@staticmethod
def verify_password(password: str, hashed_password: str) -> bool:
"""Verify password against hash"""
return bcrypt.checkpw(password.encode('utf-8'), hashed_password.encode('utf-8'))
@staticmethod
def validate_password(password: str) -> bool:
"""Validate password strength"""
if len(password) < settings.PASSWORD_MIN_LENGTH:
return False
if settings.PASSWORD_REQUIRE_UPPERCASE and not re.search(r'[A-Z]', password):
return False
if settings.PASSWORD_REQUIRE_LOWERCASE and not re.search(r'[a-z]', password):
return False
if settings.PASSWORD_REQUIRE_NUMBERS and not re.search(r'\d', password):
return False
if settings.PASSWORD_REQUIRE_SYMBOLS and not re.search(r'[!@#$%^&*(),.?":{}|<>]', password):
return False
return True
@staticmethod
def create_access_token(user_data: Dict[str, Any]) -> str:
"""Create JWT access token"""
expires_delta = timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
return jwt_handler.create_access_token(user_data, expires_delta)
@staticmethod
def create_refresh_token(user_data: Dict[str, Any]) -> str:
"""Create JWT refresh token"""
expires_delta = timedelta(days=settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS)
return jwt_handler.create_refresh_token(user_data, expires_delta)
@staticmethod
def verify_token(token: str) -> Optional[Dict[str, Any]]:
"""Verify JWT token"""
return jwt_handler.verify_token(token)
@staticmethod
async def check_login_attempts(email: str) -> bool:
"""Check if user has exceeded login attempts"""
try:
key = f"login_attempts:{email}"
attempts = await redis_client.get(key)
if attempts is None:
return True
return int(attempts) < settings.MAX_LOGIN_ATTEMPTS
except Exception as e:
logger.error(f"Error checking login attempts: {e}")
return True
@staticmethod
async def increment_login_attempts(email: str):
"""Increment login attempts counter"""
try:
key = f"login_attempts:{email}"
current_attempts = await redis_client.incr(key)
# Set TTL on first attempt
if current_attempts == 1:
await redis_client.expire(key, settings.LOCKOUT_DURATION_MINUTES * 60)
except Exception as e:
logger.error(f"Error incrementing login attempts: {e}")
@staticmethod
async def clear_login_attempts(email: str):
"""Clear login attempts counter"""
try:
key = f"login_attempts:{email}"
await redis_client.delete(key)
except Exception as e:
logger.error(f"Error clearing login attempts: {e}")
@staticmethod
async def store_refresh_token(user_id: str, refresh_token: str):
"""Store refresh token in Redis"""
try:
key = f"refresh_token:{user_id}"
expires_seconds = settings.JWT_REFRESH_TOKEN_EXPIRE_DAYS * 24 * 3600
await redis_client.setex(key, expires_seconds, refresh_token)
except Exception as e:
logger.error(f"Error storing refresh token: {e}")
@staticmethod
async def verify_refresh_token(user_id: str, refresh_token: str) -> bool:
"""Verify refresh token"""
try:
key = f"refresh_token:{user_id}"
stored_token = await redis_client.get(key)
if stored_token is None:
return False
return stored_token.decode() == refresh_token
except Exception as e:
logger.error(f"Error verifying refresh token: {e}")
return False
@staticmethod
async def revoke_refresh_token(user_id: str):
"""Revoke refresh token"""
try:
key = f"refresh_token:{user_id}"
await redis_client.delete(key)
except Exception as e:
logger.error(f"Error revoking refresh token: {e}")
# Global security manager instance
security_manager = SecurityManager()

83
services/auth/app/main.py Normal file
View File

@@ -0,0 +1,83 @@
"""
Authentication Service
Handles user authentication, registration, and token management
"""
import logging
from datetime import timedelta
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer
from app.core.config import settings
from app.core.database import database_manager
from app.api import auth, users
from app.services.messaging import message_publisher
from shared.monitoring.logging import setup_logging
from shared.monitoring.metrics import MetricsCollector
# Setup logging
setup_logging("auth-service", settings.LOG_LEVEL)
logger = logging.getLogger(__name__)
# Create FastAPI app
app = FastAPI(
title="Authentication Service",
description="User authentication and authorization service",
version="1.0.0"
)
# Initialize metrics collector
metrics_collector = MetricsCollector("auth-service")
# CORS middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# Include routers
app.include_router(auth.router, prefix="/auth", tags=["authentication"])
app.include_router(users.router, prefix="/users", tags=["users"])
@app.on_event("startup")
async def startup_event():
"""Application startup"""
logger.info("Starting Authentication Service")
# Create database tables
await database_manager.create_tables()
# Initialize message publisher
await message_publisher.connect()
# Start metrics server
metrics_collector.start_metrics_server(8080)
logger.info("Authentication Service started successfully")
@app.on_event("shutdown")
async def shutdown_event():
"""Application shutdown"""
logger.info("Shutting down Authentication Service")
# Cleanup message publisher
await message_publisher.disconnect()
logger.info("Authentication Service shutdown complete")
@app.get("/health")
async def health_check():
"""Health check endpoint"""
return {
"status": "healthy",
"service": "auth-service",
"version": "1.0.0"
}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)

View File

View File

@@ -0,0 +1,108 @@
"""
Authentication schemas
"""
from pydantic import BaseModel, EmailStr, Field, validator
from typing import Optional
from datetime import datetime
from app.core.config import settings
from shared.utils.validation import validate_spanish_phone
class UserRegistration(BaseModel):
"""User registration schema"""
email: EmailStr
password: str = Field(..., min_length=settings.PASSWORD_MIN_LENGTH)
full_name: str = Field(..., min_length=2, max_length=100)
phone: Optional[str] = None
language: str = Field(default="es", regex="^(es|en)$")
@validator('password')
def validate_password(cls, v):
"""Validate password strength"""
from app.core.security import security_manager
if not security_manager.validate_password(v):
raise ValueError('Password does not meet security requirements')
return v
@validator('phone')
def validate_phone(cls, v):
"""Validate phone number"""
if v and not validate_spanish_phone(v):
raise ValueError('Invalid Spanish phone number')
return v
class UserLogin(BaseModel):
"""User login schema"""
email: EmailStr
password: str
class TokenResponse(BaseModel):
"""Token response schema"""
access_token: str
refresh_token: str
token_type: str = "bearer"
expires_in: int
class RefreshTokenRequest(BaseModel):
"""Refresh token request schema"""
refresh_token: str
class UserResponse(BaseModel):
"""User response schema"""
id: str
email: str
full_name: str
is_active: bool
is_verified: bool
tenant_id: Optional[str]
role: str
phone: Optional[str]
language: str
timezone: str
created_at: Optional[datetime]
last_login: Optional[datetime]
class PasswordChangeRequest(BaseModel):
"""Password change request schema"""
current_password: str
new_password: str = Field(..., min_length=settings.PASSWORD_MIN_LENGTH)
@validator('new_password')
def validate_new_password(cls, v):
"""Validate new password strength"""
from app.core.security import security_manager
if not security_manager.validate_password(v):
raise ValueError('New password does not meet security requirements')
return v
class PasswordResetRequest(BaseModel):
"""Password reset request schema"""
email: EmailStr
class PasswordResetConfirm(BaseModel):
"""Password reset confirmation schema"""
token: str
new_password: str = Field(..., min_length=settings.PASSWORD_MIN_LENGTH)
@validator('new_password')
def validate_new_password(cls, v):
"""Validate new password strength"""
from app.core.security import security_manager
if not security_manager.validate_password(v):
raise ValueError('New password does not meet security requirements')
return v
class UserUpdate(BaseModel):
"""User update schema"""
full_name: Optional[str] = Field(None, min_length=2, max_length=100)
phone: Optional[str] = None
language: Optional[str] = Field(None, regex="^(es|en)$")
timezone: Optional[str] = None
@validator('phone')
def validate_phone(cls, v):
"""Validate phone number"""
if v and not validate_spanish_phone(v):
raise ValueError('Invalid Spanish phone number')
return v

View File

View File

@@ -0,0 +1,46 @@
"""
Messaging service for auth service
"""
from shared.messaging.rabbitmq import RabbitMQClient
from app.core.config import settings
# Global message publisher
message_publisher = RabbitMQClient(settings.RABBITMQ_URL)
# services/auth/Dockerfile
FROM python:3.11-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
curl \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements
COPY requirements.txt .
# Install Python dependencies
RUN pip install --no-cache-dir -r requirements.txt
# Copy shared libraries
COPY --from=shared /shared /app/shared
# Copy application code
COPY . .
# Add shared libraries to Python path
ENV PYTHONPATH="/app:/app/shared:$PYTHONPATH"
# Expose port
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Run application
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]