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

@@ -3,6 +3,7 @@ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from sqlalchemy.orm import Session
from typing import Optional
from datetime import datetime
import os
from ...shared.config.database import get_db
from ...shared.config.settings import settings
@@ -15,26 +16,39 @@ def get_jwt_secret() -> str:
Get JWT secret securely, fail if not configured.
Never use hardcoded fallback secrets.
"""
default_secret = 'dev-secret-key-change-in-production-12345'
# Remove default secret entirely - fail fast if not configured
jwt_secret = getattr(settings, 'JWT_SECRET', None) or os.getenv('JWT_SECRET', None)
# Fail fast if secret is not configured or using default value
if not jwt_secret or jwt_secret == default_secret:
if settings.is_production:
raise ValueError(
'CRITICAL: JWT_SECRET is not properly configured in production. '
'Please set JWT_SECRET environment variable to a secure random string.'
)
# In development, warn but allow (startup validation should catch this)
import warnings
warnings.warn(
f'JWT_SECRET not configured. Using settings value but this is insecure. '
f'Set JWT_SECRET environment variable.',
UserWarning
# Fail fast if secret is not configured
if not jwt_secret:
error_msg = (
'CRITICAL: JWT_SECRET is not configured. '
'Please set JWT_SECRET environment variable to a secure random string (minimum 32 characters).'
)
jwt_secret = getattr(settings, 'JWT_SECRET', None)
if not jwt_secret:
raise ValueError('JWT_SECRET must be configured')
import logging
logger = logging.getLogger(__name__)
logger.error(error_msg)
if settings.is_production:
raise ValueError(error_msg)
else:
# In development, generate a secure secret but warn
import secrets
jwt_secret = secrets.token_urlsafe(64)
logger.warning(
f'JWT_SECRET not configured. Auto-generated secret for development. '
f'Set JWT_SECRET environment variable for production: {jwt_secret}'
)
# Validate JWT secret strength
if len(jwt_secret) < 32:
error_msg = 'JWT_SECRET must be at least 32 characters long for security.'
import logging
logger = logging.getLogger(__name__)
logger.error(error_msg)
if settings.is_production:
raise ValueError(error_msg)
else:
logger.warning(error_msg)
return jwt_secret
@@ -80,6 +94,22 @@ def get_current_user(
user = db.query(User).filter(User.id == user_id).first()
if user is None:
raise credentials_exception
# Check if user account is active
if not user.is_active:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail='Account is disabled. Please contact support.'
)
# Check if account is locked
if user.locked_until and user.locked_until > datetime.utcnow():
remaining_minutes = int((user.locked_until - datetime.utcnow()).total_seconds() / 60)
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f'Account is temporarily locked due to multiple failed login attempts. Please try again in {remaining_minutes} minute(s).'
)
return user
def authorize_roles(*allowed_roles: str):
@@ -102,7 +132,8 @@ def get_current_user_optional(
) -> Optional[User]:
"""
Get current user optionally from either Authorization header or httpOnly cookie.
Returns None if no valid token is found.
Returns None if no valid token is found, or if user is inactive/locked.
This ensures inactive/locked users are never considered "authenticated" even for optional features.
"""
# Try to get token from Authorization header first
token = None
@@ -124,7 +155,19 @@ def get_current_user_optional(
return None
except (JWTError, ValueError):
return None
user = db.query(User).filter(User.id == user_id).first()
if user is None:
return None
# Check if user account is active - return None for inactive users
if not user.is_active:
return None
# Check if account is locked - return None for locked users
if user.locked_until and user.locked_until > datetime.utcnow():
return None
return user
def verify_token(token: str) -> dict: