This commit is contained in:
Iliyan Angelov
2025-12-01 01:08:39 +02:00
parent 0fa2adeb19
commit 1a103a769f
234 changed files with 5513 additions and 283 deletions

View File

@@ -28,6 +28,10 @@ class Settings(BaseSettings):
CORS_ORIGINS: List[str] = Field(default_factory=lambda: ['http://localhost:5173', 'http://localhost:3000', 'http://127.0.0.1:5173'], description='Allowed CORS origins')
RATE_LIMIT_ENABLED: bool = Field(default=True, description='Enable rate limiting')
RATE_LIMIT_PER_MINUTE: int = Field(default=60, description='Requests per minute per IP')
RATE_LIMIT_ADMIN_PER_MINUTE: int = Field(default=300, description='Requests per minute for admin users')
RATE_LIMIT_STAFF_PER_MINUTE: int = Field(default=200, description='Requests per minute for staff users')
RATE_LIMIT_ACCOUNTANT_PER_MINUTE: int = Field(default=200, description='Requests per minute for accountant users')
RATE_LIMIT_CUSTOMER_PER_MINUTE: int = Field(default=100, description='Requests per minute for customer users')
CSRF_PROTECTION_ENABLED: bool = Field(default=True, description='Enable CSRF protection')
HSTS_PRELOAD_ENABLED: bool = Field(default=False, description='Enable HSTS preload directive (requires domain submission to hstspreload.org)')
LOG_LEVEL: str = Field(default='INFO', description='Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL')

View File

@@ -0,0 +1,51 @@
"""
API versioning middleware for backward compatibility.
"""
from fastapi import Request, HTTPException, status
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import Response
from typing import Callable
import re
from ...shared.config.logging_config import get_logger
logger = get_logger(__name__)
class APIVersioningMiddleware(BaseHTTPMiddleware):
"""Middleware to handle API versioning."""
def __init__(self, app, default_version: str = "v1"):
super().__init__(app)
self.default_version = default_version
self.version_pattern = re.compile(r'/api/v(\d+)/')
async def dispatch(self, request: Request, call_next: Callable) -> Response:
"""Process request and handle versioning."""
path = request.url.path
# Check if path starts with /api
if path.startswith('/api'):
# Extract version from path
version_match = self.version_pattern.search(path)
if version_match:
version = version_match.group(1)
# Store version in request state
request.state.api_version = f"v{version}"
# Remove version from path for routing
new_path = self.version_pattern.sub('/api/', path)
request.scope['path'] = new_path
elif path.startswith('/api/') and not path.startswith('/api/v'):
# No version specified, use default
request.state.api_version = self.default_version
else:
# Health check or other non-versioned endpoints
request.state.api_version = None
response = await call_next(request)
# Add version header to response
if hasattr(request.state, 'api_version') and request.state.api_version:
response.headers['X-API-Version'] = request.state.api_version
return response

View File

@@ -0,0 +1,111 @@
"""
Decorator for automatic audit logging of financial operations.
"""
from functools import wraps
from typing import Callable, Any
from fastapi import Request
from sqlalchemy.orm import Session
from ...analytics.services.audit_service import audit_service
from ...shared.config.logging_config import get_logger
logger = get_logger(__name__)
def audit_financial_operation(action: str, resource_type: str):
"""
Decorator to automatically log financial operations to audit trail.
Usage:
@audit_financial_operation('payment_refunded', 'payment')
async def refund_payment(...):
...
"""
def decorator(func: Callable) -> Callable:
@wraps(func)
async def wrapper(*args, **kwargs):
# Extract request and db from function arguments
request: Request = None
db: Session = None
current_user = None
# Find request and db in kwargs or args
for arg in list(args) + list(kwargs.values()):
if isinstance(arg, Request):
request = arg
elif isinstance(arg, Session):
db = arg
elif hasattr(arg, 'id') and hasattr(arg, 'email'): # Likely a User object
current_user = arg
# Get request from kwargs if not found
if not request and 'request' in kwargs:
request = kwargs['request']
if not db and 'db' in kwargs:
db = kwargs['db']
if not current_user and 'current_user' in kwargs:
current_user = kwargs['current_user']
# Extract resource_id from function arguments if available
resource_id = None
if 'id' in kwargs:
resource_id = kwargs['id']
elif len(args) > 0 and isinstance(args[0], int):
resource_id = args[0]
# Get client info
client_ip = None
user_agent = None
request_id = None
if request:
client_ip = request.client.host if request.client else None
user_agent = request.headers.get('User-Agent')
request_id = getattr(request.state, 'request_id', None)
try:
# Execute the function
result = await func(*args, **kwargs)
# Log successful operation
if db and current_user:
await audit_service.log_action(
db=db,
action=action,
resource_type=resource_type,
user_id=current_user.id if current_user else None,
resource_id=resource_id,
ip_address=client_ip,
user_agent=user_agent,
request_id=request_id,
details={
'function': func.__name__,
'result': 'success'
},
status='success'
)
return result
except Exception as e:
# Log failed operation
if db and current_user:
await audit_service.log_action(
db=db,
action=action,
resource_type=resource_type,
user_id=current_user.id if current_user else None,
resource_id=resource_id,
ip_address=client_ip,
user_agent=user_agent,
request_id=request_id,
details={
'function': func.__name__,
'result': 'failed',
'error': str(e)
},
status='failed',
error_message=str(e)
)
raise
return wrapper
return decorator