Browse Source

fix(account): persist client IP, device OS, and user agent on auth and add serializer fallbacks

master
Mohsen Taba 3 hours ago
parent
commit
55bb015e32
  1. 34
      apps/account/serializers/user.py
  2. 37
      apps/account/views/auth.py
  3. 109
      apps/account/views/user.py
  4. 24
      utils/ip_helper.py

34
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 = [

37
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:

109
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'
)

24
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):
"""

Loading…
Cancel
Save