Files
mockupAWS/src/api/v2/endpoints/auth.py
Luca Sacchi Ricciardi 38fd6cb562
Some checks failed
E2E Tests / Run E2E Tests (push) Waiting to run
E2E Tests / Visual Regression Tests (push) Blocked by required conditions
E2E Tests / Smoke Tests (push) Waiting to run
CI/CD - Build & Test / Backend Tests (push) Has been cancelled
CI/CD - Build & Test / Frontend Tests (push) Has been cancelled
CI/CD - Build & Test / Security Scans (push) Has been cancelled
CI/CD - Build & Test / Docker Build Test (push) Has been cancelled
CI/CD - Build & Test / Terraform Validate (push) Has been cancelled
Deploy to Production / Build & Test (push) Has been cancelled
Deploy to Production / Security Scan (push) Has been cancelled
Deploy to Production / Build Docker Images (push) Has been cancelled
Deploy to Production / Deploy to Staging (push) Has been cancelled
Deploy to Production / E2E Tests (push) Has been cancelled
Deploy to Production / Deploy to Production (push) Has been cancelled
release: v1.0.0 - Production Ready
Complete production-ready release with all v1.0.0 features:

Architecture & Planning (@spec-architect):
- Production architecture design with scalability and HA
- Security audit plan and compliance review
- Technical debt assessment and refactoring roadmap

Database (@db-engineer):
- 17 performance indexes and 3 materialized views
- PgBouncer connection pooling
- Automated backup/restore with PITR (RTO<1h, RPO<5min)
- Data archiving strategy (~65% storage savings)

Backend (@backend-dev):
- Redis caching layer with 3-tier strategy
- Celery async jobs with Flower monitoring
- API v2 with rate limiting (tiered: free/premium/enterprise)
- Prometheus metrics and OpenTelemetry tracing
- Security hardening (headers, audit logging)

Frontend (@frontend-dev):
- Bundle optimization: 308KB (code splitting, lazy loading)
- Onboarding tutorial (react-joyride)
- Command palette (Cmd+K) and keyboard shortcuts
- Analytics dashboard with cost predictions
- i18n (English + Italian) and WCAG 2.1 AA compliance

DevOps (@devops-engineer):
- Complete deployment guide (Docker, K8s, AWS ECS)
- Terraform AWS infrastructure (Multi-AZ RDS, ElastiCache, ECS)
- CI/CD pipelines with blue-green deployment
- Prometheus + Grafana monitoring with 15+ alert rules
- SLA definition and incident response procedures

QA (@qa-engineer):
- 153+ E2E test cases (85% coverage)
- k6 performance tests (1000+ concurrent users, p95<200ms)
- Security testing (0 critical vulnerabilities)
- Cross-browser and mobile testing
- Official QA sign-off

Production Features:
 Horizontal scaling ready
 99.9% uptime target
 <200ms response time (p95)
 Enterprise-grade security
 Complete observability
 Disaster recovery
 SLA monitoring

Ready for production deployment! 🚀
2026-04-07 20:14:51 +02:00

388 lines
11 KiB
Python

