diff --git a/config/asgi.py b/config/asgi.py index 2548f49..bd4fb70 100644 --- a/config/asgi.py +++ b/config/asgi.py @@ -1,7 +1,23 @@ import os - -from django.core.asgi import get_asgi_application +import django os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.base') -application = get_asgi_application() +django.setup() + +from django.core.asgi import get_asgi_application +from channels.routing import ProtocolTypeRouter, URLRouter +from channels.auth import AuthMiddlewareStack +from django.urls import re_path +from core.apps.authentication.websocket.consumers import UserActivationConsumer + +django_asgi_app = get_asgi_application() + +application = ProtocolTypeRouter({ + 'http': django_asgi_app, + 'websocket': AuthMiddlewareStack( + URLRouter([ + re_path(r'^ws/user_activation/(?P\d+)/$', UserActivationConsumer.as_asgi()), + ]) + ), +}) \ No newline at end of file diff --git a/config/conf/__init__.py b/config/conf/__init__.py index a0b05f0..206bfd6 100644 --- a/config/conf/__init__.py +++ b/config/conf/__init__.py @@ -1,4 +1,5 @@ from .rest_framework import * from .corsheaders import * from .simple_jwt import * -from .drf_yasg import * \ No newline at end of file +from .drf_yasg import * +from .redis import * \ No newline at end of file diff --git a/config/conf/redis.py b/config/conf/redis.py new file mode 100644 index 0000000..7b1328c --- /dev/null +++ b/config/conf/redis.py @@ -0,0 +1,10 @@ +from config.env import env + +CHANNEL_LAYERS = { + 'default': { + 'BACKEND': 'channels_redis.core.RedisChannelLayer', + 'CONFIG': { + "hosts": [(env.str('REDIS_HOST'), env.str('REDIS_PORT'))], + }, + }, +} \ No newline at end of file diff --git a/config/settings/base.py b/config/settings/base.py index b04699a..f0ee4b0 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -61,6 +61,7 @@ TEMPLATES = [ ] WSGI_APPLICATION = 'config.wsgi.application' +ASGI_APPLICATION = 'config.asgi.application' DATABASES = { diff --git a/core/apps/authentication/websocket/consumers.py b/core/apps/authentication/websocket/consumers.py new file mode 100644 index 0000000..f62449a --- /dev/null +++ b/core/apps/authentication/websocket/consumers.py @@ -0,0 +1,55 @@ +import json + +# channels +from channels.generic.websocket import AsyncJsonWebsocketConsumer +from channels.db import database_sync_to_async + +# accounts +from core.apps.accounts.models import User + + +class UserActivationConsumer(AsyncJsonWebsocketConsumer): + async def connect(self): + self.user_id = self.scope['url_route']['kwargs'].get('user_id') + if self.user_id: + await self.channel_layer.group_add( + f"user_{self.user_id}", + self.channel_name + ) + await self.accept() + print(f"User {self.user_id} ulandi") + else: + await self.close() + + async def disconnect(self, code): + if self.user_id: + await self.channel_layer.group_discard( + f"user_{self.user_id}", + self.channel_name, + ) + print(f'User {self.user_id} uzilib ketti') + + async def receive(self, text_data): + data = json.loads(text_data) + message_type = data.get('type') + + if message_type == 'ping': + await self.send(text_data=json.dumps( + { + 'type': 'pong', + 'message': "boglanish faol" + } + )) + + async def user_activated(self, event): + await self.send(text_data=json.dumps({ + 'type': 'user_activated', + 'message': 'user aktive qilindi', + 'timestamp': event.get('timestamp'), + 'status': "success", + "status_code": 200, + "status_message": "user_activated", + "token": event.get('token'), + })) + + \ No newline at end of file diff --git a/core/apps/dashboard/urls.py b/core/apps/dashboard/urls.py index b594f9b..e05902b 100644 --- a/core/apps/dashboard/urls.py +++ b/core/apps/dashboard/urls.py @@ -22,6 +22,7 @@ urlpatterns = [ path('create/', user_views.UserCreateApiView.as_view(), name='user-create-api'), path('/delete/', user_views.UserDeleteApiView.as_view(), name='user-delete-api'), path('/update/', user_views.UserUpdateApiView.as_view(), name='user-update-api'), + path('/activate/', user_views.UserActivateApiView.as_view(), name='user-activate-api'), ], )), # -------------- district -------------- diff --git a/core/apps/dashboard/views/user.py b/core/apps/dashboard/views/user.py index 93ebdd6..a15a500 100644 --- a/core/apps/dashboard/views/user.py +++ b/core/apps/dashboard/views/user.py @@ -1,3 +1,6 @@ +from asgiref.sync import async_to_sync +import datetime + # django from django.shortcuts import get_object_or_404 from django.db.models import Q @@ -10,6 +13,12 @@ from rest_framework.permissions import IsAdminUser from drf_yasg import openapi from drf_yasg.utils import swagger_auto_schema +# rest framework simple jwt +from rest_framework_simplejwt.tokens import RefreshToken + +# channels +from channels.layers import get_channel_layer + # dashboard from core.apps.dashboard.serializers import user as serializers # accounts @@ -349,4 +358,89 @@ class UserDeleteApiView(views.APIView, ResponseMixin): ) except Exception as e: return self.error_response(data=str(e), message="xatolik") - \ No newline at end of file + + + +class UserActivateApiView(views.APIView, ResponseMixin): + permission_classes = [IsAdminUser] + + @swagger_auto_schema( + responses={ + 200: openapi.Response( + schema=None, + description="Success", + examples={ + "application/json": { + "status_code": 204, + "status": "success", + "message": "user tasdiqlandi", + "data": { + "id": 0, + "first_name": "string", + "last_name": "string", + "region": { + "id": 0, + "name": "string" + }, + "is_active": True, + "created_at": "string" + } + } + } + ), + 404: openapi.Response( + schema=None, + description="Not Found", + examples={ + "application/json": { + "status_code": 404, + "success": "failure", + "message": "User not found", + "data": {}, + } + } + ), + 500: openapi.Response( + schema=None, + description="Server Error", + examples={ + "application/json": { + "status_code": 500, + "status": "error", + "message": "xatolik", + "data": "string" + } + } + ), + } + ) + def post(self, request, id): + try: + channel_layer = get_channel_layer() + user = User.objects.filter(id=id).first() + if not user: + return self.failure_response(data={}, message='User not found', status_code=404) + user.is_active = True + user.save() + token = RefreshToken.for_user(user) + + async_to_sync(channel_layer.group_send)( + f"user_{user.id}", + { + 'type': 'user_activated', + 'message': 'user aktive qilindi', + 'status': "success", + "status_code": 200, + "status_message": "user_activated", + "token": str(token.access_token), + } + ) + return self.success_response( + data=serializers.UserListSerializer(user).data, + message="user tasdiqlandi", + ) + except Exception as e: + return self.error_response( + data=str(e), + message='xatolik' + ) diff --git a/docker-compose.yaml b/docker-compose.yaml index 0a5554e..5d1888f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -34,6 +34,7 @@ services: - '.:/code' depends_on: - db + - redis restart: always db: @@ -46,4 +47,9 @@ services: POSTGRES_DB: ${POSTGRES_DB} volumes: - pg_data:/var/lib/postgresql/data - restart: always \ No newline at end of file + restart: always + + redis: + image: redis + networks: + - meridyn_pharma \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 2b5ce1b..9334418 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,38 +1,63 @@ asgiref==3.11.0 +attrs==25.4.0 +autobahn==25.11.1 +Automat==25.4.16 brotli==1.2.0 +cbor2==5.7.1 certifi==2025.11.12 cffi==2.0.0 +channels==4.3.2 +channels_redis==4.3.0 charset-normalizer==3.4.4 click==8.3.1 +constantly==23.10.4 +cryptography==46.0.3 cssselect2==0.8.0 +daphne==4.2.1 Django==5.2 django-cors-headers==4.9.0 django-environ==0.12.0 +django-redis==6.0.0 djangorestframework==3.16.1 djangorestframework_simplejwt==5.5.1 drf-yasg==1.21.11 fonttools==4.60.1 gunicorn==23.0.0 h11==0.16.0 +hyperlink==21.0.0 idna==3.11 +Incremental==24.11.0 inflection==0.5.1 +msgpack==1.1.2 packaging==25.0 pillow==12.0.0 psycopg2-binary==2.9.11 +py-ubjson==0.16.1 +pyasn1==0.6.1 +pyasn1_modules==0.4.2 pycparser==2.23 pydyf==0.11.0 PyJWT==2.10.1 +pyOpenSSL==25.3.0 pyphen==0.17.2 pytz==2025.2 PyYAML==6.0.3 +redis==7.1.0 reportlab==4.4.5 requests==2.32.5 +service-identity==24.2.0 sqlparse==0.5.3 tinycss2==1.5.1 tinyhtml5==2.0.0 +Twisted==25.5.0 +txaio==25.9.2 +typing_extensions==4.15.0 +ujson==5.11.0 uritemplate==4.2.0 urllib3==2.5.0 uvicorn==0.38.0 weasyprint==66.0 webencodings==0.5.1 +zope.interface==8.1.1 zopfli==0.4.0 +websockets