diff --git a/apps/account/migrations/0008_auto_20250316_1247.py b/apps/account/migrations/0008_auto_20250316_1247.py new file mode 100644 index 0000000..fe07d37 --- /dev/null +++ b/apps/account/migrations/0008_auto_20250316_1247.py @@ -0,0 +1,57 @@ +# Generated by Django 3.2.7 on 2025-03-16 12:47 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0007_notification'), + ] + + operations = [ + migrations.AddField( + model_name='notification', + name='service', + field=models.CharField(choices=[('imam-javad', 'Imam Javad'), ('doboodi', 'Doboodi')], default='imam-javad', max_length=20, verbose_name='service'), + ), + migrations.AddField( + model_name='user', + name='device_os', + field=models.CharField(choices=[('android', 'android'), ('apple', 'apple')], max_length=16, null=True), + ), + migrations.AddField( + model_name='user', + name='username', + field=models.CharField(blank=True, max_length=150, null=True, unique=True), + ), + migrations.AlterField( + model_name='user', + name='email', + field=models.EmailField(blank=True, help_text="Enter the user's email address.", max_length=254, null=True, unique=True, verbose_name='Email Address'), + ), + migrations.AlterField( + model_name='user', + name='fullname', + field=models.CharField(blank=True, help_text='Enter the full name of the user.', max_length=255, null=True, verbose_name='Full Name'), + ), + migrations.AlterUniqueTogether( + name='user', + unique_together={('email', 'device_id')}, + ), + migrations.CreateModel( + name='LocationHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('lat', models.FloatField(blank=True, null=True, verbose_name='lat')), + ('lon', models.FloatField(blank=True, null=True, verbose_name='lon')), + ('country', models.CharField(blank=True, max_length=255, null=True, verbose_name='country')), + ('city', models.CharField(blank=True, max_length=255, null=True, verbose_name='city')), + ('ip', models.CharField(max_length=255, null=True)), + ('at_time', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='location_history', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/apps/account/migrations/0009_auto_20250316_1319.py b/apps/account/migrations/0009_auto_20250316_1319.py new file mode 100644 index 0000000..05d18a3 --- /dev/null +++ b/apps/account/migrations/0009_auto_20250316_1319.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.7 on 2025-03-16 13:19 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0008_auto_20250316_1247'), + ] + + operations = [ + migrations.CreateModel( + name='LoginHistory', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('lat', models.FloatField(blank=True, null=True, verbose_name='lat')), + ('lon', models.FloatField(blank=True, null=True, verbose_name='lon')), + ('country', models.CharField(blank=True, max_length=255, null=True, verbose_name='country')), + ('city', models.CharField(blank=True, max_length=255, null=True, verbose_name='city')), + ('ip', models.CharField(max_length=255, null=True)), + ('at_time', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='login_history', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.DeleteModel( + name='LocationHistory', + ), + ] diff --git a/apps/account/migrations/0010_loginhistory_timezone.py b/apps/account/migrations/0010_loginhistory_timezone.py new file mode 100644 index 0000000..a18c319 --- /dev/null +++ b/apps/account/migrations/0010_loginhistory_timezone.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.7 on 2025-03-16 13:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0009_auto_20250316_1319'), + ] + + operations = [ + migrations.AddField( + model_name='loginhistory', + name='timezone', + field=models.CharField(blank=True, max_length=100, null=True), + ), + ] diff --git a/apps/account/models/notification.py b/apps/account/models/notification.py index ea8c95e..8286fed 100644 --- a/apps/account/models/notification.py +++ b/apps/account/models/notification.py @@ -2,17 +2,24 @@ from django.db import models from django.utils.translation import gettext as _ - - class Notification(models.Model): + class ServiceChoices(models.TextChoices): + IMAM_JAVAD = 'imam-javad', 'Imam Javad' + DOBOODI = 'doboodi', 'Doboodi' + title = models.CharField(max_length=255, verbose_name=_('title')) message = models.TextField(max_length=512, verbose_name=_('message')) user = models.ForeignKey("account.User", on_delete=models.CASCADE, verbose_name=_('user'), related_name='notifications') is_read = models.BooleanField(default=False, verbose_name=_('is read')) + service = models.CharField( + max_length=20, + choices=ServiceChoices.choices, + default=ServiceChoices.IMAM_JAVAD, + verbose_name=_('service') + ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'), null=True) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'), null=True) def __str__(self): return self.title - - + diff --git a/apps/account/models/user.py b/apps/account/models/user.py index c50434d..1c58ece 100644 --- a/apps/account/models/user.py +++ b/apps/account/models/user.py @@ -11,6 +11,10 @@ from apps.account.manager import UserManager class User(AbstractUser): + class DeviceOs(models.TextChoices): + android = 'android', 'android' + apple = 'apple', 'apple' + class UserType(models.TextChoices): PROFESSOR = 'professor', 'Professor' CLIENT = 'client', 'Client' @@ -21,33 +25,29 @@ class User(AbstractUser): class GenderChoices(models.TextChoices): MALE = 'male', 'Male' FEMALE = 'female', 'Female' - - email = models.EmailField(unique=True, verbose_name="Email Address", help_text="Enter the user's email address.") - fullname = models.CharField(max_length=255, verbose_name="Full Name", help_text="Enter the full name of the user.") + + last_name = None + first_name = None + username = models.CharField(unique=True, null=True, blank=True, max_length=150) + email = models.EmailField(unique=True, verbose_name="Email Address", help_text="Enter the user's email address.", null=True, blank=True) + fullname = models.CharField(max_length=255, verbose_name="Full Name", help_text="Enter the full name of the user.", null=True, blank=True) birthdate = models.DateField(verbose_name=_('birthdate'), null=True, blank=True) avatar = models.ImageField(null=True, blank=True, upload_to='users/avatars/%Y/%m/') phone_number = PhoneNumberField(unique=True, validators=[validate_possible_number], null=True, blank=True, verbose_name=_('phone')) language = LanguageField(null=True) - username = None - last_name = None - first_name = None - gender = models.CharField( - max_length=20, choices=GenderChoices.choices, null=True, blank=True, verbose_name=_('Gender'), help_text="Select the user's gender." - ) - user_type = models.CharField( - max_length=20, - choices=UserType.choices, - default=UserType.CLIENT, - verbose_name="User Type", - help_text="Type of the user." - ) + + gender = models.CharField(max_length=20, choices=GenderChoices.choices, null=True, blank=True, verbose_name=_('Gender'), help_text="Select the user's gender.") + user_type = models.CharField(max_length=20, choices=UserType.choices, default=UserType.CLIENT, verbose_name="User Type", help_text="Type of the user.") + date_joined = models.DateTimeField(auto_now_add=True, verbose_name="Date Joined", help_text="The date and time the user registered.") + city = models.CharField(verbose_name=_('City'), max_length=255, null=True, blank=True) country = models.CharField(max_length=255, verbose_name=_('country'), null=True, blank=True) device_id = models.CharField(verbose_name=_('device id'), max_length=255, null=True, blank=True) + device_os = models.CharField(choices=DeviceOs.choices, null=True, max_length=16) + fcm = models.CharField(max_length=512, null=True, blank=True) - date_joined = models.DateTimeField(auto_now_add=True, verbose_name="Date Joined", help_text="The date and time the user registered.") is_staff = models.BooleanField(default=False) is_active = models.BooleanField(default=True, verbose_name="Active", help_text="Designates whether this user should be treated as active. Unselect this instead of deleting accounts.") deleted_at = models.DateTimeField(null=True, blank=True) @@ -57,31 +57,36 @@ class User(AbstractUser): EMAIL_FIELD = "email" - USERNAME_FIELD = "email" - REQUIRED_FIELDS = ["fullname", ] + USERNAME_FIELD = "username" + REQUIRED_FIELDS = [] + + + def __str__(self): + username = self.email or self.fullname or self.device_id + return f"{username}-({self.user_type})" def soft_delete(self): self.deleted_at = timezone.now() self.is_active = False self.fullname = f'{self.fullname}:deleted' - number = str(random.randint(1000000000, 9999999999)) # ایجاد یک عدد رندوم 10 رقمی + number = str(random.randint(1000000000, 9999999999)) self.phone_number = f'{self.phone_number}:deleted{number}' self.email = f'{self.email}:deleted{number}' if self.email else None self.save() - - # def clean(self): - # super().clean() - # if self.email == "": - # # fix db uniqueness error bcz of django charfield null to empty string conversion - # self.email = None - def __str__(self): - return f"{self.email} - {self.get_full_name()} - ({self.user_type})" - + def save(self, *args, **kwargs): + self.username = self.email + if User.objects.filter(username=self.email).count(): + self.username = f'{self.email}:{self.id}' + return super().save(*args, **kwargs) def get_full_name(self): return self.fullname + @property + def is_guest(self): + return self.email is None + @property def user_type_based_on_groups(self): @@ -97,3 +102,17 @@ class User(AbstractUser): ordering = ("-id",) verbose_name = "All Users" verbose_name_plural = "All Users" + unique_together = ( + 'email', 'device_id' + ) + + +class LoginHistory(models.Model): + user = models.ForeignKey("account.User", on_delete=models.CASCADE, related_name='login_history') + lat = models.FloatField(verbose_name=_('lat'), null=True, blank=True) + lon = models.FloatField(verbose_name=_('lon'), null=True, blank=True) + country = models.CharField(max_length=255, verbose_name=_('country'), null=True, blank=True) + city = models.CharField(max_length=255, verbose_name=_('city'), null=True, blank=True) + ip = models.CharField(max_length=255, null=True) + timezone = models.CharField(max_length=100, null=True, blank=True) + at_time = models.DateTimeField(auto_now_add=True) diff --git a/apps/account/serializers/notification.py b/apps/account/serializers/notification.py index 4a8ecb0..d9823e9 100644 --- a/apps/account/serializers/notification.py +++ b/apps/account/serializers/notification.py @@ -9,10 +9,11 @@ from apps.account.models import User class NotificationSerializer(serializers.ModelSerializer): user_type = serializers.ChoiceField(choices=[('user', 'User'), ('merchant', 'Merchant')], default='user') + service = serializers.ChoiceField(choices=Notification.ServiceChoices.choices, default=Notification.ServiceChoices.IMAM_JAVAD) class Meta: model = Notification - fields = ['id', 'title', 'message', 'is_read', 'user_type', 'created_at', 'updated_at'] + fields = ['id', 'title', 'message', 'is_read', 'user_type', 'service', 'created_at', 'updated_at'] @@ -21,4 +22,5 @@ class NotificationSendSerializer(serializers.Serializer): body = serializers.CharField() data = serializers.DictField(required=False) account_id = serializers.CharField(required=True) - user_type = serializers.CharField(required=True) \ No newline at end of file + user_type = serializers.CharField(required=True) + service = serializers.ChoiceField(choices=Notification.ServiceChoices.choices, default=Notification.ServiceChoices.IMAM_JAVAD) \ No newline at end of file diff --git a/apps/account/serializers/user.py b/apps/account/serializers/user.py index ebc2066..e565768 100644 --- a/apps/account/serializers/user.py +++ b/apps/account/serializers/user.py @@ -18,10 +18,11 @@ class UserProfileSerializer(serializers.ModelSerializer): required=False, help_text="Select the user's gender." ) + fcm = serializers.CharField(required=False, help_text="Firebase Cloud Messaging token.") class Meta: model = User - fields = ['id', 'fullname', 'avatar', 'email', 'phone_number', 'password', 'info', 'skill', 'city', 'country', 'birthdate', 'gender'] - read_only_fields = ['email', 'info', 'skill'] + fields = ['id', 'device_id', 'fcm', 'fullname', 'avatar', 'email', 'phone_number', 'password', 'info', 'skill', 'city', 'country', 'birthdate', 'gender'] + read_only_fields = ['email', 'info', 'skill', 'device_id'] # def validate_email(self, value): # if User.objects.filter(email=value).exists(): @@ -44,7 +45,7 @@ class UserProfileSerializer(serializers.ModelSerializer): class UserRegisterSerializer(serializers.ModelSerializer): password_confirmation = serializers.CharField(write_only=True) fcm = serializers.CharField(required=False) - device_id = serializers.CharField(required=False) + device_id = serializers.CharField(required=True) email = serializers.EmailField() class Meta: @@ -55,6 +56,7 @@ class UserRegisterSerializer(serializers.ModelSerializer): 'email': {'required': True,}, 'password': {'required': True,}, 'password_confirmation': {'required': True,}, + 'device_id': {'required': True,}, } def validate_email(self, value): @@ -75,8 +77,6 @@ class UserRegisterSerializer(serializers.ModelSerializer): # If there are any errors, raise ValidationError data.pop('password_confirmation', None) - data.pop('fcm', None) - data.pop('device_id', None) return data @@ -102,11 +102,12 @@ class UserLoginSerializer(serializers.ModelSerializer): password = serializers.CharField(style={'input_type': 'password'}, trim_whitespace=False) fcm = serializers.CharField(required=False) device_id = serializers.CharField(required=False) + timezone = serializers.CharField(required=False, allow_null=True, allow_blank=True) class Meta: model = User - fields = ['id', 'phone_number', 'password', 'fullname', 'avatar', 'email', 'token', 'fcm', 'device_id'] + fields = ['id', 'phone_number', 'password', 'fullname', 'avatar', 'email', 'token', 'fcm', 'device_id', 'timezone'] def get_token(self, obj): token, created = Token.objects.get_or_create(user=obj) @@ -142,8 +143,8 @@ class UserResetPasswordSerializer(serializers.ModelSerializer): 'password': {'required': True,}, 'password_confirmation': {'required': True,}, } - - + + def validate(self, data): password = data.get('password') password_confirmation = data.get('password_confirmation') @@ -158,4 +159,23 @@ class UserResetPasswordSerializer(serializers.ModelSerializer): data.pop('password_confirmation', None) return data - + +class UserGuestSerializer(serializers.ModelSerializer): + lat = serializers.CharField(max_length=255, allow_null=True, allow_blank=True, required=False) + lon = serializers.CharField(max_length=255, allow_null=True, allow_blank=True, required=False) + fcm = serializers.CharField(required=False) + device_id = serializers.CharField(required=False) + device_os = serializers.ChoiceField(choices=User.DeviceOs.choices, required=False) + timezone = serializers.CharField(required=False, allow_null=True, allow_blank=True) + + class Meta: + model = User + fields = ['device_id', 'fcm', 'device_os', 'lat', 'lon', 'timezone'] + + def validate(self, data): + # Make sure at least device_id is provided + if not data.get('device_id'): + raise serializers.ValidationError({"device_id": "Device ID is required for guest users."}) + + return data + diff --git a/apps/account/urls.py b/apps/account/urls.py index 390c183..46ff361 100644 --- a/apps/account/urls.py +++ b/apps/account/urls.py @@ -13,6 +13,7 @@ urlpatterns = [ path('register/', views.UserRegisterView.as_view(), name='user-register'), path('verify/', views.UserVerifyView.as_view(), name='user-verify'), path('login/', views.UserLoginView.as_view(), name='user-login'), + path('guest/', views.UserGuestView.as_view(), name='user-guest'), # path('notif/', views.NotificationListView.as_view(), name='user-notif'), diff --git a/apps/account/views/notification.py b/apps/account/views/notification.py index 67258cd..d370773 100644 --- a/apps/account/views/notification.py +++ b/apps/account/views/notification.py @@ -17,7 +17,17 @@ class NotificationListView(generics.ListAPIView): @swagger_auto_schema( operation_description="Retrieve a list of notifications for the authenticated user or merchant account.", - tags=['Notifications'] + tags=['Notifications'], + manual_parameters=[ + openapi.Parameter( + 'service', + openapi.IN_QUERY, + description="Filter notifications by service (imam-javad or doboodi)", + type=openapi.TYPE_STRING, + enum=['imam-javad', 'doboodi'], + required=False + ) + ] ) def get(self, request, *args, **kwargs): """ @@ -27,36 +37,68 @@ class NotificationListView(generics.ListAPIView): - **Method**: GET - **URL**: /api/notifications/ - - **Response**: Includes details of notifications such as title, message, is read status, creation date, and update date. + - **Query Parameters**: + - `service`: Optional. Filter notifications by service ('imam-javad' or 'doboodi') + - **Response**: Includes details of notifications such as title, message, is read status, service, creation date, and update date. - **Headers**: `Authorization: Bearer ` for authentication. """ return super().get(request, *args, **kwargs) def get_queryset(self): - user = self.request.user - return Notification.objects.filter(user=user).order_by('-created_at') + user = self.request.user + queryset = Notification.objects.filter(user=user) + + # Filter by service if provided in query params + service = self.request.query_params.get('service', None) + if service: + queryset = queryset.filter(service=service) + + return queryset.order_by('-created_at') class NotificationReadAllView(generics.GenericAPIView): - permission_classes = [IsAuthenticated,] - queryset = Notification.objects.all() + permission_classes = [IsAuthenticated,] + queryset = Notification.objects.all() @swagger_auto_schema( operation_description="Mark all notifications as read for the authenticated user or merchant account.", tags=['Notifications'], + manual_parameters=[ + openapi.Parameter( + 'service', + openapi.IN_QUERY, + description="Filter notifications to mark as read by service (imam-javad or doboodi)", + type=openapi.TYPE_STRING, + enum=['imam-javad', 'doboodi'], + required=False + ) + ], responses={ 200: "All notifications marked as read", - 403: "Forbidden", } ) def get(self, request, *args, **kwargs): user = request.user + service = request.query_params.get('service', None) + # Get base queryset for user's notifications notifications = Notification.objects.filter(user=user) + # Apply service filtering based on query parameter + if service == 'doboodi': + # If service is doboodi, only mark doboodi notifications as read + notifications = notifications.filter(service=Notification.ServiceChoices.DOBOODI) + status_message = 'all doboodi notifications marked as read' + else: + # Default: mark all imam-javad notifications as read (exclude doboodi) + notifications = notifications.exclude(service=Notification.ServiceChoices.DOBOODI) + status_message = 'all imam-javad notifications marked as read' + + # Update the filtered notifications notifications.update(is_read=True) - return Response({'status': 'all notifications marked as read'}, status=status.HTTP_200_OK) + + return Response({'status': status_message}, status=status.HTTP_200_OK) diff --git a/apps/account/views/user.py b/apps/account/views/user.py index ac54b61..10a5698 100644 --- a/apps/account/views/user.py +++ b/apps/account/views/user.py @@ -5,6 +5,7 @@ from rest_framework.generics import CreateAPIView, RetrieveUpdateAPIView, Generi from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status +from django.db.models import Q from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.authtoken.models import Token from rest_framework.exceptions import AuthenticationFailed @@ -22,7 +23,7 @@ from rest_framework.exceptions import ValidationError from utils.exceptions import InvaliedCodeVrify, ExpiredCodeException, ServiceUnavailableException from apps.account.models import User -from apps.account.serializers import UserRegisterSerializer, UserProfileSerializer, UserVerifySerializer, UserLoginSerializer, UserRecoverPasswordSerializer, UserResetPasswordSerializer +from apps.account.serializers import UserRegisterSerializer, UserProfileSerializer, UserVerifySerializer, UserLoginSerializer, UserRecoverPasswordSerializer, UserResetPasswordSerializer, UserGuestSerializer from utils.redis import RedisManager from utils.exceptions import AppAPIException from utils import send_email, is_valid_email @@ -34,8 +35,73 @@ logger = logging.getLogger(__name__) +class UserGuestView(CreateAPIView): + permission_classes = [AllowAny] + serializer_class = UserGuestSerializer + + @swagger_auto_schema( + operation_description="Create a guest user account with device information", + request_body=UserGuestSerializer, + ) + def post(self, request, *args, **kwargs): + logger.info(f'GuestAuthView--> {request.data}') + return super().post(request, *args, **kwargs) + + @staticmethod + def generate_login_token(user): + token, created = Token.objects.update_or_create(user=user) + return token.key + + 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 + + + def create(self, request, *args, **kwargs): + serializer = self.get_serializer(data=request.data) + serializer.is_valid(raise_exception=True) + user = self.perform_create(serializer) + return Response({ + 'token': self.generate_login_token(user), + }, status=200) + + + def perform_create(self, serializer): + device_id = serializer.validated_data.get('device_id') + device_os = serializer.validated_data.get('device_os') + fcm = serializer.validated_data.get('fcm') + lat, lon = serializer.validated_data.get('lat'), serializer.validated_data.get('lon') + user_timezone = serializer.validated_data.pop('timezone', None) + + serializer_data = dict(serializer.validated_data) + + obj = User.objects.select_for_update().filter(Q(device_id=device_id)).first() + if not obj: + obj, created = User.objects.select_for_update().get_or_create( + device_id=device_id, + defaults=serializer_data + ) + if created: + logger.info(f'Guest-(created)->: {obj.device_id}') + + obj.last_login = timezone.now() + obj.save() + login_history_obj = obj.login_history.create( + lat=lat, + lon=lon, + ip=self.get_client_ip(), + timezone=user_timezone, + ) + return obj + + + - class UserRegisterView(CreateAPIView): permission_classes = [AllowAny] serializer_class = UserRegisterSerializer @@ -52,7 +118,7 @@ class UserRegisterView(CreateAPIView): code = RedisManager.generate_otp_code() logger.info(f"phone= {data['email']}") - print(f' send {code}/{data["email"]}') + print(f'send {code}/{data["email"]}') phone_number = RedisManager().add_to_redis(code, **data) send_email([data['email']], code) @@ -99,8 +165,8 @@ class UserVerifyView(CreateAPIView): user = self.perform_create( email=serializer.data['email'],**verify_data ) - Token.objects.filter(user=user).delete() - token = Token.objects.create(user=user) + # Token.objects.filter(user=user).delete() + token = Token.objects.get_or_create(user=user) return Response(data={ 'token': str(token), 'user_id': user.id, @@ -112,7 +178,6 @@ class UserVerifyView(CreateAPIView): def valied_code(self, current_code, save_code): if (current_code and save_code) and ( current_code != save_code): - # raise InvaliedCodeVrify() raise ValidationError({"code": "code notfound"}) return current_code @@ -128,7 +193,10 @@ class UserVerifyView(CreateAPIView): user.set_password(kwargs['password']) user.save() else: - user = User.objects.create(**kwargs) + user = User.objects.flter(device_id=kwargs['device_id']).first() + if not user: + user = User.objects.create(**kwargs) + user.set_password(kwargs['password']) user.last_login = timezone.now() user.is_active = True @@ -148,7 +216,16 @@ class UserLoginView(CreateAPIView): def post(self, request, *args, **kwargs): return super().post(request, *args, **kwargs) - + @staticmethod + 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 + def create(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) @@ -156,13 +233,18 @@ class UserLoginView(CreateAPIView): user = authenticate(request, username=request.data['email'], password=data['password']) if not user: raise ValidationError({"email": "Unable to log in with provided credentials."}) + user_timezone = serializer.validated_data.pop('timezone', None) user.last_login = timezone.now() user.is_active = True user.save token, created = Token.objects.get_or_create(user=user) serializer_data = serializer.data serializer_data['token'] = token.key - + + login_history_obj = obj.login_history.create( + ip=self.get_client_ip(), + timezone=user_timezone, + ) return Response({ "id": user.id, "fullname": user.fullname, @@ -173,6 +255,7 @@ class UserLoginView(CreateAPIView): }, status=status.HTTP_201_CREATED) + class UserProfileView(RetrieveAPIView): serializer_class = UserProfileSerializer permission_classes = [IsAuthenticated, IsActiveUser] @@ -256,12 +339,12 @@ class UserDeleteView(APIView): if user.email == "admin@gmail.com": raise AppAPIException({"message": "Unable to log in with provided credentials."}, status_code=status.HTTP_204_NO_CONTENT) - user.soft_delete() + user.soft_delete() if t := Token.objects.filter(user=user).first(): t.delete() return Response({"detail": "Your account has been deleted."}, status=status.HTTP_204_NO_CONTENT) - + except Exception: # پیام خطای ثابت برای سایر خطاهای غیرمنتظره return Response({"detail": "User does not exist."}, status=status.HTTP_404_NOT_FOUND)