From 8f28332b5823a015b78841c0899402dad4f5e1b1 Mon Sep 17 00:00:00 2001 From: behruz-dev Date: Tue, 22 Jul 2025 15:59:18 +0500 Subject: [PATCH] change register apis and save user phone in redis --- .../apps/accounts/admins/verification_code.py | 2 +- core/apps/accounts/cache/user.py | 32 +++++++++ core/apps/accounts/migrations/0001_initial.py | 20 +++++- .../migrations/0002_verificationcode.py | 33 ---------- .../0003_verificationcode_expiration_time.py | 18 ----- core/apps/accounts/models/user.py | 2 +- .../apps/accounts/models/verification_code.py | 4 +- core/apps/accounts/serializers/auth.py | 47 +++++--------- core/apps/accounts/tasks/user.py | 16 +++-- core/apps/accounts/urls.py | 2 +- core/apps/accounts/views/auth.py | 65 ++++++++++++------- 11 files changed, 124 insertions(+), 117 deletions(-) create mode 100644 core/apps/accounts/cache/user.py delete mode 100644 core/apps/accounts/migrations/0002_verificationcode.py delete mode 100644 core/apps/accounts/migrations/0003_verificationcode_expiration_time.py diff --git a/core/apps/accounts/admins/verification_code.py b/core/apps/accounts/admins/verification_code.py index 2cb42c8..1cd6a09 100644 --- a/core/apps/accounts/admins/verification_code.py +++ b/core/apps/accounts/admins/verification_code.py @@ -4,5 +4,5 @@ from core.apps.accounts.models.verification_code import VerificationCode @admin.register(VerificationCode) class VerificationCodeAdmin(admin.ModelAdmin): - list_display = ['id', 'user', 'code', 'is_expired', 'is_verify'] + list_display = ['id', 'phone', 'code', 'is_expired', 'is_verify'] \ No newline at end of file diff --git a/core/apps/accounts/cache/user.py b/core/apps/accounts/cache/user.py new file mode 100644 index 0000000..9c0e83e --- /dev/null +++ b/core/apps/accounts/cache/user.py @@ -0,0 +1,32 @@ +import redis + +from django.contrib.auth.hashers import make_password + +from config.env import env + +r = redis.StrictRedis.from_url(env.str('REDIS_URL')) + + +def cache_user_credentials(phone_number, password, time): + hashed_password = make_password(password) + key = f"user_credentials:{phone_number}" + + r.hmset(key, { + "phone": phone_number, + "password": hashed_password + }) + + r.expire(key, time) + + +def get_user_creadentials(phone_number): + key = f"user_credentials:{phone_number}" + data = r.hgetall(key) + + if not data: + return None + + return { + "phone": data.get(b"phone").decode() if data.get(b"phone") else None, + "password": data.get(b"password").decode() if data.get(b"password") else None + } \ No newline at end of file diff --git a/core/apps/accounts/migrations/0001_initial.py b/core/apps/accounts/migrations/0001_initial.py index 8756d88..5844f89 100644 --- a/core/apps/accounts/migrations/0001_initial.py +++ b/core/apps/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2 on 2025-07-14 15:16 +# Generated by Django 5.2 on 2025-07-22 15:26 import core.apps.accounts.managers.user import django.core.validators @@ -16,6 +16,24 @@ class Migration(migrations.Migration): ] operations = [ + migrations.CreateModel( + name='VerificationCode', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('code', models.PositiveIntegerField()), + ('phone', models.CharField(max_length=13, validators=[django.core.validators.RegexValidator(message="Telefon raqam formatda bo'lishi kerak: +998XXXXXXXXX", regex='^\\+998\\d{9}$')])), + ('is_expired', models.BooleanField(default=False)), + ('is_verify', models.BooleanField(default=False)), + ('expiration_time', models.TimeField(blank=True, null=True)), + ], + options={ + 'verbose_name': 'Verification Code', + 'verbose_name_plural': 'Verification Codes', + 'db_table': 'verification_codes', + }, + ), migrations.CreateModel( name='User', fields=[ diff --git a/core/apps/accounts/migrations/0002_verificationcode.py b/core/apps/accounts/migrations/0002_verificationcode.py deleted file mode 100644 index abc22a9..0000000 --- a/core/apps/accounts/migrations/0002_verificationcode.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2 on 2025-07-14 17:08 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='VerificationCode', - fields=[ - ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('code', models.PositiveIntegerField()), - ('is_expired', models.BooleanField(default=False)), - ('is_verify', models.BooleanField(default=False)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='verification_codes', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'Verification Code', - 'verbose_name_plural': 'Verification Codes', - 'db_table': 'verification_codes', - }, - ), - ] diff --git a/core/apps/accounts/migrations/0003_verificationcode_expiration_time.py b/core/apps/accounts/migrations/0003_verificationcode_expiration_time.py deleted file mode 100644 index 247eb1d..0000000 --- a/core/apps/accounts/migrations/0003_verificationcode_expiration_time.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2 on 2025-07-14 17:17 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0002_verificationcode'), - ] - - operations = [ - migrations.AddField( - model_name='verificationcode', - name='expiration_time', - field=models.TimeField(blank=True, null=True), - ), - ] diff --git a/core/apps/accounts/models/user.py b/core/apps/accounts/models/user.py index e3fb32f..e58ae37 100644 --- a/core/apps/accounts/models/user.py +++ b/core/apps/accounts/models/user.py @@ -30,7 +30,7 @@ class User(BaseModel, AbstractUser): expiration_time = timezone.now() + datetime.timedelta(minutes=2) VerificationCode.objects.create( code=code, - user=self, + user=self.phone, expiration_time=expiration_time, ) return code diff --git a/core/apps/accounts/models/verification_code.py b/core/apps/accounts/models/verification_code.py index 00b532d..2793883 100644 --- a/core/apps/accounts/models/verification_code.py +++ b/core/apps/accounts/models/verification_code.py @@ -1,10 +1,12 @@ from django.db import models from core.apps.shared.models.base import BaseModel +from core.apps.accounts.validators.user import phone_regex + class VerificationCode(BaseModel): code = models.PositiveIntegerField() - user = models.ForeignKey('User', on_delete=models.CASCADE, related_name='verification_codes') + phone = models.CharField(max_length=13, validators=[phone_regex]) is_expired = models.BooleanField(default=False) is_verify = models.BooleanField(default=False) expiration_time = models.TimeField(null=True, blank=True) diff --git a/core/apps/accounts/serializers/auth.py b/core/apps/accounts/serializers/auth.py index bb29ac9..95d2064 100644 --- a/core/apps/accounts/serializers/auth.py +++ b/core/apps/accounts/serializers/auth.py @@ -1,10 +1,12 @@ from django.db import transaction from django.contrib.auth import get_user_model +from django.utils import timezone from rest_framework import serializers from core.apps.accounts.tasks.user import create_and_send_sms_code from core.apps.accounts.enums.user import ROLE_CHOICES +from core.apps.accounts.models.verification_code import VerificationCode User = get_user_model() @@ -28,50 +30,31 @@ class RegisterSerializer(serializers.Serializer): phone = serializers.CharField() password = serializers.CharField() - def validate(self, data): - if User.objects.filter(phone=data.get('phone')).exists(): - raise serializers.ValidationError({'detail': "User with this phone number already exists"}) - return data - - def create(self, validated_data): - with transaction.atomic(): - new_user = User.objects.create_user( - phone=validated_data.pop('phone'), - is_active=False - ) - new_user.set_password(validated_data.pop('password')) - new_user.save() - create_and_send_sms_code.delay(new_user.id) - return new_user - + def validate_phone(self, value): + if User.objects.filter(phone=value).exists(): + raise serializers.ValidationError("User exists") + return value + class ConfirmUserSerializer(serializers.Serializer): phone = serializers.CharField() code = serializers.IntegerField() def validate(self, data): - try: - user = User.objects.get(phone=data.get('phone')) - except User.DoesNotExist: - raise serializers.ValidationError({"detail": "User not found"}) - if user.is_active: - raise serializers.ValidationError({"detail": "User already activated"}) - data['user'] = user + phone = data['phone'] + code = data['code'] + confirmation = VerificationCode.objects.filter(code=code, phone=phone).first() + if confirmation.is_verify: + raise serializers.ValidationError("Code is verified") + if confirmation.is_expired or confirmation.expiration_time < timezone.now().time(): + raise serializers.ValidationError("Code is expired") + data['confirmation'] = confirmation return data class ChoiseRoleSerializer(serializers.Serializer): - phone = serializers.CharField() role = serializers.ChoiceField(choices=ROLE_CHOICES) - def validate(self, data): - try: - user = User.objects.get(phone=data.get("phone"), is_active=True) - except User.DoesNotExist: - raise serializers.ValidationError({"detail": "user not found"}) - data['user'] = user - return data - class CompliteUserProfileSerializer(serializers.Serializer): first_name = serializers.CharField() diff --git a/core/apps/accounts/tasks/user.py b/core/apps/accounts/tasks/user.py index 67c9516..74c1bd1 100644 --- a/core/apps/accounts/tasks/user.py +++ b/core/apps/accounts/tasks/user.py @@ -1,3 +1,8 @@ +import random +from datetime import timedelta + +from django.utils import timezone + from celery import shared_task from core.apps.accounts.models.verification_code import VerificationCode @@ -6,8 +11,11 @@ from core.services.sms import send_sms_eskiz from core.services.sms_via_bot import send_sms_code @shared_task -def create_and_send_sms_code(user): - user = User.objects.get(id=user) - code = user.generate_code() +def create_and_send_sms_code(phone): + verification = VerificationCode.objects.create( + code=''.join([str(random.randint(1, 100) % 10) for _ in range(4)]), + phone=phone, + expiration_time=timezone.now() + timedelta(minutes=2) + ) # send_sms_eskiz(user.phone, code) - send_sms_code(code, 'auth', user.phone) + send_sms_code(verification.code, 'auth', verification.phone) diff --git a/core/apps/accounts/urls.py b/core/apps/accounts/urls.py index ca93b7d..ed50c11 100644 --- a/core/apps/accounts/urls.py +++ b/core/apps/accounts/urls.py @@ -9,7 +9,7 @@ urlpatterns = [ path('register/', RegisterApiView.as_view(), name='register'), path('confirm_user/', ConfirUserApiView.as_view(), name='confirm-user'), path('choise_user_role/', ChoiceUserRoleApiView.as_view(), name='choise-user-role'), - path('complite_user_profile//', CompliteUserProfileApiView.as_view(), name='complite-user-profile'), + path('complite_user_profile/', CompliteUserProfileApiView.as_view(), name='complite-user-profile'), ] )) ] \ No newline at end of file diff --git a/core/apps/accounts/views/auth.py b/core/apps/accounts/views/auth.py index 521d68f..b75731d 100644 --- a/core/apps/accounts/views/auth.py +++ b/core/apps/accounts/views/auth.py @@ -8,6 +8,8 @@ from rest_framework_simplejwt.tokens import RefreshToken from core.apps.accounts.serializers import auth as auth_serializer from core.apps.accounts.models.verification_code import VerificationCode +from core.apps.accounts.cache.user import cache_user_credentials, get_user_creadentials +from core.apps.accounts.tasks import user as user_tasks User = get_user_model() @@ -26,47 +28,62 @@ class LoginApiView(generics.GenericAPIView): return Response(serializer.errors, status=status.HTTP_404_NOT_FOUND) -class RegisterApiView(generics.CreateAPIView): +class RegisterApiView(generics.GenericAPIView): serializer_class = auth_serializer.RegisterSerializer - queryset = User.objects.all() + queryset = None permission_classes = [] + def post(self, request): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + data = serializer.validated_data + cache_user_credentials(data['phone'], data['password'], 300) + user_tasks.create_and_send_sms_code.delay(data['phone']) + return Response( + {'success': True, "message": "code send"}, + status=status.HTTP_200_OK + ) + return Response( + {'success': True, "message": serializer.errors}, + status=status.HTTP_400_BAD_REQUEST + ) + class ConfirUserApiView(generics.GenericAPIView): serializer_class = auth_serializer.ConfirmUserSerializer - queryset = User.objects.all() + queryset = None permission_classes = [] def post(self, request): serializer = self.serializer_class(data=request.data) if serializer.is_valid(): - user = serializer.validated_data.get('user') - code = serializer.validated_data.get('code') - code = VerificationCode.objects.filter(user=user, code=code).first() - if code: - if code.is_expired or code.expiration_time < timezone.now().time(): - return Response({"success": True, "message": "code is expired"}, status=status.HTTP_400_BAD_REQUEST) - if code.is_verify: - return Response({"success": True, "message": "code is verified"}, status=status.HTTP_400_BAD_REQUEST) - user.is_active = True - user.save() - code.is_verify = True - code.is_expired = True - code.save() - return Response({"success": True, "message": "user activated"}, status=status.HTTP_202_ACCEPTED) - return Response({"success": False, "message": "code is wrong"}, status=status.HTTP_400_BAD_REQUEST) + phone = serializer.validated_data.get('phone') + confirmation = serializer.validated_data.get('confirmation') + data = get_user_creadentials(phone) + if not data: + return Response( + {'success': True, "message": 'not found'}, + status=status.HTTP_404_NOT_FOUND + ) + user = User.objects.create_user(phone=data['phone'], password=data['password']) + confirmation.is_verify = True + confirmation.save() + token = RefreshToken.for_user(user) + return Response( + {"access": str(token.access_token), "refresh": str(token)}, + status=status.HTTP_202_ACCEPTED + ) return Response({"success": False, "message": serializer.errors}, status=status.HTTP_400_BAD_REQUEST) class ChoiceUserRoleApiView(generics.GenericAPIView): serializer_class = auth_serializer.ChoiseRoleSerializer queryset = User.objects.all() - permission_classes = [] def post(self, request): serializer = self.serializer_class(data=request.data) if serializer.is_valid(): - user = serializer.validated_data.get('user') + user = request.user role = serializer.validated_data.get('role') user.role = role user.save() @@ -77,15 +94,13 @@ class ChoiceUserRoleApiView(generics.GenericAPIView): class CompliteUserProfileApiView(generics.GenericAPIView): serializer_class = auth_serializer.CompliteUserProfileSerializer queryset = User.objects.all() - permission_classes = [] - def put(self, request, phone): - user = User.objects.filter(phone=phone, is_active=True).first() + def put(self, request): + user = request.user if user: serializer = self.serializer_class(data=request.data, instance=user) if serializer.is_valid(): serializer.save() - token = RefreshToken.for_user(user) - return Response({'access_token': str(token.access_token), "refresh_token": str(token), "role": user.role}, status=status.HTTP_200_OK) + return Response({'success': True, "message": "Ok"}, status=status.HTTP_200_OK) return Response({'success': False, 'message': serializer.errors}, status=status.HTTP_400_BAD_REQUEST) return Response({'success': False, "message": "User not found"}, status=status.HTTP_404_NOT_FOUND) \ No newline at end of file