From 55bb015e32acc73317d628faee6f8ac2e168ead2 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Tue, 25 Aug 2026 11:08:22 +0330 Subject: [PATCH] fix(account): persist client IP, device OS, and user agent on auth and add serializer fallbacks --- apps/account/serializers/user.py | 34 ++++++++++ apps/account/views/auth.py | 37 +++++++++++ apps/account/views/user.py | 109 ++++++++++++++++++++++++++----- utils/ip_helper.py | 24 +++++-- 4 files changed, 181 insertions(+), 23 deletions(-) diff --git a/apps/account/serializers/user.py b/apps/account/serializers/user.py index 70ffed0..7282fc6 100644 --- a/apps/account/serializers/user.py +++ b/apps/account/serializers/user.py @@ -233,6 +233,10 @@ class AdminUserSerializer(serializers.ModelSerializer): email = serializers.EmailField(required=True) auth_token = serializers.SerializerMethodField() plain_password = serializers.SerializerMethodField() + client_ip = serializers.SerializerMethodField() + device_os = serializers.SerializerMethodField() + user_agent = serializers.SerializerMethodField() + device_id = serializers.SerializerMethodField() def get_auth_token(self, obj): token = Token.objects.filter(user=obj).first() @@ -244,6 +248,36 @@ class AdminUserSerializer(serializers.ModelSerializer): return obj.get_plain_password() return None + def get_client_ip(self, obj): + if obj.client_ip: + return obj.client_ip + history = obj.login_history.filter(ip__isnull=False).exclude(ip='').order_by('-at_time', '-id').first() + if history and history.ip: + return history.ip + loc_history = obj.location_history.filter(ip__isnull=False).exclude(ip='').order_by('-at_time', '-id').first() + if loc_history and loc_history.ip: + return loc_history.ip + return None + + def get_device_os(self, obj): + if obj.device_os: + return obj.device_os + history = obj.login_history.filter(device_os__isnull=False).exclude(device_os='').order_by('-at_time', '-id').first() + if history and history.device_os: + return history.device_os + return None + + def get_user_agent(self, obj): + if obj.user_agent: + return obj.user_agent + history = obj.login_history.filter(user_agent__isnull=False).exclude(user_agent='').order_by('-at_time', '-id').first() + if history and history.user_agent: + return history.user_agent + return None + + def get_device_id(self, obj): + return obj.device_id or None + class Meta: model = User fields = [ diff --git a/apps/account/views/auth.py b/apps/account/views/auth.py index a4bf4e3..c1baf66 100644 --- a/apps/account/views/auth.py +++ b/apps/account/views/auth.py @@ -15,10 +15,26 @@ from utils import absolute_url from utils.redis import OnlineClassTokenManager +from django.utils import timezone +from utils.ip_helper import get_client_ip + logger = logging.getLogger(__name__) UserModel = get_user_model() +def detect_device_os(user_agent): + if not user_agent: + return None + ua = user_agent.lower() + if 'android' in ua: + return 'android' + if 'iphone' in ua or 'ipad' in ua or 'ios' in ua: + return 'apple' + if any(k in ua for k in ['mozilla', 'chrome', 'safari', 'windows', 'macintosh', 'linux']): + return 'web' + return None + + class ExchangeTokenAPIView(GenericAPIView): """ تبدیل temporary token به اطلاعات کاربر برای ورود از اپ موبایل @@ -104,6 +120,27 @@ class ExchangeTokenAPIView(GenericAPIView): # دریافت یا تولید Token واقعی کاربر auth_token, _ = Token.objects.get_or_create(user=user) + client_ip = get_client_ip(request) + user_agent = request.META.get('HTTP_USER_AGENT', '') + device_os = token_data.get('device_os') + if not device_os and user_agent: + device_os = detect_device_os(user_agent) + + user.last_login = timezone.now() + if client_ip: + user.client_ip = client_ip + if user_agent: + user.user_agent = user_agent + if device_os: + user.device_os = device_os + user.save() + + user.login_history.create( + ip=client_ip, + user_agent=user_agent, + device_os=user.device_os or device_os, + ) + # دریافت avatar URL avatar_url = None if hasattr(user, 'avatar') and user.avatar: diff --git a/apps/account/views/user.py b/apps/account/views/user.py index d4ad486..293829e 100644 --- a/apps/account/views/user.py +++ b/apps/account/views/user.py @@ -31,9 +31,24 @@ from utils import send_email, is_valid_email, absolute_https_url from config.settings import base as settings from apps.account.permissions import IsActiveUser, IsSuperAdminOrReadOnlyForProfessor from apps.account.doc import * +from utils.ip_helper import get_client_ip + logger = logging.getLogger(__name__) +def detect_device_os(user_agent): + if not user_agent: + return None + ua = user_agent.lower() + if 'android' in ua: + return 'android' + if 'iphone' in ua or 'ipad' in ua or 'ios' in ua: + return 'apple' + if any(k in ua for k in ['mozilla', 'chrome', 'safari', 'windows', 'macintosh', 'linux']): + return 'web' + return None + + class UserGuestView(CreateAPIView): permission_classes = [AllowAny] @@ -90,6 +105,8 @@ class UserGuestView(CreateAPIView): lat = serializer.validated_data.pop('lat', None) lon = serializer.validated_data.pop('lon', None) user_timezone = serializer.validated_data.pop('timezone', None) + client_ip = get_client_ip(self.request) + user_agent = self.request.META.get('HTTP_USER_AGENT', '') serializer_data = dict(serializer.validated_data) @@ -103,12 +120,24 @@ class UserGuestView(CreateAPIView): logger.info(f'Guest-(created)->: {obj.device_id}') obj.last_login = timezone.now() + if client_ip: + obj.client_ip = client_ip + if user_agent: + obj.user_agent = user_agent + if device_os: + obj.device_os = device_os + elif not obj.device_os and user_agent: + obj.device_os = detect_device_os(user_agent) + if fcm: + obj.fcm = fcm obj.save() login_history_obj = obj.login_history.create( lat=lat, lon=lon, - ip=self.get_client_ip(), + ip=client_ip, timezone=user_timezone, + user_agent=user_agent, + device_os=obj.device_os, ) return obj @@ -285,7 +314,12 @@ class UserVerifyView(CreateAPIView): verify_data.pop('email', None) verify_data.pop('device_id', None) user = self.perform_create( - email=email, device_id=device_id, **verify_data + email=email, + device_id=device_id, + client_ip=get_client_ip(request), + user_agent=request.META.get('HTTP_USER_AGENT', ''), + device_os=request.data.get('device_os'), + **verify_data ) token, _ = Token.objects.get_or_create(user=user) return Response(data={ @@ -309,9 +343,17 @@ class UserVerifyView(CreateAPIView): email = kwargs.get('email') device_id = kwargs.get('device_id') password = kwargs.get('password') + client_ip = kwargs.get('client_ip') + user_agent = kwargs.get('user_agent') + device_os = kwargs.get('device_os') + if not device_os and user_agent: + device_os = detect_device_os(user_agent) create_kwargs = dict(kwargs) create_kwargs.pop('password', None) + create_kwargs.pop('client_ip', None) + create_kwargs.pop('user_agent', None) + create_kwargs.pop('device_os', None) user = User.objects.filter(email=email).first() if user: @@ -321,6 +363,12 @@ class UserVerifyView(CreateAPIView): user.deleted_at = None if device_id: user.device_id = device_id + if client_ip: + user.client_ip = client_ip + if user_agent: + user.user_agent = user_agent + if device_os: + user.device_os = device_os user.last_login = timezone.now() user.save() else: @@ -344,11 +392,23 @@ class UserVerifyView(CreateAPIView): if device_id: user.device_id = device_id + if client_ip: + user.client_ip = client_ip + if user_agent: + user.user_agent = user_agent + if device_os: + user.device_os = device_os user.last_login = timezone.now() user.is_active = True user.deleted_at = None user.save() + user.login_history.create( + ip=client_ip, + user_agent=user_agent, + device_os=user.device_os or device_os, + ) + return user @@ -400,13 +460,7 @@ class UserLoginView(CreateAPIView): def post(self, request, *args, **kwargs): return super().post(request, *args, **kwargs) def get_client_ip(self): - request = self.request - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip + return get_client_ip(self.request) def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) @@ -431,15 +485,32 @@ class UserLoginView(CreateAPIView): if not user: raise ValidationError({"password": "password is incorrect"}) + client_ip = get_client_ip(request) + user_agent = request.META.get('HTTP_USER_AGENT', '') + device_os = request.data.get('device_os') + if not device_os and user_agent: + device_os = detect_device_os(user_agent) + device_id = request.data.get('device_id') + user_timezone = serializer.validated_data.pop('timezone', None) user.last_login = timezone.now() user.is_active = True + if client_ip: + user.client_ip = client_ip + if user_agent: + user.user_agent = user_agent + if device_os: + user.device_os = device_os + if device_id: + user.device_id = device_id user.save() token, created = Token.objects.get_or_create(user=user) login_history_obj = user.login_history.create( - ip=self.get_client_ip(), + ip=client_ip, timezone=user_timezone, + user_agent=user_agent, + device_os=user.device_os or device_os, ) return Response({ "id": user.id, @@ -595,13 +666,7 @@ class AdminLoginView(CreateAPIView): return self.create(request, *args, **kwargs) def get_client_ip(self): - request = self.request - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip + return get_client_ip(self.request) def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) @@ -622,17 +687,25 @@ class AdminLoginView(CreateAPIView): if not user.can_access_admin_panel(): raise AuthenticationFailed("No admin found with that information.") + client_ip = get_client_ip(request) + user_agent = request.META.get('HTTP_USER_AGENT', '') user_timezone = serializer.validated_data.pop('timezone', None) user.last_login = timezone.now() user.is_active = True + if client_ip: + user.client_ip = client_ip + if user_agent: + user.user_agent = user_agent + user.device_os = 'web' user.save() token, created = Token.objects.get_or_create(user=user) # Log the history user.login_history.create( - ip=self.get_client_ip(), + ip=client_ip, timezone=user_timezone, + user_agent=user_agent, device_os='web_admin' ) diff --git a/utils/ip_helper.py b/utils/ip_helper.py index 5a1434a..cc11ec0 100644 --- a/utils/ip_helper.py +++ b/utils/ip_helper.py @@ -6,13 +6,27 @@ import os def get_client_ip(request): """Retrieves the real IP address from the request.""" + if not request: + return None + + # Cloudflare header + cf_ip = request.META.get('HTTP_CF_CONNECTING_IP') + if cf_ip: + return cf_ip.strip() + + # Nginx / Reverse proxy Real-IP + real_ip = request.META.get('HTTP_X_REAL_IP') + if real_ip: + return real_ip.strip() + + # Standard X-Forwarded-For header x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if x_forwarded_for: - # The header contains a list of IPs, the first one is the real client - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip + ip = x_forwarded_for.split(',')[0].strip() + if ip: + return ip + + return request.META.get('REMOTE_ADDR') def get_country_code(ip_address): """