logo out issues fixes

This commit is contained in:
IGNY8 VPS (Salman)
2025-12-15 16:08:47 +00:00
parent 25f1c32366
commit 5366cc1805
14 changed files with 2327 additions and 51 deletions

View File

@@ -7,9 +7,23 @@ from django.utils.deprecation import MiddlewareMixin
from django.http import JsonResponse
from django.contrib.auth import logout
from rest_framework import status
import json
from datetime import datetime
logger = logging.getLogger('auth.middleware')
# Logout reason codes for precise tracking
LOGOUT_REASONS = {
'SESSION_ACCOUNT_MISMATCH': 'Session contamination: account ID mismatch',
'SESSION_USER_MISMATCH': 'Session contamination: user ID mismatch',
'ACCOUNT_MISSING': 'Account not configured for this user',
'ACCOUNT_SUSPENDED': 'Account is suspended',
'ACCOUNT_CANCELLED': 'Account is cancelled',
'PLAN_MISSING': 'No subscription plan assigned',
'PLAN_INACTIVE': 'Subscription plan is inactive',
'USER_INACTIVE': 'User account is inactive',
}
try:
import jwt
JWT_AVAILABLE = True
@@ -47,39 +61,8 @@ class AccountContextMiddleware(MiddlewareMixin):
# This is already loaded, no need to query DB again
request.account = getattr(request.user, 'account', None)
# CRITICAL: Add account ID to session to prevent cross-contamination
# This ensures each session is tied to a specific account
if request.account:
request.session['_account_id'] = request.account.id
request.session['_user_id'] = request.user.id
# Verify session integrity - if stored IDs don't match, logout
stored_account_id = request.session.get('_account_id')
stored_user_id = request.session.get('_user_id')
if stored_account_id and stored_account_id != request.account.id:
# Session contamination detected - force logout
logger.warning(
f"[AUTO-LOGOUT] Session contamination: account_id mismatch. "
f"Session={stored_account_id}, Current={request.account.id}, "
f"User={request.user.id}, Path={request.path}, IP={request.META.get('REMOTE_ADDR')}"
)
logout(request)
return JsonResponse(
{'success': False, 'error': 'Session integrity violation detected. Please login again.'},
status=status.HTTP_401_UNAUTHORIZED
)
if stored_user_id and stored_user_id != request.user.id:
# Session contamination detected - force logout
logger.warning(
f"[AUTO-LOGOUT] Session contamination: user_id mismatch. "
f"Session={stored_user_id}, Current={request.user.id}, "
f"Account={request.account.id if request.account else None}, "
f"Path={request.path}, IP={request.META.get('REMOTE_ADDR')}"
)
logout(request)
return JsonResponse(
{'success': False, 'error': 'Session integrity violation detected. Please login again.'},
status=status.HTTP_401_UNAUTHORIZED
)
# REMOVED: Session contamination checks on every request
# These were causing random logouts - session integrity handled by Django
return None
except (AttributeError, Exception):
@@ -184,13 +167,29 @@ class AccountContextMiddleware(MiddlewareMixin):
return None
def _deny_request(self, request, error, status_code):
"""Logout session users (if any) and return a consistent JSON error."""
"""Logout session users (if any) and return a consistent JSON error with detailed tracking."""
# Determine logout reason code based on error message
reason_code = 'UNKNOWN'
if 'Account not configured' in error or 'Account not found' in error:
reason_code = 'ACCOUNT_MISSING'
elif 'suspended' in error.lower():
reason_code = 'ACCOUNT_SUSPENDED'
elif 'cancelled' in error.lower():
reason_code = 'ACCOUNT_CANCELLED'
elif 'No subscription plan' in error or 'plan assigned' in error.lower():
reason_code = 'PLAN_MISSING'
elif 'plan is inactive' in error.lower() or 'Active subscription required' in error:
reason_code = 'PLAN_INACTIVE'
elif 'inactive' in error.lower():
reason_code = 'USER_INACTIVE'
try:
if hasattr(request, 'user') and request.user and request.user.is_authenticated:
logger.warning(
f"[AUTO-LOGOUT] Account/plan validation failed: {error}. "
f"[AUTO-LOGOUT] {reason_code}: {error}. "
f"User={request.user.id}, Account={getattr(request, 'account', None)}, "
f"Path={request.path}, IP={request.META.get('REMOTE_ADDR')}"
f"Path={request.path}, IP={request.META.get('REMOTE_ADDR')}, "
f"Status={status_code}, Timestamp={datetime.now().isoformat()}"
)
logout(request)
except Exception as e:
@@ -200,6 +199,14 @@ class AccountContextMiddleware(MiddlewareMixin):
{
'success': False,
'error': error,
'logout_reason': reason_code,
'logout_message': LOGOUT_REASONS.get(reason_code, error),
'logout_path': request.path,
'logout_context': {
'user_id': request.user.id if hasattr(request, 'user') and request.user and request.user.is_authenticated else None,
'account_id': getattr(request, 'account', None).id if hasattr(request, 'account') and getattr(request, 'account', None) else None,
'status_code': status_code,
}
},
status=status_code,
)