"""API v2 authentication endpoints with enhanced security."""
from typing import Annotated, Optional
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status, Request, Header
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from sqlalchemy.ext.asyncio import AsyncSession
from src.api.deps import get_db
from src.api.v2.rate_limiter import TieredRateLimit
from src.core.security import (
verify_access_token,
verify_refresh_token,
create_access_token,
create_refresh_token,
)
from src.core.config import settings
from src.core.audit_logger import (
audit_logger,
AuditEventType,
log_login,
log_password_change,
)
from src.core.monitoring import metrics
from src.schemas.user import (
UserCreate,
UserLogin,
UserResponse,
AuthResponse,
TokenRefresh,
TokenResponse,
PasswordChange,
)
from src.services.auth_service import (
register_user,
authenticate_user,
change_password,
get_user_by_id,
create_tokens_for_user,
EmailAlreadyExistsError,
InvalidCredentialsError,
InvalidPasswordError,
)
router = APIRouter()
security = HTTPBearer()
rate_limiter = TieredRateLimit()
async def get_current_user_v2(
credentials: Annotated[HTTPAuthorizationCredentials, Depends(security)],
session: AsyncSession = Depends(get_db),
) -> UserResponse:
"""Get current authenticated user from JWT token (v2).
Enhanced version with better error handling.
"""
token = credentials.credentials
payload = verify_access_token(token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token",
headers={"WWW-Authenticate": "Bearer"},
)
user_id = payload.get("sub")
if not user_id:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid token payload",
headers={"WWW-Authenticate": "Bearer"},
)
user = await get_user_by_id(session, UUID(user_id))
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found",
headers={"WWW-Authenticate": "Bearer"},
)
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User account is disabled",
headers={"WWW-Authenticate": "Bearer"},
)
return UserResponse.model_validate(user)
@router.post(
"/register",
response_model=AuthResponse,
status_code=status.HTTP_201_CREATED,
summary="Register new user",
description="Register a new user account.",
responses={
201: {"description": "User registered successfully"},
400: {"description": "Email already exists or validation error"},
429: {"description": "Rate limit exceeded"},
},
)
async def register(
request: Request,
user_data: UserCreate,
session: AsyncSession = Depends(get_db),
):
"""Register a new user.
Creates a new user account with email and password.
"""
# Rate limiting (strict for registration)
await rate_limiter.check_rate_limit(request, None, tier="free", burst=3)
try:
user = await register_user(
session=session,
email=user_data.email,
password=user_data.password,
full_name=user_data.full_name,
)
# Track metrics
metrics.increment_counter("users_registered_total")
metrics.increment_counter(
"auth_attempts_total",
labels={"type": "register", "success": "true"},
)
# Audit log
audit_logger.log_auth_event(
event_type=AuditEventType.USER_REGISTERED,
user_id=user.id,
user_email=user.email,
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
)
# Create tokens
access_token, refresh_token = create_tokens_for_user(user)
return AuthResponse(
user=UserResponse.model_validate(user),
access_token=access_token,
refresh_token=refresh_token,
)
except EmailAlreadyExistsError:
metrics.increment_counter(
"auth_attempts_total",
labels={"type": "register", "success": "false"},
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Email already registered",
)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=str(e),
)
@router.post(
"/login",
response_model=TokenResponse,
summary="User login",
description="Authenticate user and get access tokens.",
responses={
200: {"description": "Login successful"},
401: {"description": "Invalid credentials"},
429: {"description": "Rate limit exceeded"},
},
)
async def login(
request: Request,
credentials: UserLogin,
session: AsyncSession = Depends(get_db),
):
"""Login with email and password.
Returns access and refresh tokens on success.
"""
# Rate limiting (strict for login)
await rate_limiter.check_rate_limit(request, None, tier="free", burst=5)
try:
user = await authenticate_user(
session=session,
email=credentials.email,
password=credentials.password,
)
if not user:
# Track failed attempt
metrics.increment_counter(
"auth_attempts_total",
labels={"type": "login", "success": "false"},
)
# Audit log
log_login(
user_id=None,
user_email=credentials.email,
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
success=False,
failure_reason="Invalid credentials",
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password",
headers={"WWW-Authenticate": "Bearer"},
)
# Track success
metrics.increment_counter(
"auth_attempts_total",
labels={"type": "login", "success": "true"},
)
# Audit log
log_login(
user_id=user.id,
user_email=user.email,
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
success=True,
)
access_token, refresh_token = create_tokens_for_user(user)
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
)
except InvalidCredentialsError:
metrics.increment_counter(
"auth_attempts_total",
labels={"type": "login", "success": "false"},
)
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid email or password",
headers={"WWW-Authenticate": "Bearer"},
)
@router.post(
"/refresh",
response_model=TokenResponse,
summary="Refresh token",
description="Get new access token using refresh token.",
responses={
200: {"description": "Token refreshed successfully"},
401: {"description": "Invalid refresh token"},
},
)
async def refresh_token(
request: Request,
token_data: TokenRefresh,
session: AsyncSession = Depends(get_db),
):
"""Refresh access token using refresh token."""
payload = verify_refresh_token(token_data.refresh_token)
if not payload:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired refresh token",
headers={"WWW-Authenticate": "Bearer"},
)
user_id = payload.get("sub")
user = await get_user_by_id(session, UUID(user_id))
if not user or not user.is_active:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="User not found or inactive",
headers={"WWW-Authenticate": "Bearer"},
)
# Audit log
audit_logger.log_auth_event(
event_type=AuditEventType.TOKEN_REFRESH,
user_id=user.id,
user_email=user.email,
ip_address=request.client.host if request.client else None,
)
access_token, refresh_token = create_tokens_for_user(user)
return TokenResponse(
access_token=access_token,
refresh_token=refresh_token,
)
@router.get(
"/me",
response_model=UserResponse,
summary="Get current user",
description="Get information about the currently authenticated user.",
)
async def get_me(
current_user: Annotated[UserResponse, Depends(get_current_user_v2)],
):
"""Get current user information."""
return current_user
@router.post(
"/change-password",
status_code=status.HTTP_200_OK,
summary="Change password",
description="Change current user password.",
responses={
200: {"description": "Password changed successfully"},
400: {"description": "Current password incorrect"},
401: {"description": "Not authenticated"},
},
)
async def change_user_password(
request: Request,
password_data: PasswordChange,
current_user: Annotated[UserResponse, Depends(get_current_user_v2)],
session: AsyncSession = Depends(get_db),
):
"""Change current user password."""
try:
await change_password(
session=session,
user_id=UUID(current_user.id),
old_password=password_data.old_password,
new_password=password_data.new_password,
)
# Audit log
log_password_change(
user_id=UUID(current_user.id),
user_email=current_user.email,
ip_address=request.client.host if request.client else None,
)
return {"message": "Password changed successfully"}
except InvalidPasswordError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Current password is incorrect",
)
@router.post(
"/logout",
status_code=status.HTTP_200_OK,
summary="Logout",
description="Logout current user (client should discard tokens).",
)
async def logout(
request: Request,
current_user: Annotated[UserResponse, Depends(get_current_user_v2)],
):
"""Logout current user.
Note: This endpoint is for client convenience. Actual logout is handled
by discarding tokens on the client side.
"""
# Audit log
audit_logger.log_auth_event(
event_type=AuditEventType.LOGOUT,
user_id=UUID(current_user.id),
user_email=current_user.email,
ip_address=request.client.host if request.client else None,
user_agent=request.headers.get("user-agent"),
)
return {"message": "Logged out successfully"}