diff --git a/config/__init__.py b/config/__init__.py index e69de29..742da6a 100644 --- a/config/__init__.py +++ b/config/__init__.py @@ -0,0 +1,3 @@ +from .celery import app as celery_app + +__all__ = ['celery_app'] \ No newline at end of file diff --git a/config/celery.py b/config/celery.py new file mode 100644 index 0000000..b5b2895 --- /dev/null +++ b/config/celery.py @@ -0,0 +1,10 @@ +import os +from celery import Celery + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.base') + +app = Celery('config', broker="redis://redis:6379", backend="redis://redis:6379") + + +app.config_from_object('django.conf:settings', namespace='CELERY') +app.autodiscover_tasks() \ No newline at end of file diff --git a/config/conf/__init__.py b/config/conf/__init__.py index e6555c5..d4d59ef 100644 --- a/config/conf/__init__.py +++ b/config/conf/__init__.py @@ -1,4 +1,5 @@ from .rest_framework import * from .jwt import * from .jazzmin import * -from .cache import * \ No newline at end of file +from .cache import * +from .celery import * \ No newline at end of file diff --git a/config/conf/celery.py b/config/conf/celery.py new file mode 100644 index 0000000..14662f9 --- /dev/null +++ b/config/conf/celery.py @@ -0,0 +1,8 @@ +from django.conf import settings + +CELERY_BROKER_URL = 'redis://redis:6379' +CELERY_RESULT_BACKEND = "redis://redis:6379" +CELERY_ACCEPT_CONTENT = ['json'] +CELERY_TASK_SERIALIZER = 'json' +CELERY_TIMEZONE = settings.TIME_ZONE +CELERY_ENABLED = True \ No newline at end of file diff --git a/config/settings/base.py b/config/settings/base.py index 215fc22..0d4fb6f 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -110,4 +110,11 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' AUTH_USER_MODEL = 'accounts.User' +EMAIL_HOST_USER = env.str('EMAIL_HOST_USER') +EMAIL_HOST_PASSWORD = env.str('EMAIL_HOST_PASSWORD') +EMAIL_BACKEND = env.str('EMAIL_BACKEND') +EMAIL_HOST = env.str('EMAIL_HOST') +EMAIL_PORT = env.str('EMAIL_PORT') +EMAIL_USE_TLS = env.str('EMAIL_USE_TLS') + from config.conf import * \ No newline at end of file diff --git a/core/apps/accounts/admin.py b/core/apps/accounts/admin.py index 8c38f3f..ce49a81 100644 --- a/core/apps/accounts/admin.py +++ b/core/apps/accounts/admin.py @@ -1,3 +1,26 @@ from django.contrib import admin +from django.contrib.auth.admin import UserAdmin as DjangoUserAdmin +from django.utils.translation import gettext_lazy as _ -# Register your models here. +from core.apps.accounts.models import User + + +@admin.register(User) +class UserAdmin(DjangoUserAdmin): + fieldsets = ( + (None, {"fields": ("email", "password")}), + (_("Personal info"), {"fields": ("full_name", "passport_id", "pnfl")}), + (_("Important dates"), {"fields": ("last_login", "date_joined")}), + ) + add_fieldsets = ( + ( + None, + { + "classes": ("wide",), + "fields": ("email", "password1", "password2"), + }, + ), + ) + list_display = ("email", "full_name", "is_staff") + search_fields = ("full_name", "email") + ordering = ("email",) \ No newline at end of file diff --git a/core/apps/accounts/cache.py b/core/apps/accounts/cache.py index 43ca6e3..a865e58 100644 --- a/core/apps/accounts/cache.py +++ b/core/apps/accounts/cache.py @@ -2,21 +2,21 @@ import redis r = redis.StrictRedis.from_url('redis://redis:6379') -def cache_user_credentials(email, password, passport_id, pnlf, time): +def cache_user_credentials(email, password, passport_id, pnfl, time): key = f"user_credentials:{email}" - r.hmset(key, { + r.hset(key, mapping={ "email": email, "password": password, "passport_id": passport_id, - "pnlf": pnlf, + "pnfl": pnfl, }) r.expire(key, time) + def get_user_credentials(email): key = f"user_credentials:{email}" - data = r.hgetall(key) if not data: return None @@ -25,6 +25,29 @@ def get_user_credentials(email): "email": data.get(b'email').decode() if data.get(b'email') else None, "password": data.get(b'password').decode() if data.get(b'password') else None, "passport_id": data.get(b'passport_id').decode() if data.get(b'passport_id') else None, - "pnlf": data.get(b'pnlf').decode() if data.get(b'pnlf') else None, + "pnfl": data.get(b'pnfl').decode() if data.get(b'pnfl') else None, } + + +def cache_user_confirmation_code(code, email, time): + key = f"user_confirmation:{email}_{code}" + + r.hset(key, mapping={ + 'email': email, + 'code': code + }) + + r.expire(key, time) + + +def get_user_confirmation_code(email, code): + key = f'user_confirmation:{email}_{code}' + + data = r.hgetall(key) + if not data: + return None + return { + "email": data.get(b'email').decode() if data.get(b'email') else None, + "code": data.get(b'code').decode() if data.get(b'code') else None + } diff --git a/core/apps/accounts/serializers.py b/core/apps/accounts/serializers.py index 6399308..81ed1bb 100644 --- a/core/apps/accounts/serializers.py +++ b/core/apps/accounts/serializers.py @@ -3,10 +3,10 @@ from django.db import transaction from rest_framework import serializers from core.apps.accounts.models import User -from core.apps.accounts.cache import get_user_credentials +from core.apps.accounts.cache import get_user_credentials, get_user_confirmation_code -class RegisterSerializer(serializers.ModelSerializer): +class RegisterSerializer(serializers.Serializer): passport_id = serializers.CharField() pnfl = serializers.CharField() email = serializers.EmailField() @@ -15,6 +15,38 @@ class RegisterSerializer(serializers.ModelSerializer): def validate_email(self, value): if User.objects.filter(email=value).exists(): raise serializers.ValidationError("User with this email already exists") - if get_user_credentials(value): + user_data = get_user_credentials(email=value) + if user_data: raise serializers.ValidationError("User with this email already exists") - return value \ No newline at end of file + return value + + +class ConfirmUserSerializer(serializers.Serializer): + email = serializers.EmailField() + code = serializers.IntegerField() + + def validate(self, data): + if User.objects.filter(email=data['email']).exists(): + raise serializers.ValidationError('User with this email already exists') + user_data = get_user_credentials(email=data.get('email')) + print(user_data) + if not user_data: + raise serializers.ValidationError("User with this email not found") + confirm_data = get_user_confirmation_code(data['email'], data['code']) + if not confirm_data: + raise serializers.ValidationError("Invalid confirmation code") + data['user_data'] = user_data + return data + + def create(self, validated_data): + with transaction.atomic(): + user_data = validated_data.get('user_data') + user = User.objects.create( + email=user_data.get('email'), + passport_id=user_data.get('passport_id'), + pnfl=user_data.get('pnfl'), + ) + user.set_password(user_data.get('password')) + user.save() + return user + \ No newline at end of file diff --git a/core/apps/accounts/tasks.py b/core/apps/accounts/tasks.py new file mode 100644 index 0000000..1a4c7de --- /dev/null +++ b/core/apps/accounts/tasks.py @@ -0,0 +1,15 @@ +from celery import shared_task + +from django.core.mail import send_mail +from django.conf import settings + + +@shared_task +def send_confirmation_code_to_email(email, code): + send_mail( + "Avto Cargo uchun tasdiqlash kod.", + f"Bu sizning tasdiqlash kodingiz: {code}", + settings.EMAIL_HOST_USER, + [email], + fail_silently=False, + ) diff --git a/core/apps/accounts/urls.py b/core/apps/accounts/urls.py index 604fd04..3939359 100644 --- a/core/apps/accounts/urls.py +++ b/core/apps/accounts/urls.py @@ -2,7 +2,10 @@ from django.urls import path, include from rest_framework_simplejwt.views import TokenObtainPairView +from core.apps.accounts import views urlpatterns = [ path('login/', TokenObtainPairView.as_view()), + path('register/', views.RegisterApiView.as_view()), + path('confirm_user/', views.ConfirmUserApiView.as_view()), ] \ No newline at end of file diff --git a/core/apps/accounts/views.py b/core/apps/accounts/views.py index 1b63aa6..30a44bd 100644 --- a/core/apps/accounts/views.py +++ b/core/apps/accounts/views.py @@ -1,9 +1,15 @@ +import random + from rest_framework import generics, views from rest_framework.response import Response +from rest_framework_simplejwt.tokens import RefreshToken + from core.apps.accounts import serializers from core.apps.accounts import models -from core.apps.accounts.cache import cache_user_credentials +from core.apps.accounts.cache import cache_user_credentials, cache_user_confirmation_code +from core.apps.accounts.tasks import send_confirmation_code_to_email + class RegisterApiView(generics.GenericAPIView): serializer_class = serializers.RegisterSerializer @@ -13,11 +19,36 @@ class RegisterApiView(generics.GenericAPIView): serializer = self.serializer_class(data=request.data) if serializer.is_valid(raise_exception=True): data = serializer.validated_data + email = data['email'] cache_user_credentials( - email=data['email'], password=data['password'], - passport_id=data['passport_id'], pnlf=data['pnlf'], time=60*5 + email=email, password=data['password'], + passport_id=data['passport_id'], pnfl=data['pnfl'], time=60*5 ) + code = ''.join([str(random.randint(0, 100)%10) for _ in range(5)]) + cache_user_confirmation_code( + email=email, code=code, time=60*5 + ) + send_confirmation_code_to_email.delay(email, code) return Response( {'success': True, 'message': "code sent"}, status=200 - ) \ No newline at end of file + ) + + +class ConfirmUserApiView(generics.GenericAPIView): + serializer_class = serializers.ConfirmUserSerializer + queryset = models.User.objects.all() + + def post(self, request): + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(raise_exception=True): + user = serializer.save() + token = RefreshToken.for_user(user) + return Response( + {'access': str(token.access_token), 'refresh': str(token)}, + status=200 + ) + return Response( + {'success': False, 'error_message': serializer.errors}, + status=400 + ) \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 0eb3c95..69bfeee 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -52,4 +52,17 @@ services: - avto_cargo image: redis ports: - - "6380:6379" \ No newline at end of file + - "6380:6379" + + celery: + networks: + - avto_cargo + build: + context: . + dockerfile: ./docker/Dockerfile.web + command: celery -A config worker -l info + depends_on: + - redis + - web + volumes: + - "./:/code" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a6c2bca..1e35bd6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,16 @@ +amqp==5.3.1 asgiref==3.9.1 +billiard==4.2.1 +celery==5.5.3 click==8.2.1 +click-didyoumean==0.3.1 +click-plugins==1.1.1.2 +click-repl==0.3.0 Django==5.2 django-cacheops==7.2 django-environ==0.12.0 django-jazzmin==3.0.1 +django-redis==6.0.0 djangorestframework==3.16.1 djangorestframework_simplejwt==5.5.1 drf-yasg==1.21.10 @@ -11,12 +18,19 @@ funcy==2.0 gunicorn==23.0.0 h11==0.16.0 inflection==0.5.1 +kombu==5.5.4 packaging==25.0 +prompt_toolkit==3.0.51 psycopg2-binary==2.9.10 PyJWT==2.10.1 +python-dateutil==2.9.0.post0 pytz==2025.2 PyYAML==6.0.2 redis==6.4.0 +six==1.17.0 sqlparse==0.5.3 +tzdata==2025.2 uritemplate==4.2.0 uvicorn==0.35.0 +vine==5.1.0 +wcwidth==0.2.13