""" Multi-Account Middleware Extracts account from JWT token and injects into request context """ from django.utils.deprecation import MiddlewareMixin from django.http import JsonResponse from rest_framework import status try: import jwt JWT_AVAILABLE = True except ImportError: JWT_AVAILABLE = False from django.conf import settings class AccountContextMiddleware(MiddlewareMixin): """ Middleware that extracts account information from JWT token and adds it to request context for account isolation. """ def process_request(self, request): """Extract account from JWT token in Authorization header or session.""" # Skip for admin and auth endpoints if request.path.startswith('/admin/') or request.path.startswith('/api/v1/auth/'): return None # First, try to get user from Django session (cookie-based auth) # This handles cases where frontend uses credentials: 'include' with session cookies if hasattr(request, 'user') and request.user and request.user.is_authenticated: # User is authenticated via session - refresh from DB to get latest account/plan data # This ensures changes to account/plan are reflected immediately without re-login try: from .models import User as UserModel # Refresh user from DB with account and plan relationships to get latest data # This is important so account/plan changes are reflected immediately user = UserModel.objects.select_related('account', 'account__plan').get(id=request.user.id) # Update request.user with fresh data request.user = user # Get account from refreshed user user_account = getattr(user, 'account', None) if user_account: request.account = user_account return None except (AttributeError, UserModel.DoesNotExist, Exception): # If refresh fails, fallback to cached account try: user_account = getattr(request.user, 'account', None) if user_account: request.account = user_account return None except (AttributeError, Exception): pass # If account access fails (e.g., column mismatch), set to None request.account = None return None # Get token from Authorization header (JWT auth - for future implementation) auth_header = request.META.get('HTTP_AUTHORIZATION', '') if not auth_header.startswith('Bearer '): # No JWT token - if session auth didn't work, set account to None # But don't set request.user to None - it might be set by Django's auth middleware if not hasattr(request, 'account'): request.account = None return None token = auth_header.split(' ')[1] if len(auth_header.split(' ')) > 1 else None if not token: if not hasattr(request, 'account'): request.account = None return None try: if not JWT_AVAILABLE: # JWT library not installed yet - skip for now request.account = None request.user = None return None # Decode JWT token with signature verification # Use JWT_SECRET_KEY from settings (falls back to SECRET_KEY if not set) jwt_secret = getattr(settings, 'JWT_SECRET_KEY', getattr(settings, 'SECRET_KEY', None)) if not jwt_secret: raise ValueError("JWT_SECRET_KEY or SECRET_KEY must be set in settings") decoded = jwt.decode(token, jwt_secret, algorithms=[getattr(settings, 'JWT_ALGORITHM', 'HS256')]) # Extract user and account info from token user_id = decoded.get('user_id') account_id = decoded.get('account_id') if user_id: from .models import User, Account try: # Refresh user from DB with account and plan relationships to get latest data # This ensures changes to account/plan are reflected immediately without re-login user = User.objects.select_related('account', 'account__plan').get(id=user_id) request.user = user if account_id: # Verify account still exists account = Account.objects.get(id=account_id) # Token's account_id is authoritative for current context # For developers/admins, they can access any account # For regular users, verify they belong to this account if not user.is_admin_or_developer() and not user.is_system_account_user(): # Regular user - must belong to this account if user.account and user.account.id != account_id: # User doesn't belong to token's account - use user's account instead request.account = user.account else: request.account = account else: # Developer/admin/system user - use token's account (they can access any) request.account = account else: try: user_account = getattr(user, 'account', None) if user_account: request.account = user_account else: request.account = None except (AttributeError, Exception): # If account access fails (e.g., column mismatch), set to None request.account = None except (User.DoesNotExist, Account.DoesNotExist): request.account = None request.user = None else: request.account = None request.user = None except jwt.InvalidTokenError: request.account = None request.user = None except Exception: # Fail silently for now - allow unauthenticated access request.account = None request.user = None return None