logo out issues fixes
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -97,7 +97,7 @@ CSRF_COOKIE_SECURE = USE_SECURE_COOKIES
|
||||
SESSION_COOKIE_NAME = 'igny8_sessionid' # Custom name to avoid conflicts
|
||||
SESSION_COOKIE_HTTPONLY = True # Prevent JavaScript access
|
||||
SESSION_COOKIE_SAMESITE = 'Strict' # Prevent cross-site cookie sharing
|
||||
SESSION_COOKIE_AGE = 86400 # 24 hours
|
||||
SESSION_COOKIE_AGE = 3600 # 1 hour default (increased if remember me checked)
|
||||
SESSION_SAVE_EVERY_REQUEST = False # Don't update session on every request (reduces DB load)
|
||||
SESSION_COOKIE_PATH = '/' # Explicit path
|
||||
# Don't set SESSION_COOKIE_DOMAIN - let it default to current domain for strict isolation
|
||||
@@ -520,7 +520,9 @@ CORS_EXPOSE_HEADERS = [
|
||||
# JWT Configuration
|
||||
JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', SECRET_KEY)
|
||||
JWT_ALGORITHM = 'HS256'
|
||||
JWT_ACCESS_TOKEN_EXPIRY = timedelta(minutes=15)
|
||||
# Default: 1 hour for normal login, 20 days for remember me
|
||||
JWT_ACCESS_TOKEN_EXPIRY = timedelta(hours=1) # Increased from 15 minutes
|
||||
JWT_ACCESS_TOKEN_EXPIRY_REMEMBER_ME = timedelta(days=20) # For remember me users
|
||||
JWT_REFRESH_TOKEN_EXPIRY = timedelta(days=30) # Extended to 30 days for persistent login
|
||||
|
||||
# Celery Configuration
|
||||
|
||||
Reference in New Issue
Block a user