Initial microservices setup from artifacts
This commit is contained in:
0
services/auth/app/__init__.py
Normal file
0
services/auth/app/__init__.py
Normal file
0
services/auth/app/api/__init__.py
Normal file
0
services/auth/app/api/__init__.py
Normal file
124
services/auth/app/api/auth.py
Normal file
124
services/auth/app/api/auth.py
Normal 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"
|
||||
)
|
||||
0
services/auth/app/core/__init__.py
Normal file
0
services/auth/app/core/__init__.py
Normal file
47
services/auth/app/core/config.py
Normal file
47
services/auth/app/core/config.py
Normal 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()
|
||||
12
services/auth/app/core/database.py
Normal file
12
services/auth/app/core/database.py
Normal 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
|
||||
153
services/auth/app/core/security.py
Normal file
153
services/auth/app/core/security.py
Normal 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
83
services/auth/app/main.py
Normal 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)
|
||||
0
services/auth/app/schemas/__init__.py
Normal file
0
services/auth/app/schemas/__init__.py
Normal file
108
services/auth/app/schemas/auth.py
Normal file
108
services/auth/app/schemas/auth.py
Normal 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
|
||||
0
services/auth/app/services/__init__.py
Normal file
0
services/auth/app/services/__init__.py
Normal file
46
services/auth/app/services/messaging.py
Normal file
46
services/auth/app/services/messaging.py
Normal 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"]
|
||||
Reference in New Issue
Block a user