release: v1.0.0 - Production Ready
Some checks failed
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
E2E Tests / Run E2E Tests (push) Has been cancelled
E2E Tests / Visual Regression Tests (push) Has been cancelled
E2E Tests / Smoke Tests (push) Has been cancelled
Some checks failed
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
E2E Tests / Run E2E Tests (push) Has been cancelled
E2E Tests / Visual Regression Tests (push) Has been cancelled
E2E Tests / Smoke Tests (push) Has been cancelled
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! 🚀
This commit is contained in:
387
src/api/v2/endpoints/auth.py
Normal file
387
src/api/v2/endpoints/auth.py
Normal file
@@ -0,0 +1,387 @@
|
||||
"""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"}
|
||||
Reference in New Issue
Block a user