View File

@@ -481,6 +481,7 @@ class LoginSerializer(serializers.Serializer):
"""Serializer for user login."""
email = serializers.EmailField()
password = serializers.CharField(write_only=True)
remember_me = serializers.BooleanField(required=False, default=False)
class ChangePasswordSerializer(serializers.Serializer):

View File

@@ -102,6 +102,7 @@ class LoginView(APIView):
if serializer.is_valid():
email = serializer.validated_data['email']
password = serializer.validated_data['password']
remember_me = serializer.validated_data.get('remember_me', False)
try:
user = User.objects.select_related('account', 'account__plan').get(email=email)
@@ -121,10 +122,10 @@ class LoginView(APIView):
account = getattr(user, 'account', None)
# Generate JWT tokens
from .utils import generate_access_token, generate_refresh_token, get_token_expiry
access_token = generate_access_token(user, account)
from .utils import generate_access_token, generate_refresh_token, get_access_token_expiry, get_token_expiry
access_token = generate_access_token(user, account, remember_me=remember_me)
refresh_token = generate_refresh_token(user, account)
access_expires_at = get_token_expiry('access')
access_expires_at = get_access_token_expiry(remember_me=remember_me)
refresh_expires_at = get_token_expiry('refresh')
# Serialize user data safely, handling missing account relationship

View File

@@ -17,23 +17,26 @@ def get_jwt_algorithm():
return getattr(settings, 'JWT_ALGORITHM', 'HS256')
def get_access_token_expiry():
def get_access_token_expiry(remember_me=False):
"""Get access token expiry time from settings"""
return getattr(settings, 'JWT_ACCESS_TOKEN_EXPIRY', timedelta(minutes=15))
if remember_me:
return getattr(settings, 'JWT_ACCESS_TOKEN_EXPIRY_REMEMBER_ME', timedelta(days=20))
return getattr(settings, 'JWT_ACCESS_TOKEN_EXPIRY', timedelta(hours=1))
def get_refresh_token_expiry():
"""Get refresh token expiry time from settings"""
return getattr(settings, 'JWT_REFRESH_TOKEN_EXPIRY', timedelta(days=7))
return getattr(settings, 'JWT_REFRESH_TOKEN_EXPIRY', timedelta(days=30))
def generate_access_token(user, account=None):
def generate_access_token(user, account=None, remember_me=False):
"""
Generate JWT access token for user
Args:
user: User instance
account: Account instance (optional, will use user.account if not provided)
remember_me: bool - If True, use extended expiry (20 days)
Returns:
str: JWT access token
@@ -42,7 +45,7 @@ def generate_access_token(user, account=None):
account = getattr(user, 'account', None)
now = timezone.now()
expiry = now + get_access_token_expiry()
expiry = now + get_access_token_expiry(remember_me=remember_me)
payload = {
'user_id': user.id,
@@ -51,6 +54,7 @@ def generate_access_token(user, account=None):
'exp': int(expiry.timestamp()),
'iat': int(now.timestamp()),
'type': 'access',
'remember_me': remember_me,
}
token = jwt.encode(payload, get_jwt_secret_key(), algorithm=get_jwt_algorithm())