10 changed files with 343 additions and 63 deletions
-
57apps/account/migrations/0008_auto_20250316_1247.py
-
31apps/account/migrations/0009_auto_20250316_1319.py
-
18apps/account/migrations/0010_loginhistory_timezone.py
-
15apps/account/models/notification.py
-
77apps/account/models/user.py
-
6apps/account/serializers/notification.py
-
38apps/account/serializers/user.py
-
1apps/account/urls.py
-
58apps/account/views/notification.py
-
105apps/account/views/user.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)), |
||||
|
], |
||||
|
), |
||||
|
] |
||||
@ -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', |
||||
|
), |
||||
|
] |
||||
@ -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), |
||||
|
), |
||||
|
] |
||||
@ -5,6 +5,7 @@ from rest_framework.generics import CreateAPIView, RetrieveUpdateAPIView, Generi |
|||||
from rest_framework.views import APIView |
from rest_framework.views import APIView |
||||
from rest_framework.response import Response |
from rest_framework.response import Response |
||||
from rest_framework import status |
from rest_framework import status |
||||
|
from django.db.models import Q |
||||
from rest_framework.permissions import AllowAny, IsAuthenticated |
from rest_framework.permissions import AllowAny, IsAuthenticated |
||||
from rest_framework.authtoken.models import Token |
from rest_framework.authtoken.models import Token |
||||
from rest_framework.exceptions import AuthenticationFailed |
from rest_framework.exceptions import AuthenticationFailed |
||||
@ -22,7 +23,7 @@ from rest_framework.exceptions import ValidationError |
|||||
|
|
||||
from utils.exceptions import InvaliedCodeVrify, ExpiredCodeException, ServiceUnavailableException |
from utils.exceptions import InvaliedCodeVrify, ExpiredCodeException, ServiceUnavailableException |
||||
from apps.account.models import User |
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.redis import RedisManager |
||||
from utils.exceptions import AppAPIException |
from utils.exceptions import AppAPIException |
||||
from utils import send_email, is_valid_email |
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): |
class UserRegisterView(CreateAPIView): |
||||
permission_classes = [AllowAny] |
permission_classes = [AllowAny] |
||||
serializer_class = UserRegisterSerializer |
serializer_class = UserRegisterSerializer |
||||
@ -52,7 +118,7 @@ class UserRegisterView(CreateAPIView): |
|||||
|
|
||||
code = RedisManager.generate_otp_code() |
code = RedisManager.generate_otp_code() |
||||
logger.info(f"phone= {data['email']}") |
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) |
phone_number = RedisManager().add_to_redis(code, **data) |
||||
|
|
||||
send_email([data['email']], code) |
send_email([data['email']], code) |
||||
@ -99,8 +165,8 @@ class UserVerifyView(CreateAPIView): |
|||||
user = self.perform_create( |
user = self.perform_create( |
||||
email=serializer.data['email'],**verify_data |
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={ |
return Response(data={ |
||||
'token': str(token), |
'token': str(token), |
||||
'user_id': user.id, |
'user_id': user.id, |
||||
@ -112,7 +178,6 @@ class UserVerifyView(CreateAPIView): |
|||||
|
|
||||
def valied_code(self, current_code, save_code): |
def valied_code(self, current_code, save_code): |
||||
if (current_code and save_code) and ( current_code != save_code): |
if (current_code and save_code) and ( current_code != save_code): |
||||
# raise InvaliedCodeVrify() |
|
||||
raise ValidationError({"code": "code notfound"}) |
raise ValidationError({"code": "code notfound"}) |
||||
|
|
||||
return current_code |
return current_code |
||||
@ -128,7 +193,10 @@ class UserVerifyView(CreateAPIView): |
|||||
user.set_password(kwargs['password']) |
user.set_password(kwargs['password']) |
||||
user.save() |
user.save() |
||||
else: |
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.set_password(kwargs['password']) |
||||
user.last_login = timezone.now() |
user.last_login = timezone.now() |
||||
user.is_active = True |
user.is_active = True |
||||
@ -148,7 +216,16 @@ class UserLoginView(CreateAPIView): |
|||||
def post(self, request, *args, **kwargs): |
def post(self, request, *args, **kwargs): |
||||
return super().post(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): |
def create(self, request, *args, **kwargs): |
||||
serializer = self.get_serializer(data=request.data) |
serializer = self.get_serializer(data=request.data) |
||||
serializer.is_valid(raise_exception=True) |
serializer.is_valid(raise_exception=True) |
||||
@ -156,13 +233,18 @@ class UserLoginView(CreateAPIView): |
|||||
user = authenticate(request, username=request.data['email'], password=data['password']) |
user = authenticate(request, username=request.data['email'], password=data['password']) |
||||
if not user: |
if not user: |
||||
raise ValidationError({"email": "Unable to log in with provided credentials."}) |
raise ValidationError({"email": "Unable to log in with provided credentials."}) |
||||
|
user_timezone = serializer.validated_data.pop('timezone', None) |
||||
user.last_login = timezone.now() |
user.last_login = timezone.now() |
||||
user.is_active = True |
user.is_active = True |
||||
user.save |
user.save |
||||
token, created = Token.objects.get_or_create(user=user) |
token, created = Token.objects.get_or_create(user=user) |
||||
serializer_data = serializer.data |
serializer_data = serializer.data |
||||
serializer_data['token'] = token.key |
serializer_data['token'] = token.key |
||||
|
|
||||
|
|
||||
|
login_history_obj = obj.login_history.create( |
||||
|
ip=self.get_client_ip(), |
||||
|
timezone=user_timezone, |
||||
|
) |
||||
return Response({ |
return Response({ |
||||
"id": user.id, |
"id": user.id, |
||||
"fullname": user.fullname, |
"fullname": user.fullname, |
||||
@ -173,6 +255,7 @@ class UserLoginView(CreateAPIView): |
|||||
}, status=status.HTTP_201_CREATED) |
}, status=status.HTTP_201_CREATED) |
||||
|
|
||||
|
|
||||
|
|
||||
class UserProfileView(RetrieveAPIView): |
class UserProfileView(RetrieveAPIView): |
||||
serializer_class = UserProfileSerializer |
serializer_class = UserProfileSerializer |
||||
permission_classes = [IsAuthenticated, IsActiveUser] |
permission_classes = [IsAuthenticated, IsActiveUser] |
||||
@ -256,12 +339,12 @@ class UserDeleteView(APIView): |
|||||
if user.email == "[email protected]": |
if user.email == "[email protected]": |
||||
raise AppAPIException({"message": "Unable to log in with provided credentials."}, status_code=status.HTTP_204_NO_CONTENT) |
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(): |
if t := Token.objects.filter(user=user).first(): |
||||
t.delete() |
t.delete() |
||||
|
|
||||
return Response({"detail": "Your account has been deleted."}, status=status.HTTP_204_NO_CONTENT) |
return Response({"detail": "Your account has been deleted."}, status=status.HTTP_204_NO_CONTENT) |
||||
|
|
||||
|
|
||||
except Exception: |
except Exception: |
||||
# پیام خطای ثابت برای سایر خطاهای غیرمنتظره |
# پیام خطای ثابت برای سایر خطاهای غیرمنتظره |
||||
return Response({"detail": "User does not exist."}, status=status.HTTP_404_NOT_FOUND) |
return Response({"detail": "User does not exist."}, status=status.HTTP_404_NOT_FOUND) |
||||
|
|||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue