From f16c573b07695c9fc2de2e772ab1b3d1ceecb8e3 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Fri, 16 Aug 2024 18:48:59 +0300 Subject: [PATCH 01/22] updated user login function to use secrets instead if random and hashed the otp --- .../user_app/user_session_views.py | 86 +++++++++---------- 1 file changed, 40 insertions(+), 46 deletions(-) diff --git a/Backend/user_service/user_service/user_app/user_session_views.py b/Backend/user_service/user_service/user_app/user_session_views.py index e61b90b..48949fc 100644 --- a/Backend/user_service/user_service/user_app/user_session_views.py +++ b/Backend/user_service/user_service/user_app/user_session_views.py @@ -1,5 +1,6 @@ import json from django.contrib.auth import authenticate +from django.contrib.auth.hashers import check_password, make_password from django.shortcuts import get_object_or_404 from rest_framework import status, viewsets from rest_framework.permissions import AllowAny, IsAuthenticated @@ -7,14 +8,13 @@ from rest_framework_simplejwt.authentication import JWTAuthentication from .models import UserProfileModel as User from .serializers import UserSerializer -from .models import UserProfileModel from datetime import timedelta from django.core.mail import send_mail from django.utils.timezone import now from django.conf import settings import logging import requests -import random +import secrets import os from dotenv import load_dotenv @@ -23,68 +23,62 @@ logger = logging.getLogger(__name__) -def generate_password(): - return random.randint(100000,999999) +def generate_secret(): + return secrets.randbelow(900000) + 100000 + class UserLoginView(viewsets.ViewSet): permission_classes = [AllowAny] + def send_email(self, user): + send_mail( + 'Verification Code', + f'Your verification code is: {user.otp}', + settings.EMAIL_HOST_USER, + [user.email], + fail_silently=False, + ) def authenticate_user(self, request): username = request.data.get("username") password = request.data.get("password") - status_code = status.HTTP_200_OK - response = {} - response_message = {} if username and password: user = authenticate(username=username, password=password) if user is not None: - return True - return False + return user + return None def login(self, request): - username = request.data.get("username") - password = request.data.get("password") - status_code = status.HTTP_200_OK response = {} response_message = {} - if username and password: - user = authenticate(username=username, password=password) - if user is not None: - if user.is_active: - serializer = UserSerializer(user) - # send post request to token-service - if user.otp_status: - user.otp = generate_password() - user.otp_expiry_time = now() + timedelta(minutes=1) - user.save() - send_mail( - 'Verification Code', - f'Your verification code is: {user.otp}', - settings.EMAIL_HOST_USER, - [user.email], - fail_silently=False, - ) - response_message = {"detail":"Verification password sent to your email"} - status_code = status.HTTP_200_OK - else: - data = {"id": serializer.data["id"], "username": serializer.data["username"]} - response = requests.post(f"{TOEKNSERVICE}/auth/token/gen-tokens/", data=data) - if response.status_code == 201: - response_message = response.json() - logger.info('user_data = %s', response_message) - if "error" in response_message: - status_code = response_message.get("status_code") - else: - status_code = status.HTTP_200_OK + user = self.authenticate_user(request) + if user is not None: + if user.is_active: + serializer = UserSerializer(user) + # send post request to token-service + if user.otp_status: + otp = generate_secret() + user.otp = make_password(otp) + user.otp_expiry_time = now() + timedelta(minutes=1) + user.save() + self.send_email(user) + response_message = {"detail":"Verification password sent to your email"} + status_code = status.HTTP_200_OK else: - response_message = {"detail": "User is Inactive"} - status_code = status.HTTP_401_UNAUTHORIZED + data = {"id": serializer.data["id"], "username": serializer.data["username"]} + response = requests.post(f"{TOEKNSERVICE}/auth/token/gen-tokens/", data=data) + if response.status_code == 201: + response_message = response.json() + logger.info('user_data = %s', response_message) + if "error" in response_message: + status_code = response_message.get("status_code") + else: + status_code = status.HTTP_200_OK else: - response_message = {"detail": "Invalid username or password"} - status_code = status.HTTP_400_BAD_REQUEST + response_message = {"detail": "User is Inactive"} + status_code = status.HTTP_401_UNAUTHORIZED else: - response_message = {"detail": "Username or password is missing"} + response_message = {"detail": "Invalid username or password"} status_code = status.HTTP_400_BAD_REQUEST return Response(response_message, status=status_code) From f67d5455d56c16d106770a51235958f8ac3ea358 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Fri, 16 Aug 2024 21:20:56 +0300 Subject: [PATCH 02/22] added check_secret 'X-SERVICE-SECRET' to token-service, commented logger.info in most of files and removed rabbitmq from docker-compose.yml --- .../token_service/token_app/views.py | 154 ++++++++++-------- .../user_service/user_app/GameRoomConsumer.py | 4 +- .../user_app/user_session_views.py | 10 +- .../user_service/user_app/views.py | 11 +- docker-compose.yml | 18 -- 5 files changed, 99 insertions(+), 98 deletions(-) diff --git a/Backend/token_service/token_service/token_app/views.py b/Backend/token_service/token_service/token_app/views.py index fd0b482..c9101f2 100644 --- a/Backend/token_service/token_service/token_app/views.py +++ b/Backend/token_service/token_service/token_app/views.py @@ -19,6 +19,17 @@ logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) +def check_secret(request, response_message, status_code): + secret_key = request.headers.get('X-SERVICE-SECRET') + if secret_key is None or secret_key != SECRET: + response_message = {"error": "Unauthorized request"} + # logger.info('Invalid secret keys = %s', response_message) + status_code = status.HTTP_401_UNAUTHORIZED + # logger.info("response_message = %s", response_message) + # logger.info("status_code = %s", status_code) + # logger.info("secret_key = %s", secret_key) + return response_message, status_code + class CustomTokenObtainPairView(TokenObtainPairView): """ CustomTokenObtainPairView class to handle token request. @@ -43,15 +54,11 @@ def post(self, request, *args, **kwargs) -> Response: Returns: Response: The response object containing the token details. """ - secret_key = request.headers.get('X-SERVICE-SECRET') + response_message = {} - if secret_key is None: - response_message = {"error": "Unauthorized request"} - logger.info('Invalid secret keys = %s', response_message) - status_code = status.HTTP_401_UNAUTHORIZED - else: - response_message = {} - status_code = status.HTTP_201_CREATED + status_code = status.HTTP_201_CREATED + response_message, status_code = check_secret(request, response_message, status_code) + if "error" not in response_message and status_code == status.HTTP_201_CREATED: id = request.data.get("id") username = request.data.get("username") if not username or not id: @@ -60,8 +67,8 @@ def post(self, request, *args, **kwargs) -> Response: else: try: user, create = UserTokens.objects.get_or_create(id=id, username=username) - logger.info('user= %s', user.username) - logger.info('create= %s', create) + # logger.info('user= %s', user.username) + # logger.info('create= %s', create) if create: refresh = RefreshToken.for_user(user) access_token = str(refresh.access_token) @@ -97,7 +104,8 @@ def post(self, request, *args, **kwargs) -> Response: response_message = {"error": str(err)} status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - logger.info('response_message = %s', response_message) + # logger.info('response_message = %s', response_message) + # logger.info('status_code = %s', status_code) return Response(response_message, status=status_code) class CustomTokenRefreshView(TokenRefreshView): @@ -152,68 +160,74 @@ def validate_token(self, access_token) -> bool: return False def validate_token_for_user(self, request, *args, **kwargs): - - try: - status_code = status.HTTP_200_OK - response_message = {} - access = request.data.get("access") - id = request.data.get("id") - if not access or not id: - response_message = {"error": "Access token and id are required"} - status_code = status.HTTP_400_BAD_REQUEST - result = self.validate_token(access) - if result: - logger.info("result= %s", result) - user = UserTokens.objects.filter(id = id, token_data__access = access).first() - - logger.info("user.username= %s", user.username) - logger.info("user.token_data['access']= %s", user.token_data["access"]) + response_message = {} + status_code = status.HTTP_201_CREATED + response_message, status_code = check_secret(request, response_message, status_code) + if "error" not in response_message and status_code == status.HTTP_201_CREATED: + try: + status_code = status.HTTP_200_OK + response_message = {} + access = request.data.get("access") + id = request.data.get("id") + if not access or not id: + response_message = {"error": "Access token and id are required"} + status_code = status.HTTP_400_BAD_REQUEST + result = self.validate_token(access) if result: - response_message = {"access_token": "Valid token"} - else: - response_message = {"error": "token mismatch"} - status_code = status.HTTP_401_UNAUTHORIZED - except jwt.ExpiredSignatureError: - response_message = {"error": "token is expired"} - status_code = status.HTTP_401_UNAUTHORIZED - except jwt.InvalidTokenError: - response_message = {"error": "Invalid token"} - status_code = status.HTTP_401_UNAUTHORIZED - except Http404: - response_message = {"error": "User has not logged in yet!!"} - status_code = status.HTTP_401_UNAUTHORIZED - except Exception as err: - response_message = {"error": str(err)} - status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - logger.info("response_message= %s", response_message) + # logger.info("result= %s", result) + user = UserTokens.objects.filter(id = id, token_data__access = access).first() + + # logger.info("user.username= %s", user.username) + # logger.info("user.token_data['access']= %s", user.token_data["access"]) + if result: + response_message = {"access_token": "Valid token"} + else: + response_message = {"error": "token mismatch"} + status_code = status.HTTP_401_UNAUTHORIZED + except jwt.ExpiredSignatureError: + response_message = {"error": "token is expired"} + status_code = status.HTTP_401_UNAUTHORIZED + except jwt.InvalidTokenError: + response_message = {"error": "Invalid token"} + status_code = status.HTTP_401_UNAUTHORIZED + except Http404: + response_message = {"error": "User has not logged in yet!!"} + status_code = status.HTTP_401_UNAUTHORIZED + except Exception as err: + response_message = {"error": str(err)} + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + # logger.info("response_message= %s", response_message) return Response(response_message, status=status_code) class InvalidateToken(viewsets.ViewSet): def invalidate_token_for_user(self, request, *args, **kwargs) -> Response: - try: - status_code = status.HTTP_200_OK - access = request.data.get("access") - id = request.data.get("id") - if not access or not id: - response_message = {"error": "Access token and id are required"} - status_code = status.HTTP_400_BAD_REQUEST - else: - check_token = ValidateToken() - if check_token.validate_token(access): - user = get_object_or_404(UserTokens, id=id) - if user is not None: - user.delete() - response_message = {"detail":"User logged out"} - except jwt.ExpiredSignatureError: - response_message = {"error": "Access token is expired"} - status_code = status.HTTP_401_UNAUTHORIZED - except jwt.InvalidTokenError: - response_message = {"error": "Invalid access token"} - status_code = status.HTTP_401_UNAUTHORIZED - except Http404: - response_message = {"error": "User has not logged in yet"} - status_code = status.HTTP_401_UNAUTHORIZED - except Exception as err: - response_message = {"error": str(err)} - status_code = status.HTTP_500_INTERNAL_SERVER_ERROR + response_message = {} + status_code = status.HTTP_201_CREATED + response_message, status_code = check_secret(request, response_message, status_code) + if "error" not in response_message and status_code == status.HTTP_201_CREATED: + try: + access = request.data.get("access") + id = request.data.get("id") + if not access or not id: + response_message = {"error": "Access token and id are required"} + status_code = status.HTTP_400_BAD_REQUEST + else: + check_token = ValidateToken() + if check_token.validate_token(access): + user = get_object_or_404(UserTokens, id=id) + if user is not None: + user.delete() + response_message = {"detail":"User logged out"} + except jwt.ExpiredSignatureError: + response_message = {"error": "Access token is expired"} + status_code = status.HTTP_401_UNAUTHORIZED + except jwt.InvalidTokenError: + response_message = {"error": "Invalid access token"} + status_code = status.HTTP_401_UNAUTHORIZED + except Http404: + response_message = {"error": "User has not logged in yet"} + status_code = status.HTTP_401_UNAUTHORIZED + except Exception as err: + response_message = {"error": str(err)} + status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return Response(response_message, status=status_code) diff --git a/Backend/user_service/user_service/user_app/GameRoomConsumer.py b/Backend/user_service/user_service/user_app/GameRoomConsumer.py index 55df4bf..350172f 100644 --- a/Backend/user_service/user_service/user_app/GameRoomConsumer.py +++ b/Backend/user_service/user_service/user_app/GameRoomConsumer.py @@ -139,7 +139,7 @@ def create_and_send_game_history_record(self): from .models import GameRoom from .serializers import GameRoomSerializer from django.utils.timezone import now - + request = {} response = {} gameroom_obj = GameRoom.objects.get(room_name=self.room_name) @@ -166,4 +166,4 @@ def check_room_players(self): if obj is not None: if obj.player1 is not None and obj.player2 is not None: return True - return False \ No newline at end of file + return False diff --git a/Backend/user_service/user_service/user_app/user_session_views.py b/Backend/user_service/user_service/user_app/user_session_views.py index 989eee8..f33c696 100644 --- a/Backend/user_service/user_service/user_app/user_session_views.py +++ b/Backend/user_service/user_service/user_app/user_session_views.py @@ -22,7 +22,7 @@ TOEKNSERVICE = os.environ.get('TOKEN_SERVICE') headers = { - "X-SERVICE-SECRET": settings.SECRET_KEY # Replace with your actual secret key + "X-SERVICE-SECRET": settings.SECRET_KEY } logger = logging.getLogger(__name__) @@ -76,7 +76,7 @@ def login(self, request): response = requests.post(f"{TOEKNSERVICE}/auth/token/gen-tokens/", data=data, headers=headers) if response.status_code == 201: response_message = response.json() - logger.info('user_data = %s', response.json()) + # logger.info('user_data = %s', response.json()) if "error" in response_message: status_code = response_message.get("status_code") response_message = response.json() @@ -106,12 +106,12 @@ def verify_otp(self, request): if user.otp == otp: if user.otp_expiry_time > now(): data = {"id": user.id, "username": username} - response = requests.post(f'{TOEKNSERVICE}/auth/token/gen-tokens/', data=data) + response = requests.post(f'{TOEKNSERVICE}/auth/token/gen-tokens/', data=data, headers=headers) user.otp = None user.otp_expiry_time = None if response.status_code == 201: response_message = response.json() - logger.info('user_data = %s', response_message) + # logger.info('user_data = %s', response_message) if "error" in response_message: status_code = response_message.get("status_code") else: @@ -144,7 +144,7 @@ def logout(self, request, pk=None): status_code =status.HTTP_400_BAD_REQUEST access_token = bearer.split(' ')[1] data = {"id":pk, "access": access_token} - response_data = requests.post(f"{TOEKNSERVICE}/auth/token/invalidate-tokens/", data=data) + response_data = requests.post(f"{TOEKNSERVICE}/auth/token/invalidate-tokens/", data=data, headers=headers) if response_data.status_code == 200: response_message = response_data.json() if "error" in response_message: diff --git a/Backend/user_service/user_service/user_app/views.py b/Backend/user_service/user_service/user_app/views.py index e6c216f..fb368d0 100644 --- a/Backend/user_service/user_service/user_app/views.py +++ b/Backend/user_service/user_service/user_app/views.py @@ -12,13 +12,18 @@ from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.decorators import parser_classes from .serializers import UserSerializer, FriendSerializer +from django.conf import settings import requests -import os from dotenv import load_dotenv +import os load_dotenv() TOEKNSERVICE = os.environ.get('TOKEN_SERVICE') +headers = { + "X-SERVICE-SECRET": settings.SECRET_KEY # Replace with your actual secret key +} + def extract_token(request): bearer = request.headers.get("Authorization") if not bearer or not bearer.startswith('Bearer '): @@ -32,7 +37,7 @@ def validate_token(request) -> None: access_token = extract_token(request) if access_token: data = {"id": request.user.id, "access": access_token} - response = requests.post(f"{TOEKNSERVICE}/auth/token/validate-token/", data=data) + response = requests.post(f"{TOEKNSERVICE}/auth/token/validate-token/", data=data, headers=headers) response_data = response.json() if "error" in response_data: raise ValidationError(detail=response_data, code=response_data.get("status_code")) @@ -155,7 +160,7 @@ def destroy_user(self, request, pk=None) -> Response: return Response(status=status.HTTP_401_UNAUTHORIZED) access_token = extract_token(request) request_data = {"id":pk, "access": access_token} - response_data = requests.post(f"{TOEKNSERVICE}/auth/token/invalidate-tokens/", data=request_data) + response_data = requests.post(f"{TOEKNSERVICE}/auth/token/invalidate-tokens/", data=request_data, headers=headers) data.delete() return Response(status=status.HTTP_204_NO_CONTENT) except Exception as err: diff --git a/docker-compose.yml b/docker-compose.yml index c51c427..efe02d3 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,20 +24,6 @@ services: networks: - transcendence_network - rabbitmq: - container_name: rabbitmq - image: rabbitmq - env_file: - - .env - build: - context: . - dockerfile: Backend/rabbitmq/Dockerfile - ports: - - 5672:5672 - - 15672:15672 - networks: - - transcendence_network - user-service: container_name: user-service image: user-service @@ -54,7 +40,6 @@ services: - www_data:/app/www/avatars # - database:/var/lib/postgresql/ depends_on: - - rabbitmq - postgresql token-service: @@ -70,7 +55,6 @@ services: ports: - 8000:8000 depends_on: - - rabbitmq - postgresql game-history: @@ -84,7 +68,6 @@ services: networks: - transcendence_network depends_on: - - rabbitmq - postgresql game-server: @@ -100,7 +83,6 @@ services: networks: - transcendence_network depends_on: - - rabbitmq - postgresql postgresql: From a44eba365454fb562794b22395fb5b30912f4f9d Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Fri, 16 Aug 2024 23:30:29 +0300 Subject: [PATCH 03/22] changed otp to char field in userprofilemodel, added three functions to send otp to email and verify email and added them to urls.py --- .../token_service/token_app/views.py | 4 -- .../user_service/user_app/models.py | 2 +- .../user_service/user_app/urls.py | 4 +- .../user_app/user_session_views.py | 1 + .../user_service/user_app/views.py | 67 ++++++++++++++++++- 5 files changed, 70 insertions(+), 8 deletions(-) diff --git a/Backend/token_service/token_service/token_app/views.py b/Backend/token_service/token_service/token_app/views.py index c9101f2..2c06798 100644 --- a/Backend/token_service/token_service/token_app/views.py +++ b/Backend/token_service/token_service/token_app/views.py @@ -23,11 +23,7 @@ def check_secret(request, response_message, status_code): secret_key = request.headers.get('X-SERVICE-SECRET') if secret_key is None or secret_key != SECRET: response_message = {"error": "Unauthorized request"} - # logger.info('Invalid secret keys = %s', response_message) status_code = status.HTTP_401_UNAUTHORIZED - # logger.info("response_message = %s", response_message) - # logger.info("status_code = %s", status_code) - # logger.info("secret_key = %s", secret_key) return response_message, status_code class CustomTokenObtainPairView(TokenObtainPairView): diff --git a/Backend/user_service/user_service/user_app/models.py b/Backend/user_service/user_service/user_app/models.py index bc6324e..09144bb 100644 --- a/Backend/user_service/user_service/user_app/models.py +++ b/Backend/user_service/user_service/user_app/models.py @@ -26,7 +26,7 @@ class UserProfileModel(AbstractUser): friends = models.ManyToManyField("self", blank=True, symmetrical=True) online_status = models.BooleanField(default=False) otp_status = models.BooleanField(default=False, blank=True, null=True) - otp = models.IntegerField(blank=True, null=True) + otp = models.CharField(blank=True, null=True) otp_expiry_time = models.DateTimeField(blank=True, null=True) REQUIRED_FIELDS = ["email"] diff --git a/Backend/user_service/user_service/user_app/urls.py b/Backend/user_service/user_service/user_app/urls.py index faea2ff..753ea58 100644 --- a/Backend/user_service/user_service/user_app/urls.py +++ b/Backend/user_service/user_service/user_app/urls.py @@ -5,9 +5,11 @@ urlpatterns = [ path("user/register/",RegisterViewSet.as_view({"post": "create_user",}),name="user-register",), + path("user/register/sendemailotp/",RegisterViewSet.as_view({"post": "send_email_otp",}),name="send-email-otp",), + path("user/register/verifyemailotp/",RegisterViewSet.as_view({"post": "verify_email_otp",}),name="verify-email-otp",), path("user/",UserViewSet.as_view({"get": "users_list",}),name="users-list",), path("user/login/",UserLoginView.as_view({"post": "login",}),name="user-login",), - path("user/verifyotp/",UserLoginView.as_view({"post": "verify_otp",}),name="verify-otp",), + path("user/login/verifyotp/",UserLoginView.as_view({"post": "verify_otp",}),name="verify-otp",), path("user//",UserViewSet.as_view({"get": "retrieve_user","patch": "update_user","delete": "destroy_user",}),name="user-detail",), path("user//logout/", UserLogoutView.as_view({"post": "logout",}),name="user-logout",), path("user//friends/", FriendsViewSet.as_view({"get": "friends_list"}), name="friends-list"), diff --git a/Backend/user_service/user_service/user_app/user_session_views.py b/Backend/user_service/user_service/user_app/user_session_views.py index 5db5e16..b0df639 100644 --- a/Backend/user_service/user_service/user_app/user_session_views.py +++ b/Backend/user_service/user_service/user_app/user_session_views.py @@ -88,6 +88,7 @@ def login(self, request): status_code = status.HTTP_400_BAD_REQUEST return Response(response_message, status=status_code) + #TODO: use check password for verify def verify_otp(self, request): status_code = status.HTTP_200_OK response = {} diff --git a/Backend/user_service/user_service/user_app/views.py b/Backend/user_service/user_service/user_app/views.py index fb368d0..7c92982 100644 --- a/Backend/user_service/user_service/user_app/views.py +++ b/Backend/user_service/user_service/user_app/views.py @@ -6,15 +6,20 @@ from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework_simplejwt.authentication import JWTAuthentication +from django.contrib.auth.hashers import check_password, make_password from rest_framework.exceptions import ValidationError -from django.db.models import Q from .models import UserProfileModel, FriendRequest from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.decorators import parser_classes +from django.utils.timezone import now from .serializers import UserSerializer, FriendSerializer +from django.core.mail import send_mail from django.conf import settings -import requests +from django.db.models import Q from dotenv import load_dotenv +from .user_session_views import generate_secret +import secrets +import requests import os load_dotenv() @@ -180,6 +185,64 @@ class RegisterViewSet(viewsets.ViewSet): """ permission_classes = [AllowAny] + def send_email(self, user, otp): + send_mail( + 'Email verification code', + f'Your verification code is: {otp}', + settings.EMAIL_HOST_USER, + [user.email], + fail_silently=False, + ) + + def send_email_otp(self, request) -> Response: + username = request.data.get("username") + try: + user_obj = get_object_or_404(UserProfileModel, username = username) + response_message = {} + status_code = status.HTTP_200_OK + if user_obj is not None: + otp = generate_secret() + user_obj.otp = make_password(str(otp)) + user_obj.otp_expiry_time = now() + user_obj.save() + self.send_email(user_obj, otp) + response_message = {"detail":"Email verification code sent to your email"} + else: + response_message = {"error": "email field required"} + status_code = status.HTTP_404_NOT_FOUND + except Exception as err: + response_message = {"error": str(err)} + status_code = status.HTTP_400_BAD_REQUEST + return Response(response_message, status=status_code) + + def verify_email_otp(self, request) -> Response: + response_message = {} + status_code = status.HTTP_200_OK + username = request.data.get('username') + if username is not None: + user = get_object_or_404(UserProfileModel, username = username) + otp = request.data.get('otp') + if otp is not None and user.otp is not None and user.otp_expiry_time is not None: + if check_password(str(otp), user.otp): + if user.otp_expiry_time > now(): + response_message = {"detail":"Email verified"} + user.otp = None + user.otp_expiry_time = None + user.save() + else: + response_message = {"error":"otp expired"} + status_code = status.HTTP_401_UNAUTHORIZED + else: + response_message = {'error':"Invalid otp"} + status_code = status.HTTP_401_UNAUTHORIZED + else: + response_message = {'otp field required'} + status_code = status.HTTP_400_BAD_REQUEST + else: + response_message = {'error': 'user field required'} + status_code = status.HTTP_400_BAD_REQUEST + return Response(response_message, status=status_code) + def create_user(self, request) -> Response: """ Method to create a new user. From b5fb47d6981698cb4890a39a06e32f295548f6d8 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Sat, 17 Aug 2024 15:32:56 +0300 Subject: [PATCH 04/22] changed email field to onetoonefield and created a confiremail model, updated register_user to send verification before user-registration, handled some error handlings in views.py for user-registratio and updated serializers.py --- .../user_service/user_app/models.py | 7 ++ .../user_service/user_app/serializers.py | 26 +++--- .../user_app/user_session_views.py | 2 - .../user_service/user_app/views.py | 93 +++++++++++-------- 4 files changed, 73 insertions(+), 55 deletions(-) diff --git a/Backend/user_service/user_service/user_app/models.py b/Backend/user_service/user_service/user_app/models.py index 09144bb..862bdc5 100644 --- a/Backend/user_service/user_service/user_app/models.py +++ b/Backend/user_service/user_service/user_app/models.py @@ -11,6 +11,12 @@ def user_directory_path(instance, filename): filename = f'{uuid.uuid4()}.{ext}' return os.path.join(str(instance.id), filename) +class ConfirmEmail(models.Model): + user_email = models.EmailField(unique=True, primary_key=True) + verify_status = models.BooleanField(default=False) + otp = models.CharField(null=True, blank=True) + otp_expiry_time = models.DateTimeField(blank=True, null=True) + class UserProfileModel(AbstractUser): """ User class to define the user model. @@ -22,6 +28,7 @@ class UserProfileModel(AbstractUser): Email: The email field is required for the user model. """ + email = models.OneToOneField(ConfirmEmail, related_name='user_profile', on_delete=models.CASCADE) avatar = models.ImageField(upload_to=user_directory_path, null=True, blank=True, default='default.jpg') friends = models.ManyToManyField("self", blank=True, symmetrical=True) online_status = models.BooleanField(default=False) diff --git a/Backend/user_service/user_service/user_app/serializers.py b/Backend/user_service/user_service/user_app/serializers.py index 4ffecb8..512bad9 100644 --- a/Backend/user_service/user_service/user_app/serializers.py +++ b/Backend/user_service/user_service/user_app/serializers.py @@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError from rest_framework import serializers from rest_framework.validators import UniqueValidator -from .models import UserProfileModel, FriendRequest, GameRoom +from .models import UserProfileModel, FriendRequest, GameRoom, ConfirmEmail from .validators import CustomPasswordValidator class GameRoomSerializer(serializers.ModelSerializer): @@ -57,9 +57,6 @@ class UserSerializer(serializers.ModelSerializer): create: Method to create a new user. update: Method to update a user. """ - email = serializers.EmailField( - validators=[UniqueValidator(queryset=UserProfileModel.objects.all())] - ) avatar = serializers.ImageField(required=False) friends = serializers.PrimaryKeyRelatedField( many=True, queryset=UserProfileModel.objects.all(), required=False # required=False means that the field is not required @@ -83,7 +80,7 @@ class Meta: ### Password should be strong password, minimum 8 characters, at least one uppercase letter, one lowercase letter, one number and one special character - def create(self, validate_data) -> UserProfileModel: + def create(self, validated_data) -> UserProfileModel: """ Method to create a new user. @@ -91,23 +88,23 @@ def create(self, validate_data) -> UserProfileModel: The password is validated using CustomPasswordValidator. The password is hashed before saving the user object. Args: - validate_data: The data to validate. + validated_data: The data to validate. Returns: User: The user object. """ try: - validate_password(validate_data["password"]) + validate_password(validated_data["password"]) except ValidationError as err: raise serializers.ValidationError(detail=err.messages) from err - password = validate_data.pop("password", None) - instance = self.Meta.model(**validate_data) + password = validated_data.pop("password", None) + instance = self.Meta.model(**validated_data) if password is not None: instance.set_password(password) instance.save() return instance - def update(self, instance, validate_data) -> UserProfileModel: + def update(self, instance, validated_data) -> UserProfileModel: """ Method to update a user. @@ -116,7 +113,7 @@ def update(self, instance, validate_data) -> UserProfileModel: Args: instance: The user object. - validate_data: The data to validate. + validated_data: The data to validate. Returns: User: The updated user object. @@ -125,7 +122,7 @@ def update(self, instance, validate_data) -> UserProfileModel: serializers.ValidationError: If the password is the same as the current password. """ - for attr, value in validate_data.items(): + for attr, value in validated_data.items(): if attr == "password" and value is not None: if instance.check_password(value): raise serializers.ValidationError(detail="New password must be different from the current password.") @@ -141,3 +138,8 @@ def update(self, instance, validate_data) -> UserProfileModel: setattr(instance, attr, value) instance.save() return instance + +class ConfirmEmailSerializer(serializers.ModelSerializer): + class Meta: + model = ConfirmEmail + fields = '__all__' diff --git a/Backend/user_service/user_service/user_app/user_session_views.py b/Backend/user_service/user_service/user_app/user_session_views.py index b0df639..6646882 100644 --- a/Backend/user_service/user_service/user_app/user_session_views.py +++ b/Backend/user_service/user_service/user_app/user_session_views.py @@ -78,8 +78,6 @@ def login(self, request): if "error" in response_message: status_code = response_message.get("status_code") response_message = response.json() - else: - status_code = status.HTTP_200_OK else: response_message = {"detail": "User is Inactive"} status_code = status.HTTP_401_UNAUTHORIZED diff --git a/Backend/user_service/user_service/user_app/views.py b/Backend/user_service/user_service/user_app/views.py index 7c92982..d8be342 100644 --- a/Backend/user_service/user_service/user_app/views.py +++ b/Backend/user_service/user_service/user_app/views.py @@ -8,11 +8,11 @@ from rest_framework_simplejwt.authentication import JWTAuthentication from django.contrib.auth.hashers import check_password, make_password from rest_framework.exceptions import ValidationError -from .models import UserProfileModel, FriendRequest +from .models import UserProfileModel, FriendRequest, ConfirmEmail from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.decorators import parser_classes -from django.utils.timezone import now -from .serializers import UserSerializer, FriendSerializer +from django.utils.timezone import now, timedelta +from .serializers import UserSerializer, FriendSerializer, ConfirmEmailSerializer from django.core.mail import send_mail from django.conf import settings from django.db.models import Q @@ -185,50 +185,50 @@ class RegisterViewSet(viewsets.ViewSet): """ permission_classes = [AllowAny] - def send_email(self, user, otp): + def send_email(self, email, otp): send_mail( 'Email verification code', f'Your verification code is: {otp}', settings.EMAIL_HOST_USER, - [user.email], + [email], fail_silently=False, ) def send_email_otp(self, request) -> Response: - username = request.data.get("username") - try: - user_obj = get_object_or_404(UserProfileModel, username = username) - response_message = {} - status_code = status.HTTP_200_OK - if user_obj is not None: + email = request.data.get("email") + response_message = {} + status_code = status.HTTP_200_OK + if email is not None: + try: + email_obj, create = ConfirmEmail.objects.get_or_create(user_email=email) otp = generate_secret() - user_obj.otp = make_password(str(otp)) - user_obj.otp_expiry_time = now() - user_obj.save() - self.send_email(user_obj, otp) + email_obj.otp = make_password(str(otp)) + email_obj.otp_expiry_time = now() + timedelta(minutes=1) + email_obj.save() + self.send_email(email_obj.user_email, otp) response_message = {"detail":"Email verification code sent to your email"} - else: - response_message = {"error": "email field required"} - status_code = status.HTTP_404_NOT_FOUND - except Exception as err: - response_message = {"error": str(err)} - status_code = status.HTTP_400_BAD_REQUEST + except Exception as err: + response_message = {"error": str(err)} + status_code = status.HTTP_400_BAD_REQUEST + else: + response_message = {"error": "email field required"} + status_code = status.HTTP_404_NOT_FOUND return Response(response_message, status=status_code) def verify_email_otp(self, request) -> Response: response_message = {} status_code = status.HTTP_200_OK - username = request.data.get('username') - if username is not None: - user = get_object_or_404(UserProfileModel, username = username) + email = request.data.get('email') + if email is not None: + email_obj = get_object_or_404(ConfirmEmail, user_email = email) otp = request.data.get('otp') - if otp is not None and user.otp is not None and user.otp_expiry_time is not None: - if check_password(str(otp), user.otp): - if user.otp_expiry_time > now(): + if otp is not None and email_obj.otp is not None and email_obj.otp_expiry_time is not None: + if check_password(str(otp), email_obj.otp): + if email_obj.otp_expiry_time > now(): response_message = {"detail":"Email verified"} - user.otp = None - user.otp_expiry_time = None - user.save() + email_obj.otp = None + email_obj.otp_expiry_time = None + email_obj.save() else: response_message = {"error":"otp expired"} status_code = status.HTTP_401_UNAUTHORIZED @@ -239,7 +239,7 @@ def verify_email_otp(self, request) -> Response: response_message = {'otp field required'} status_code = status.HTTP_400_BAD_REQUEST else: - response_message = {'error': 'user field required'} + response_message = {'error': 'email field required'} status_code = status.HTTP_400_BAD_REQUEST return Response(response_message, status=status_code) @@ -254,21 +254,32 @@ def create_user(self, request) -> Response: Returns: Response: The response object containing the user data. """ + response_message = {} + status_code = status.HTTP_201_CREATED try: - serializer = UserSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - if serializer.errors: - data = serializer.errors - if "email" in data: - data["email"] = ["A user with that email already exists."] - return Response({"error":data}, status=status.HTTP_400_BAD_REQUEST) + email = request.data.get("email") + email_obj = get_object_or_404(ConfirmEmail, user_email = email) + if email_obj is not None: + serializer = UserSerializer(data=request.data) + if serializer.is_valid(): + serializer.save() + response_message = serializer.data + if serializer.errors: + data = serializer.errors + if "email" in data: + data["email"] = ["A user with that email already exists."] + response_message = {"error":data}, + status_code=status.HTTP_400_BAD_REQUEST + except Http404: + response_message = {"error": "You have not verified your email yet!"} + status_code = status.HTTP_401_UNAUTHORIZED except ValidationError as err: item_lists = [] for item in err.detail: item_lists.append(item) - return Response({'error': item_lists}, status=status.HTTP_400_BAD_REQUEST) + response_message = {'error': item_lists} + status_code=status.HTTP_400_BAD_REQUEST + return Response(response_message, status = status_code) class FriendsViewSet(viewsets.ViewSet): authentication_classes = [JWTAuthentication] From 8c6b3f60f2f8399af5e5bd8e4a7ffe32ac2330bb Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Sat, 17 Aug 2024 18:47:24 +0300 Subject: [PATCH 05/22] handled email_obj verify_status in user registration views functions, modified email related name in models,fixed user-login and verify otp in user_session_views.py --- .../user_service/user_app/models.py | 2 +- .../user_app/user_session_views.py | 113 ++++++++++-------- .../user_service/user_app/views.py | 42 ++++--- 3 files changed, 90 insertions(+), 67 deletions(-) diff --git a/Backend/user_service/user_service/user_app/models.py b/Backend/user_service/user_service/user_app/models.py index 862bdc5..ba67891 100644 --- a/Backend/user_service/user_service/user_app/models.py +++ b/Backend/user_service/user_service/user_app/models.py @@ -28,7 +28,7 @@ class UserProfileModel(AbstractUser): Email: The email field is required for the user model. """ - email = models.OneToOneField(ConfirmEmail, related_name='user_profile', on_delete=models.CASCADE) + email = models.OneToOneField(ConfirmEmail, related_name='user_profile_email', on_delete=models.CASCADE) avatar = models.ImageField(upload_to=user_directory_path, null=True, blank=True, default='default.jpg') friends = models.ManyToManyField("self", blank=True, symmetrical=True) online_status = models.BooleanField(default=False) diff --git a/Backend/user_service/user_service/user_app/user_session_views.py b/Backend/user_service/user_service/user_app/user_session_views.py index 6646882..ca87a6f 100644 --- a/Backend/user_service/user_service/user_app/user_session_views.py +++ b/Backend/user_service/user_service/user_app/user_session_views.py @@ -33,19 +33,16 @@ def generate_secret(): class UserLoginView(viewsets.ViewSet): permission_classes = [AllowAny] - def send_email(self, user): + def send_email(self, email, otp): send_mail( 'Verification Code', - f'Your verification code is: {user.otp}', + f'Your verification code is: {otp}', settings.EMAIL_HOST_USER, - [user.email], + [email], fail_silently=False, ) - def authenticate_user(self, request): - username = request.data.get("username") - password = request.data.get("password") - + def authenticate_user(self, request, username, password): if username and password: user = authenticate(username=username, password=password) if user is not None: @@ -56,33 +53,39 @@ def login(self, request): status_code = status.HTTP_200_OK response = {} response_message = {} - user = self.authenticate_user(request) - if user is not None: - if user.is_active: - serializer = UserSerializer(user) - # send post request to token-service - if user.otp_status: - otp = generate_secret() - user.otp = make_password(otp) - user.otp_expiry_time = now() + timedelta(minutes=1) - user.save() - self.send_email(user) - response_message = {"detail":"Verification password sent to your email"} - status_code = status.HTTP_200_OK + username = request.data.get("username") + password = request.data.get("password") + if username and password: + user = self.authenticate_user(request, username, password) + if user is not None: + if user.is_active: + serializer = UserSerializer(user) + if user.otp_status: + otp = generate_secret() + user.otp = make_password(str(otp)) + user.otp_expiry_time = now() + timedelta(minutes=3) + user.save() + logger.info('user email = %s', serializer.data["email"]) + self.send_email(serializer.data["email"], otp) + response_message = {"detail":"Verification password sent to your email"} + status_code = status.HTTP_200_OK + else: + data = {"id": serializer.data["id"], "username": serializer.data["username"]} + response = requests.post(f"{TOEKNSERVICE}/auth/token/gen-tokens/", data=data, headers=headers) + if response.status_code == 201: + response_message = response.json() + # logger.info('user_data = %s', response.json()) + if "error" in response_message: + status_code = response_message.get("status_code") + response_message = response.json() else: - data = {"id": serializer.data["id"], "username": serializer.data["username"]} - response = requests.post(f"{TOEKNSERVICE}/auth/token/gen-tokens/", data=data, headers=headers) - if response.status_code == 201: - response_message = response.json() - # logger.info('user_data = %s', response.json()) - if "error" in response_message: - status_code = response_message.get("status_code") - response_message = response.json() + response_message = {"error": "User is Inactive"} + status_code = status.HTTP_401_UNAUTHORIZED else: - response_message = {"detail": "User is Inactive"} - status_code = status.HTTP_401_UNAUTHORIZED + response_message = {"error": "Invalid username or password"} + status_code = status.HTTP_400_BAD_REQUEST else: - response_message = {"detail": "Invalid username or password"} + response_message = {"error": "username and password fields are required"} status_code = status.HTTP_400_BAD_REQUEST return Response(response_message, status=status_code) @@ -91,31 +94,41 @@ def verify_otp(self, request): status_code = status.HTTP_200_OK response = {} response_message = {} - user = self.authenticate_user(request) - if user is not None: - if user.otp_status: - if user.otp == otp: - if user.otp_expiry_time > now(): - data = {"id": user.id, "username": username} - response = requests.post(f'{TOEKNSERVICE}/auth/token/gen-tokens/', data=data, headers=headers) - user.otp = None - user.otp_expiry_time = None - if response.status_code == 201: - response_message = response.json() - # logger.info('user_data = %s', response_message) - if "error" in response_message: - status_code = response_message.get("status_code") + username = request.data.get("username") + password = request.data.get("password") + otp = request.data.get("otp") + if username and password and otp: + user = self.authenticate_user(request, username, password) + if user is not None: + if user.otp_status: + if check_password(str(otp), user.otp): + if user.otp_expiry_time > now(): + data = {"id": user.id, "username": username} + response = requests.post(f'{TOEKNSERVICE}/auth/token/gen-tokens/', data=data, headers=headers) + if response.status_code == 201: + response_message = response.json() + user.otp = None + user.otp_expiry_time = None + # logger.info('user_data = %s', response_message) + if "error" in response_message: + status_code = response_message.get("status_code") + else: + status_code = status.HTTP_200_OK else: - status_code = status.HTTP_200_OK + response_message = {"error":"expired code"} + status_code = status.HTTP_401_UNAUTHORIZED else: - response_message = {"error":"expired password"} + response_message = {"error":"Invalid code"} status_code = status.HTTP_401_UNAUTHORIZED else: - response_message = {"error":"Invalid password"} - status_code = status.HTTP_401_UNAUTHORIZED + response_message = {"error":"You have not enable 2FA yet!"} + status_code = status.HTTP_400_BAD_REQUEST else: - response_message = {"error":"You have not enable 2FA yet!"} + response_message = {"error": "Invalid username or password"} status_code = status.HTTP_400_BAD_REQUEST + else: + response_message = {"error": "username, password and otp fields are required"} + status_code = status.HTTP_400_BAD_REQUEST return Response(response_message, status=status_code) class UserLogoutView(viewsets.ViewSet): diff --git a/Backend/user_service/user_service/user_app/views.py b/Backend/user_service/user_service/user_app/views.py index d8be342..1351ded 100644 --- a/Backend/user_service/user_service/user_app/views.py +++ b/Backend/user_service/user_service/user_app/views.py @@ -201,12 +201,16 @@ def send_email_otp(self, request) -> Response: if email is not None: try: email_obj, create = ConfirmEmail.objects.get_or_create(user_email=email) - otp = generate_secret() - email_obj.otp = make_password(str(otp)) - email_obj.otp_expiry_time = now() + timedelta(minutes=1) - email_obj.save() - self.send_email(email_obj.user_email, otp) - response_message = {"detail":"Email verification code sent to your email"} + if email_obj.verify_status == False: + otp = generate_secret() + email_obj.otp = make_password(str(otp)) + email_obj.otp_expiry_time = now() + timedelta(minutes=3) + email_obj.save() + self.send_email(email_obj.user_email, otp) + response_message = {"detail":"Email verification code sent to your email"} + else: + response_message = {"error": "This email already verified."} + status_code = status.HTTP_401_UNAUTHORIZED except Exception as err: response_message = {"error": str(err)} status_code = status.HTTP_400_BAD_REQUEST @@ -228,6 +232,7 @@ def verify_email_otp(self, request) -> Response: response_message = {"detail":"Email verified"} email_obj.otp = None email_obj.otp_expiry_time = None + email_obj.verify_status = True email_obj.save() else: response_message = {"error":"otp expired"} @@ -260,16 +265,21 @@ def create_user(self, request) -> Response: email = request.data.get("email") email_obj = get_object_or_404(ConfirmEmail, user_email = email) if email_obj is not None: - serializer = UserSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - response_message = serializer.data - if serializer.errors: - data = serializer.errors - if "email" in data: - data["email"] = ["A user with that email already exists."] - response_message = {"error":data}, - status_code=status.HTTP_400_BAD_REQUEST + if email_obj.verify_status: + serializer = UserSerializer(data=request.data) + if serializer.is_valid(): + serializer.validated_data["email"] = email_obj + serializer.save() + response_message = serializer.data + if serializer.errors: + data = serializer.errors + if "email" in data: + data["email"] = ["A user with that email already exists."] + response_message = {"error":data}, + status_code=status.HTTP_400_BAD_REQUEST + else: + response_message = {"error": "You have not confirmed your email yet!"}, + status_code=status.HTTP_401_UNAUTHORIZED except Http404: response_message = {"error": "You have not verified your email yet!"} status_code = status.HTTP_401_UNAUTHORIZED From 840650554a9f3a065f34ffd75f7fe442561f7a2f Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Sat, 17 Aug 2024 18:56:57 +0300 Subject: [PATCH 06/22] updated readme with 2fa urls and instructions --- Backend/user_service/user_service/README.md | 24 +++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/Backend/user_service/user_service/README.md b/Backend/user_service/user_service/README.md index efa4596..b5b14ea 100644 --- a/Backend/user_service/user_service/README.md +++ b/Backend/user_service/user_service/README.md @@ -28,7 +28,22 @@ You should send a JSON object with the following fields: "password": "password" } ``` +- `"user/register/sendemailotp/"` "send add email to confirmemail model and send otp to user email using POST method" +You should send a JSON object with the following fields: +```JSON +{ + "email": "email" +} +``` +- `"user/register/verifyemailotp/"` "verify email otp using POST method" +You should send a JSON object with the following fields: +```JSON +{ + "email": "email", + "otp": "otp" +} +``` - `http://localhost:3000/user/` "List users records using GET method" - `http://localhost:3000/user//` "without angel brackets" "retrieve, update and delete user record using GET, PUT and DELETE methods respectively" You can enable otp by sending a JSON object with the following fields: @@ -38,6 +53,15 @@ You can enable otp by sending a JSON object with the following fields: } ``` - `http://localhost:3000/user/login/` "login user using POST method" +You should send a JSON object with the following fields the user will receive an otp in the email if their otp_status is True: + +```JSON +{ + "username": "username", + "password": "password" +} +``` + - `http://localhost:3000/user/verifyotp/` "send user otp using POST method" You should send a JSON object with the following fields: ```JSON From 30a73ef9867b5570254912f20cb50d5b1f6f4dd0 Mon Sep 17 00:00:00 2001 From: mtoof Date: Sat, 17 Aug 2024 19:19:46 +0300 Subject: [PATCH 07/22] removed rabbitmq from backend --- Backend/rabbitmq/Dockerfile | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 Backend/rabbitmq/Dockerfile diff --git a/Backend/rabbitmq/Dockerfile b/Backend/rabbitmq/Dockerfile deleted file mode 100644 index 216a5f0..0000000 --- a/Backend/rabbitmq/Dockerfile +++ /dev/null @@ -1,28 +0,0 @@ -FROM alpine:3.20 - -# trunk-ignore(hadolint/DL3018) -RUN apk add --no-cache erlang openssl bash curl xz - -# Set environment variable for RabbitMQ installation directory -RUN addgroup -S rabbitmq && adduser -S rabbitmq -G rabbitmq -ENV RABBITMQ_HOME=/usr/local/bin/rabbitmq_server-3.13.3 -RUN mkdir -p $RABBITMQ_HOME && chown -R rabbitmq:rabbitmq /usr/local/bin/ && \ - # Download and install RabbitMQ - curl -L -o rabbitmq-server-generic-unix-3.13.3.tar.xz https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.13.3/rabbitmq-server-generic-unix-3.13.3.tar.xz && \ - tar xvf rabbitmq-server-generic-unix-3.13.3.tar.xz -C /usr/local/bin/ && \ - rm rabbitmq-server-generic-unix-3.13.3.tar.xz - - # Add RabbitMQ binaries to PATH -ENV PATH=$PATH:$RABBITMQ_HOME/sbin -USER root -RUN chown -R rabbitmq:rabbitmq $RABBITMQ_HOME -USER rabbitmq -RUN rabbitmq-plugins enable rabbitmq_management -# Expose ports used by RabbitMQ -EXPOSE 5672 15672 - -# Add HEALTHCHECK instruction -HEALTHCHECK --interval=30s --timeout=2s CMD curl -sSf http://localhost:15672 > /dev/null && echo "success" || echo "failure" - -# Run RabbitMQ server -CMD ["rabbitmq-server"] From cc6a5e9d2c6f4289dc2eddc86494cf251dfe93ad Mon Sep 17 00:00:00 2001 From: mtoof Date: Sat, 17 Aug 2024 19:21:50 +0300 Subject: [PATCH 08/22] updated readme line 48 changed put to patch --- Backend/user_service/user_service/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/user_service/user_service/README.md b/Backend/user_service/user_service/README.md index b5b14ea..143319e 100644 --- a/Backend/user_service/user_service/README.md +++ b/Backend/user_service/user_service/README.md @@ -45,7 +45,7 @@ You should send a JSON object with the following fields: } ``` - `http://localhost:3000/user/` "List users records using GET method" -- `http://localhost:3000/user//` "without angel brackets" "retrieve, update and delete user record using GET, PUT and DELETE methods respectively" +- `http://localhost:3000/user//` "without angel brackets" "retrieve, update and delete user record using GET, PATCH and DELETE methods respectively" You can enable otp by sending a JSON object with the following fields: ```JSON { From 5042002cdbb1615e81e9779b6cef7fb84d103b49 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Sun, 18 Aug 2024 19:27:11 +0300 Subject: [PATCH 09/22] Added available_username_email to views.py and handled error handling in views.py for user registration through create_user function, updated models for email field one to one relations, updated serializers to fetch the email to set constraint for username and email in userserializer, added online_status to user_session_views.py and updated urls.py --- Backend/user_service/user_service/README.md | 15 +++- .../user_service/user_app/models.py | 17 ---- .../user_service/user_app/serializers.py | 2 +- .../user_service/user_app/urls.py | 1 + .../user_app/user_session_views.py | 6 ++ .../user_service/user_app/views.py | 88 +++++++++++++------ Makefile | 2 +- 7 files changed, 83 insertions(+), 48 deletions(-) diff --git a/Backend/user_service/user_service/README.md b/Backend/user_service/user_service/README.md index 143319e..5821c25 100644 --- a/Backend/user_service/user_service/README.md +++ b/Backend/user_service/user_service/README.md @@ -44,6 +44,16 @@ You should send a JSON object with the following fields: "otp": "otp" } ``` + +- `"user/register/availableuser/"` "check if the username and email are available using POST method" +You should send a JSON object with the following fields: +```JSON +{ + "username": "username", + "email": "email" +} +``` + - `http://localhost:3000/user/` "List users records using GET method" - `http://localhost:3000/user//` "without angel brackets" "retrieve, update and delete user record using GET, PATCH and DELETE methods respectively" You can enable otp by sending a JSON object with the following fields: @@ -212,9 +222,8 @@ pip install channels-redis from . import consumers websocket_urlpatterns = [ - re_path(r'ws/notify/', consumers.NotificationConsumer.as_asgi()), - re_path(r'ws/online/', consumers.OnlineStatusConsumer.as_asgi()), - re_path(r'ws//', consumers.PersonalChatConsumer.as_asgi()), + re_path(r'ws/online/', consumers.OnlineStatusConsumer.as_asgi()), + re_path(r'ws/game/room/(?P\w+)/$', GameRoomConsumer.GameRoomConsumer.as_asgi()), ] ``` diff --git a/Backend/user_service/user_service/user_app/models.py b/Backend/user_service/user_service/user_app/models.py index ba67891..6285aba 100644 --- a/Backend/user_service/user_service/user_app/models.py +++ b/Backend/user_service/user_service/user_app/models.py @@ -54,23 +54,6 @@ def __str__(self): class Meta: unique_together = ('sender_user', 'receiver_user') -class ChatModel(models.Model): - sender = models.CharField(max_length=100, default=None) - message = models.TextField(null=True, blank=True) - thread_name = models.CharField(null=True, blank=True, max_length=50) - timestamp = models.DateTimeField(auto_now_add=True) - - def __str__(self) -> str: - return self.message or "" - -class ChatNotification(models.Model): - chat = models.ForeignKey(to=ChatModel, on_delete=models.CASCADE) - user = models.ForeignKey(to=UserProfileModel, on_delete=models.CASCADE) - is_seen = models.BooleanField(default=False) - - def __str__(self) -> str: - return self.user.username - class GameRoom(models.Model): room_name = models.CharField(max_length=50, unique=True) player1 = models.ForeignKey(UserProfileModel, on_delete=models.CASCADE, null=True, related_name='player1') diff --git a/Backend/user_service/user_service/user_app/serializers.py b/Backend/user_service/user_service/user_app/serializers.py index 512bad9..24613e8 100644 --- a/Backend/user_service/user_service/user_app/serializers.py +++ b/Backend/user_service/user_service/user_app/serializers.py @@ -57,6 +57,7 @@ class UserSerializer(serializers.ModelSerializer): create: Method to create a new user. update: Method to update a user. """ + email = serializers.PrimaryKeyRelatedField(queryset=ConfirmEmail.objects.all()) avatar = serializers.ImageField(required=False) friends = serializers.PrimaryKeyRelatedField( many=True, queryset=UserProfileModel.objects.all(), required=False # required=False means that the field is not required @@ -77,7 +78,6 @@ class Meta: "otp_expiry_time" ] extra_kwargs = {"password": {"write_only": True}} - ### Password should be strong password, minimum 8 characters, at least one uppercase letter, one lowercase letter, one number and one special character def create(self, validated_data) -> UserProfileModel: diff --git a/Backend/user_service/user_service/user_app/urls.py b/Backend/user_service/user_service/user_app/urls.py index 753ea58..6d47e4a 100644 --- a/Backend/user_service/user_service/user_app/urls.py +++ b/Backend/user_service/user_service/user_app/urls.py @@ -7,6 +7,7 @@ path("user/register/",RegisterViewSet.as_view({"post": "create_user",}),name="user-register",), path("user/register/sendemailotp/",RegisterViewSet.as_view({"post": "send_email_otp",}),name="send-email-otp",), path("user/register/verifyemailotp/",RegisterViewSet.as_view({"post": "verify_email_otp",}),name="verify-email-otp",), + path("user/register/availableuser/",RegisterViewSet.as_view({"post": "available_username_email",}),name="available-user-obj",), path("user/",UserViewSet.as_view({"get": "users_list",}),name="users-list",), path("user/login/",UserLoginView.as_view({"post": "login",}),name="user-login",), path("user/login/verifyotp/",UserLoginView.as_view({"post": "verify_otp",}),name="verify-otp",), diff --git a/Backend/user_service/user_service/user_app/user_session_views.py b/Backend/user_service/user_service/user_app/user_session_views.py index ca87a6f..b573849 100644 --- a/Backend/user_service/user_service/user_app/user_session_views.py +++ b/Backend/user_service/user_service/user_app/user_session_views.py @@ -73,6 +73,8 @@ def login(self, request): data = {"id": serializer.data["id"], "username": serializer.data["username"]} response = requests.post(f"{TOEKNSERVICE}/auth/token/gen-tokens/", data=data, headers=headers) if response.status_code == 201: + user.online_status = True + user.save() response_message = response.json() # logger.info('user_data = %s', response.json()) if "error" in response_message: @@ -109,6 +111,8 @@ def verify_otp(self, request): response_message = response.json() user.otp = None user.otp_expiry_time = None + user.online_status = True + user.save() # logger.info('user_data = %s', response_message) if "error" in response_message: status_code = response_message.get("status_code") @@ -155,5 +159,7 @@ def logout(self, request, pk=None): status_code = response_message.get("status_code") else: response_message = {"detail": "User logged out successfully"} + user.online_status = False + user.save() status_code = status.HTTP_200_OK return Response(response_message, status=status_code) diff --git a/Backend/user_service/user_service/user_app/views.py b/Backend/user_service/user_service/user_app/views.py index 1351ded..797a41f 100644 --- a/Backend/user_service/user_service/user_app/views.py +++ b/Backend/user_service/user_service/user_app/views.py @@ -2,12 +2,12 @@ from django.shortcuts import get_object_or_404, render from django.http import Http404 from django.core.exceptions import ValidationError as DjangoValidationError +from rest_framework.exceptions import ValidationError from rest_framework import status, viewsets from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework_simplejwt.authentication import JWTAuthentication from django.contrib.auth.hashers import check_password, make_password -from rest_framework.exceptions import ValidationError from .models import UserProfileModel, FriendRequest, ConfirmEmail from rest_framework.parsers import MultiPartParser, FormParser from rest_framework.decorators import parser_classes @@ -18,13 +18,15 @@ from django.db.models import Q from dotenv import load_dotenv from .user_session_views import generate_secret -import secrets +import logging import requests import os load_dotenv() + TOEKNSERVICE = os.environ.get('TOKEN_SERVICE') +logger = logging.getLogger(__name__) headers = { "X-SERVICE-SECRET": settings.SECRET_KEY # Replace with your actual secret key } @@ -225,29 +227,57 @@ def verify_email_otp(self, request) -> Response: email = request.data.get('email') if email is not None: email_obj = get_object_or_404(ConfirmEmail, user_email = email) - otp = request.data.get('otp') - if otp is not None and email_obj.otp is not None and email_obj.otp_expiry_time is not None: - if check_password(str(otp), email_obj.otp): - if email_obj.otp_expiry_time > now(): - response_message = {"detail":"Email verified"} - email_obj.otp = None - email_obj.otp_expiry_time = None - email_obj.verify_status = True - email_obj.save() + if email_obj.verify_status == False: + otp = request.data.get('otp') + if otp is not None: + if check_password(str(otp), email_obj.otp): + if email_obj.otp_expiry_time > now(): + response_message = {"detail":"Email verified"} + email_obj.otp = None + email_obj.otp_expiry_time = None + email_obj.verify_status = True + email_obj.save() + else: + response_message = {"error":"otp expired"} + status_code = status.HTTP_401_UNAUTHORIZED else: - response_message = {"error":"otp expired"} + response_message = {'error':"Invalid otp"} status_code = status.HTTP_401_UNAUTHORIZED else: - response_message = {'error':"Invalid otp"} - status_code = status.HTTP_401_UNAUTHORIZED + response_message = {'error':'otp field required'} + status_code = status.HTTP_400_BAD_REQUEST else: - response_message = {'otp field required'} + response_message = {'error':'This email is already verified.'} status_code = status.HTTP_400_BAD_REQUEST else: response_message = {'error': 'email field required'} status_code = status.HTTP_400_BAD_REQUEST return Response(response_message, status=status_code) + def available_username_email(self, request) -> Response: + email = request.data.get("email") + username = request.data.get("username") + response_message = {} + status_code = status.HTTP_200_OK + if username is not None and email is not None: + user_obj = UserProfileModel.objects.filter(username = username).first() + email_obj = UserProfileModel.objects.filter(email = email).first() + if user_obj is not None and email_obj is not None: + response_message = {"error": "username and email are not available"} + status_code = status.HTTP_400_BAD_REQUEST + elif user_obj is not None: + response_message = {"error": "username is not available"} + status_code = status.HTTP_400_BAD_REQUEST + elif email_obj is not None: + response_message = {"error": "email is not available"} + status_code = status.HTTP_400_BAD_REQUEST + else: + response_message = {"detail": "username and email are available"} + else: + response_message = {'error':"username and email fields are required"} + status_code = status.HTTP_400_BAD_REQUEST + return Response(response_message, status=status_code) + def create_user(self, request) -> Response: """ Method to create a new user. @@ -264,18 +294,21 @@ def create_user(self, request) -> Response: try: email = request.data.get("email") email_obj = get_object_or_404(ConfirmEmail, user_email = email) + result = UserProfileModel.objects.filter(email = email_obj.pk).first() if email_obj is not None: - if email_obj.verify_status: - serializer = UserSerializer(data=request.data) - if serializer.is_valid(): - serializer.validated_data["email"] = email_obj - serializer.save() - response_message = serializer.data - if serializer.errors: - data = serializer.errors - if "email" in data: - data["email"] = ["A user with that email already exists."] - response_message = {"error":data}, + if email_obj.verify_status == True: + if result is not None: + response_message = {'error': {"email":"A user with that email already exists."}} + status_code=status.HTTP_400_BAD_REQUEST + else: + data = request.data + data["email"] = email_obj.pk + serializer = UserSerializer(data=data) + if serializer.is_valid(): + serializer.save() + response_message = serializer.data + if serializer.errors: + response_message = {"error":serializer.errors}, status_code=status.HTTP_400_BAD_REQUEST else: response_message = {"error": "You have not confirmed your email yet!"}, @@ -289,6 +322,9 @@ def create_user(self, request) -> Response: item_lists.append(item) response_message = {'error': item_lists} status_code=status.HTTP_400_BAD_REQUEST + except Exception as err: + response_message = {'error':str(err)} + status_code = status.HTTP_400_BAD_REQUEST return Response(response_message, status = status_code) class FriendsViewSet(viewsets.ViewSet): diff --git a/Makefile b/Makefile index f296478..e57e503 100644 --- a/Makefile +++ b/Makefile @@ -108,7 +108,7 @@ dlog: fi docker exec -it $(filter-out $@,$(MAKECMDGOALS)) bash -c 'cat /var/log/django_debug.log' -.PHONY: dlog-err +.PHONY: dlogerr dlog-err: @if [ -z "$(filter-out $@,$(MAKECMDGOALS))" ]; then \ echo "Error: No container name provided."; \ From a49e73b6e13478048ed6ff13df7aae1a832c9245 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Sun, 18 Aug 2024 21:11:54 +0300 Subject: [PATCH 10/22] removed models related to chat and notifications --- .../user_service/user_app/admin.py | 6 +++--- .../user_service/user_app/routing.py | 3 +-- .../user_service/user_app/signals.py | 19 +------------------ 3 files changed, 5 insertions(+), 23 deletions(-) diff --git a/Backend/user_service/user_service/user_app/admin.py b/Backend/user_service/user_service/user_app/admin.py index 909c4bf..c94c8b0 100644 --- a/Backend/user_service/user_service/user_app/admin.py +++ b/Backend/user_service/user_service/user_app/admin.py @@ -1,6 +1,6 @@ from django.contrib import admin -from user_app.models import ChatModel, UserProfileModel, ChatNotification +from user_app.models import UserProfileModel # Register your models here. -admin.site.register(ChatModel) +# admin.site.register(ChatModel) admin.site.register(UserProfileModel) -admin.site.register(ChatNotification) +# admin.site.register(ChatNotification) diff --git a/Backend/user_service/user_service/user_app/routing.py b/Backend/user_service/user_service/user_app/routing.py index 94935d9..28012ce 100644 --- a/Backend/user_service/user_service/user_app/routing.py +++ b/Backend/user_service/user_service/user_app/routing.py @@ -1,8 +1,7 @@ from django.urls import re_path -from . import NotificationConsumer, GameRoomConsumer, OnlineStatusConsumer +from . import GameRoomConsumer, OnlineStatusConsumer websocket_urlpatterns = [ - re_path(r'ws/notify/', NotificationConsumer.NotificationConsumer.as_asgi()), re_path(r'ws/online/', OnlineStatusConsumer.OnlineStatusConsumer.as_asgi()), re_path(r'ws/game/room/(?P\w+)/$', GameRoomConsumer.GameRoomConsumer.as_asgi()), ] diff --git a/Backend/user_service/user_service/user_app/signals.py b/Backend/user_service/user_service/user_app/signals.py index dc693a4..876a5cf 100755 --- a/Backend/user_service/user_service/user_app/signals.py +++ b/Backend/user_service/user_service/user_app/signals.py @@ -1,27 +1,10 @@ from django.db.models.signals import post_save from django.dispatch import receiver -from .models import UserProfileModel, ChatNotification +from .models import UserProfileModel import json from channels.layers import get_channel_layer from asgiref.sync import async_to_sync -@receiver(post_save, sender=ChatNotification) -def send_notification(sender, instance, created, **kwargs): - if created: - channel_layer = get_channel_layer() - notification_obj = ChatNotification.objects.filter(is_seen=False, user=instance.user).count() - user_id = str(instance.user.id) - data = { - 'count':notification_obj - } - - async_to_sync(channel_layer.group_send)( - user_id, { - 'type':'send_notification', - 'value':json.dumps(data) - } - ) - @receiver(post_save, sender=UserProfileModel) def send_onlineStatus(sender, instance, created, **kwargs): if not created: From ca752239b239cf768b95a82b902cca91df943b09 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Sun, 18 Aug 2024 21:13:23 +0300 Subject: [PATCH 11/22] Delete Backend/user_service/user_service/user_app/PersonalChatConsumer.py --- .../user_app/PersonalChatConsumer.py | 70 ------------------- 1 file changed, 70 deletions(-) delete mode 100644 Backend/user_service/user_service/user_app/PersonalChatConsumer.py diff --git a/Backend/user_service/user_service/user_app/PersonalChatConsumer.py b/Backend/user_service/user_service/user_app/PersonalChatConsumer.py deleted file mode 100644 index fd476e1..0000000 --- a/Backend/user_service/user_service/user_app/PersonalChatConsumer.py +++ /dev/null @@ -1,70 +0,0 @@ -import json -from channels.generic.websocket import AsyncWebsocketConsumer -import asyncio -import threading -from channels.db import database_sync_to_async -from django.views.decorators.csrf import csrf_exempt -from django.utils.decorators import method_decorator - -@method_decorator(csrf_exempt, name='dispatch') -class PersonalChatConsumer(AsyncWebsocketConsumer): - async def connect(self): - my_id = self.scope['user'].id - other_user_id = self.scope['url_route']['kwargs']['id'] - if int(my_id) > int(other_user_id): - self.room_name = f'{my_id}-{other_user_id}' - else: - self.room_name = f'{other_user_id}-{my_id}' - - self.room_group_name = 'chat_%s' % self.room_name - - await self.channel_layer.group_add( - self.room_group_name, - self.channel_name - ) - - await self.accept() - - async def receive(self, text_data: str = "", bytes_data=None): - data = json.loads(text_data) - print(data) - message = data['message'] - username = data['username'] - receiver = data['receiver'] - - await self.save_message(username, self.room_group_name, message, receiver) - await self.channel_layer.group_send( - self.room_group_name, - { - 'type': 'chat_message', - 'message': message, - 'username': username, - } - ) - - async def chat_message(self, event): - message = event['message'] - username = event['username'] - - await self.send(text_data=json.dumps({ - 'message': message, - 'username': username - })) - - async def disconnect(self, code): - self.channel_layer.group_discard( - self.room_group_name, - self.channel_name - ) - - @database_sync_to_async - def save_message(self, username, thread_name, message, receiver): - from .models import ChatModel, ChatNotification - from django.contrib.auth.models import User - - chat_obj = ChatModel.objects.create( - sender=username, message=message, thread_name=thread_name) - other_user_id = self.scope['url_route']['kwargs']['id'] - get_user = User.objects.get(id=other_user_id) - if receiver == get_user.username: - ChatNotification.objects.create(chat=chat_obj, user=get_user) \ No newline at end of file From 828e29747350473147a005d3d119f9fcc0f0d667 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Sun, 18 Aug 2024 21:13:40 +0300 Subject: [PATCH 12/22] Delete Backend/user_service/user_service/user_app/NotificationConsumer.py --- .../user_app/NotificationConsumer.py | 33 ------------------- 1 file changed, 33 deletions(-) delete mode 100644 Backend/user_service/user_service/user_app/NotificationConsumer.py diff --git a/Backend/user_service/user_service/user_app/NotificationConsumer.py b/Backend/user_service/user_service/user_app/NotificationConsumer.py deleted file mode 100644 index fcca9c8..0000000 --- a/Backend/user_service/user_service/user_app/NotificationConsumer.py +++ /dev/null @@ -1,33 +0,0 @@ -import json -from channels.generic.websocket import AsyncWebsocketConsumer -import asyncio -import threading -from channels.db import database_sync_to_async -from django.views.decorators.csrf import csrf_exempt -from django.utils.decorators import method_decorator - -@method_decorator(csrf_exempt, name='dispatch') -class NotificationConsumer(AsyncWebsocketConsumer): - async def connect(self): - my_id = self.scope['user'].id - self.room_group_name = f'{my_id}' - await self.channel_layer.group_add( - self.room_group_name, - self.channel_name - ) - - await self.accept() - - async def disconnect(self, code): - self.channel_layer.group_discard( - self.room_group_name, - self.channel_name - ) - - async def send_notification(self, event): - data = json.loads(event.get('value')) - count = data['count'] - print(count) - await self.send(text_data=json.dumps({ - 'count':count - })) From 0093fb6648c7b5275a40c05222c361e24066960a Mon Sep 17 00:00:00 2001 From: mtoof Date: Mon, 19 Aug 2024 09:28:04 +0300 Subject: [PATCH 13/22] updated readme and fixed typo error 'user/login/verifyotp/' in line 75 --- Backend/user_service/README.md | 316 ++++++++++++++++-- Backend/user_service/user_service/README.md | 353 -------------------- 2 files changed, 296 insertions(+), 373 deletions(-) delete mode 100644 Backend/user_service/user_service/README.md diff --git a/Backend/user_service/README.md b/Backend/user_service/README.md index 016a50d..e97b3b1 100644 --- a/Backend/user_service/README.md +++ b/Backend/user_service/README.md @@ -6,15 +6,13 @@ Upon GET,PUT and DELETE requests for a user record, the user service will retrie ## Docker container configuration -Every single REST API endpoint has their own database. The database is a PostgreSQL database. The database name for all the endpoints is `postgres`. The database user and password for all the endpoints is inside the env file. The database port for all the endpoints is `5432`. +Every single REST API endpoint has their own database. The database is a PostgreSQL database. The database name for this endpoints is `user_service`. The database user and password for all the endpoints is inside the env file. The database port for all the endpoints is `5432`. The requirements package is inside the requirements.txt file. -The run_consumer.sh file is used to run the consumer.py file inside the user_management/user_management folder. The consumer.py file is used to consume the message from the RabbitMQ message broker and check if the username and password are correct. -The tools.sh and run_consumer.sh files are running inside the Docker container using the supervisord service. The supervisord service is used to run multiple services inside the Docker container. -The tools.sh file is used to run the init_database.sh file and run the API. +The tools.sh file is used to run the API. The API runs inside a virtual environment. The virtual environment is created inside the Docker container using command python3.12 -m venv venv. The virtual environment is activated using command source venv/bin/activate inside the tools.sh file. -The API runs on port 8000 and exposed to 8000. +The API runs on port 8000. ## Tutorial to use the user_service @@ -30,12 +28,61 @@ You should send a JSON object with the following fields: "password": "password" } ``` +- `"user/register/sendemailotp/"` "send add email to confirmemail model and send otp to user email using POST method" +You should send a JSON object with the following fields: +```JSON +{ + "email": "email" +} +``` + +- `"user/register/verifyemailotp/"` "verify email otp using POST method" +You should send a JSON object with the following fields: +```JSON +{ + "email": "email", + "otp": "otp" +} +``` + +- `"user/register/availableuser/"` "check if the username and email are available using POST method" +You should send a JSON object with the following fields: +```JSON +{ + "username": "username", + "email": "email" +} +``` - `http://localhost:3000/user/` "List users records using GET method" -- `http://localhost:3000/user//` "without angel brackets" "retrieve, update and delete user record using GET, PUT and DELETE methods respectively" +- `http://localhost:3000/user//` "without angel brackets" "retrieve, update and delete user record using GET, PATCH and DELETE methods respectively" +You can enable otp by sending a JSON object with the following fields: +```JSON +{ + "otp_status": "True" +} +``` - `http://localhost:3000/user/login/` "login user using POST method" +You should send a JSON object with the following fields the user will receive an otp in the email if their otp_status is True: + +```JSON +{ + "username": "username", + "password": "password" +} +``` + +- `http://localhost:3000/user/login/verifyotp/` "send user otp using POST method" +You should send a JSON object with the following fields: +```JSON +{ + "username": "username", + "password": "password", + "otp": "otp" +} +``` - `http://localhost:3000/user/logout/` "logout user using POST method" -- `"http://localhost:3000/user//friends/"` "List friends of a user using GET method" +- `http://localhost:3000/user//friends/` "List friends of a user using GET method" The endpoint will return value is a JSON object with the following fields: ```JSON [ @@ -46,14 +93,14 @@ The endpoint will return value is a JSON object with the following fields: } ] ``` -- `"http://localhost:3000/user//request/"` send friend request to a user in a JSON object using POST method the JSON object should contain the following fields: +- `http://localhost:3000/user//request/` send friend request to a user in a JSON object using POST method the JSON object should contain the following fields: ```JSON { - "username": "username", + "username": "username" } ``` -- `"http://localhost:3000/user//accept//"` accept friend request PUT method -- `"http://localhost:3000/user//pending/"` "List pending friend requests of a user using GET method" +- `http://localhost:3000/user//accept//` accept friend request PUT method +- `http://localhost:3000/user//pending/` "List pending friend requests of a user using GET method" The endpoint will return value is a JSON object with the following fields: ```JSON [ @@ -66,8 +113,8 @@ The endpoint will return value is a JSON object with the following fields: } ] ``` -- `"http://localhost:3000/user//reject//"` "Accept or reject friend request using PUT method" -- `"http://localhost:3000/user//friends//remove/"` "Remove friend using DELETE method" +- `http://localhost:3000/user//reject//` "Accept or reject friend request using PUT method" +- `http://localhost:3000/user//friends//remove/` "Remove friend using DELETE method" The API will store the username, email and hashed password in the User table. @@ -76,11 +123,240 @@ The username and email are unique. The User table consists of the following fields: You can find it in user_management/user_management/users/models.py -| Field Name | Data Type | Description | -| ---------- | --------- | ---------------------------------- | -| id | Integer | Primary Key | -| username | String | User Name | -| email | String | User Email | -| password | String | User Password (Password is hashed) | +| Field Name | Data Type | Description | +| --------------- | --------- | ---------------------------------- | +| id | Integer | Primary Key | +| username | String | User Name | +| email | String | User Email | +| password | String | User Password (Password is hashed) | +| friends | ManyToMany| Friends of the user | +| avatar | Image | User Avatar | +| otp_status | Boolean | OTP Status | +| otp | Integer | OTP | +| otp_expiry_time | DateTime | OTP Expiry Time | + + +The ConfirmEmail table consists of the following fields: + +| Field Name | Data Type | Description | +| --------------- | --------- | ---------------------------------- | +| user_email | String | User Email | +| verify_status | Boolean | OTP Status | +| otp | Integer | OTP | +| otp_expiry_time | DateTime | OTP Expiry Time | + + +## WebSocket Integration Documentation + +### Overview + +This document provides an overview of how WebSocket integration has been implemented in the Django project, enabling real-time online status updates and notifications. It includes information on the setup, testing, and how the frontend can access the WebSocket services. + +### Backend Setup + +## GameRoom model +The GameRoom model is used to store the game room information. The GameRoom model consists of the following fields: + +| Field Name | Data Type | Description | +| ---------- | --------------------------- | ----------- | +| room_name | String | Room Name | +| player1 | ForeignKey from UserProfile | Player 1 | +| player2 | ForeignKey from UserProfile | Player 2 | + +#### Dependencies + +Django Channels and Redis were installed: + +```bash +pip install channels +pip install channels-redis +``` + +#### Project Configuration + +- **`settings.py`:** + + `channels` was added to `INSTALLED_APPS` and the channel layers were configured with Redis: + + ```python + INSTALLED_APPS = [ + # other apps + 'channels', + ] + + ASGI_APPLICATION = 'your_project_name.asgi.application' + + CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer", + "CONFIG": { + "hosts": [{ + "address": "redis://redis:6379", + "ssl_cert_reqs": None, + }], + }, + }, + } + ``` + +- **`asgi.py`:** + + `asgi.py` was set up to handle WebSocket connections: + + ```python + import os + from django.core.asgi import get_asgi_application + from channels.routing import ProtocolTypeRouter, URLRouter + from channels.auth import AuthMiddlewareStack + import user_app.routing + + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project_name.settings') + + application = ProtocolTypeRouter({ + 'http': get_asgi_application(), + 'websocket': AuthMiddlewareStack( + URLRouter( + user_app.routing.websocket_urlpatterns + ) + ) + }) + ``` + +- **`routing.py`:** + + WebSocket URL patterns were defined: + + ```python + from django.urls import re_path + from . import consumers + + websocket_urlpatterns = [ + re_path(r'ws/online/', consumers.OnlineStatusConsumer.as_asgi()), + re_path(r'ws/game/room/(?P\w+)/$', GameRoomConsumer.GameRoomConsumer.as_asgi()), + ] + ``` + +#### Nginx Configuration + +Nginx was set up to support WebSocket connections. The `nginx.conf` was updated: + +```nginx +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 443 ssl; + # other configurations + + location /ws/ { + proxy_pass http://websocket-service; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +#### Docker Configuration + +The Docker setup included Redis service: + +```yaml +services: + redis: + image: redis + container_name: redis + networks: + - transcendence_network + +### Testing WebSocket Connections + +#### Using Postman + +1. **Connect to WebSocket:** + + - Open Postman. + - Select "New" -> "WebSocket Request". + - Enter the WebSocket URL: `ws://127.0.0.1:3000/ws/online/`. + +2. **Send Messages:** + + - Use the JSON format to send messages: + ```json + { + "username": "example_user", + "type": "open" + } + ``` + - To close the connection: + ```json + { + "username": "example_user", + "type": "close" + } + ``` + +### Frontend Access + +To integrate WebSocket connections in the frontend, follow these steps: + +1. **Connect to WebSocket:** + + Use JavaScript to create a WebSocket connection: + + ```javascript + const socket = new WebSocket('ws://127.0.0.1:3000/ws/online/'); + + socket.onopen = function(event) { + console.log('WebSocket is connected.'); + socket.send(JSON.stringify({ + username: 'example_user', + type: 'open' + })); + }; + + socket.onmessage = function(event) { + const data = JSON.parse(event.data); + console.log('Message from server ', data); + }; + + socket.onclose = function(event) { + console.log('WebSocket is closed now.'); + }; + + socket.onerror = function(error) { + console.log('WebSocket Error: ' + error); + }; + ``` + +2. **Handling WebSocket Events:** + + Handle WebSocket events to update the frontend UI accordingly. For example: + + ```javascript + socket.onmessage = function(event) { + const data = JSON.parse(event.data); + if (data.online_status !== undefined) { + updateUserStatus(data.username, data.online_status); + } + }; + + function updateUserStatus(username, status) { + const userElement = document.getElementById(username); + if (userElement) { + userElement.className = status ? 'online' : 'offline'; + } + } + ``` + +### Summary + +The setup included configuring Django Channels, Redis, and Nginx to support WebSocket connections. The frontend can connect to the WebSocket service and handle events to provide real-time updates to users. + -Later I will limit the access to the API using nginx reverse proxy and only the frontend will be able to access the API. diff --git a/Backend/user_service/user_service/README.md b/Backend/user_service/user_service/README.md deleted file mode 100644 index 5821c25..0000000 --- a/Backend/user_service/user_service/README.md +++ /dev/null @@ -1,353 +0,0 @@ -# User_service - -The user service is responsible for creating, reading, updating and deleting user records. The user service receives the username, email and password then stores them in the User table. The username and email are unique and the password is hashed and write-only. - -Upon GET,PUT and DELETE requests for a user record, the user service will retrieve the access token from the request header and send it to the profile service to check if the access token exists in the UserTokens table. If the access token exists, the user service will process the request for the user record if the user record exists in the User table. - -## Docker container configuration - -Every single REST API endpoint has their own database. The database is a PostgreSQL database. The database name for this endpoints is `user_service`. The database user and password for all the endpoints is inside the env file. The database port for all the endpoints is `5432`. - -The requirements package is inside the requirements.txt file. -The tools.sh file is used to run the API. -The API runs inside a virtual environment. The virtual environment is created inside the Docker container using command python3.12 -m venv venv. The virtual environment is activated using command source venv/bin/activate inside the tools.sh file. - -The API runs on port 8000. - -## Tutorial to use the user_service - -After running the makefile, you can access the API using the following url: - -- `http://localhost:3000/user/register/` "create user record using POST method" -You should send a JSON object with the following fields: - -```JSON -{ - "username": "username", - "email": "email", - "password": "password" -} -``` -- `"user/register/sendemailotp/"` "send add email to confirmemail model and send otp to user email using POST method" -You should send a JSON object with the following fields: -```JSON -{ - "email": "email" -} -``` - -- `"user/register/verifyemailotp/"` "verify email otp using POST method" -You should send a JSON object with the following fields: -```JSON -{ - "email": "email", - "otp": "otp" -} -``` - -- `"user/register/availableuser/"` "check if the username and email are available using POST method" -You should send a JSON object with the following fields: -```JSON -{ - "username": "username", - "email": "email" -} -``` - -- `http://localhost:3000/user/` "List users records using GET method" -- `http://localhost:3000/user//` "without angel brackets" "retrieve, update and delete user record using GET, PATCH and DELETE methods respectively" -You can enable otp by sending a JSON object with the following fields: -```JSON -{ - "otp_status": "True" -} -``` -- `http://localhost:3000/user/login/` "login user using POST method" -You should send a JSON object with the following fields the user will receive an otp in the email if their otp_status is True: - -```JSON -{ - "username": "username", - "password": "password" -} -``` - -- `http://localhost:3000/user/verifyotp/` "send user otp using POST method" -You should send a JSON object with the following fields: -```JSON -{ - "username": "username", - "password": "password", - "otp": "otp" -} -``` -- `http://localhost:3000/user/logout/` "logout user using POST method" -- `http://localhost:3000/user//friends/` "List friends of a user using GET method" -The endpoint will return value is a JSON object with the following fields: -```JSON -[ - { - "id": "id", - "username": "xxx", - "status": "status" - } -] -``` -- `http://localhost:3000/user//request/` send friend request to a user in a JSON object using POST method the JSON object should contain the following fields: -```JSON -{ - "username": "username" -} -``` -- `http://localhost:3000/user//accept//` accept friend request PUT method -- `http://localhost:3000/user//pending/` "List pending friend requests of a user using GET method" -The endpoint will return value is a JSON object with the following fields: -```JSON -[ - { - "sender_id": "id", - "sender_username": "xxx", - "receiver_id": "id", - "receiver_username": "xxx", - "status": "status" - } -] -``` -- `http://localhost:3000/user//reject//` "Accept or reject friend request using PUT method" -- `http://localhost:3000/user//friends//remove/` "Remove friend using DELETE method" - - -The API will store the username, email and hashed password in the User table. -The username and email are unique. - -The User table consists of the following fields: -You can find it in user_management/user_management/users/models.py - -| Field Name | Data Type | Description | -| --------------- | --------- | ---------------------------------- | -| id | Integer | Primary Key | -| username | String | User Name | -| email | String | User Email | -| password | String | User Password (Password is hashed) | -| friends | ManyToMany| Friends of the user | -| avatar | Image | User Avatar | -| otp_status | Boolean | OTP Status | -| otp | Integer | OTP | -| otp_expiry_time | DateTime | OTP Expiry Time | - -Later I will limit the access to the API using nginx reverse proxy and only the frontend will be able to access the API. - -## WebSocket Integration Documentation - -### Overview - -This document provides an overview of how WebSocket integration has been implemented in the Django project, enabling real-time online status updates and notifications. It includes information on the setup, testing, and how the frontend can access the WebSocket services. - -### Backend Setup - -## GameRoom model -The GameRoom model is used to store the game room information. The GameRoom model consists of the following fields: - -| Field Name | Data Type | Description | -| ---------- | --------------------------- | ----------- | -| room_name | String | Room Name | -| player1 | ForeignKey from UserProfile | Player 1 | -| player2 | ForeignKey from UserProfile | Player 2 | - -#### Dependencies - -Django Channels and Redis were installed: - -```bash -pip install channels -pip install channels-redis -``` - -#### Project Configuration - -- **`settings.py`:** - - `channels` was added to `INSTALLED_APPS` and the channel layers were configured with Redis: - - ```python - INSTALLED_APPS = [ - # other apps - 'channels', - ] - - ASGI_APPLICATION = 'your_project_name.asgi.application' - - CHANNEL_LAYERS = { - "default": { - "BACKEND": "channels_redis.pubsub.RedisPubSubChannelLayer", - "CONFIG": { - "hosts": [{ - "address": "redis://redis:6379", - "ssl_cert_reqs": None, - }], - }, - }, - } - ``` - -- **`asgi.py`:** - - `asgi.py` was set up to handle WebSocket connections: - - ```python - import os - from django.core.asgi import get_asgi_application - from channels.routing import ProtocolTypeRouter, URLRouter - from channels.auth import AuthMiddlewareStack - import user_app.routing - - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'your_project_name.settings') - - application = ProtocolTypeRouter({ - 'http': get_asgi_application(), - 'websocket': AuthMiddlewareStack( - URLRouter( - user_app.routing.websocket_urlpatterns - ) - ) - }) - ``` - -- **`routing.py`:** - - WebSocket URL patterns were defined: - - ```python - from django.urls import re_path - from . import consumers - - websocket_urlpatterns = [ - re_path(r'ws/online/', consumers.OnlineStatusConsumer.as_asgi()), - re_path(r'ws/game/room/(?P\w+)/$', GameRoomConsumer.GameRoomConsumer.as_asgi()), - ] - ``` - -#### Nginx Configuration - -Nginx was set up to support WebSocket connections. The `nginx.conf` was updated: - -```nginx -map $http_upgrade $connection_upgrade { - default upgrade; - '' close; -} - -server { - listen 443 ssl; - # other configurations - - location /ws/ { - proxy_pass http://websocket-service; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} -``` - -#### Docker Configuration - -The Docker setup included Redis service: - -```yaml -services: - redis: - image: redis - container_name: redis - networks: - - transcendence_network - -### Testing WebSocket Connections - -#### Using Postman - -1. **Connect to WebSocket:** - - - Open Postman. - - Select "New" -> "WebSocket Request". - - Enter the WebSocket URL: `ws://127.0.0.1:3000/ws/online/`. - -2. **Send Messages:** - - - Use the JSON format to send messages: - ```json - { - "username": "example_user", - "type": "open" - } - ``` - - To close the connection: - ```json - { - "username": "example_user", - "type": "close" - } - ``` - -### Frontend Access - -To integrate WebSocket connections in the frontend, follow these steps: - -1. **Connect to WebSocket:** - - Use JavaScript to create a WebSocket connection: - - ```javascript - const socket = new WebSocket('ws://127.0.0.1:3000/ws/online/'); - - socket.onopen = function(event) { - console.log('WebSocket is connected.'); - socket.send(JSON.stringify({ - username: 'example_user', - type: 'open' - })); - }; - - socket.onmessage = function(event) { - const data = JSON.parse(event.data); - console.log('Message from server ', data); - }; - - socket.onclose = function(event) { - console.log('WebSocket is closed now.'); - }; - - socket.onerror = function(error) { - console.log('WebSocket Error: ' + error); - }; - ``` - -2. **Handling WebSocket Events:** - - Handle WebSocket events to update the frontend UI accordingly. For example: - - ```javascript - socket.onmessage = function(event) { - const data = JSON.parse(event.data); - if (data.online_status !== undefined) { - updateUserStatus(data.username, data.online_status); - } - }; - - function updateUserStatus(username, status) { - const userElement = document.getElementById(username); - if (userElement) { - userElement.className = status ? 'online' : 'offline'; - } - } - ``` - -### Summary - -The setup included configuring Django Channels, Redis, and Nginx to support WebSocket connections. The frontend can connect to the WebSocket service and handle events to provide real-time updates to users. - - From d1ad5c1cb31819cb0a99ddafe7ccdc1e31dc66dd Mon Sep 17 00:00:00 2001 From: Pooria Toof Date: Tue, 20 Aug 2024 20:23:40 +0300 Subject: [PATCH 14/22] fixed first condition of available_username_email funtion from "and" to or\ --- Backend/user_service/user_service/user_app/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Backend/user_service/user_service/user_app/views.py b/Backend/user_service/user_service/user_app/views.py index 797a41f..e0af73f 100644 --- a/Backend/user_service/user_service/user_app/views.py +++ b/Backend/user_service/user_service/user_app/views.py @@ -259,7 +259,7 @@ def available_username_email(self, request) -> Response: username = request.data.get("username") response_message = {} status_code = status.HTTP_200_OK - if username is not None and email is not None: + if username is not None or email is not None: user_obj = UserProfileModel.objects.filter(username = username).first() email_obj = UserProfileModel.objects.filter(email = email).first() if user_obj is not None and email_obj is not None: From 6ac3e01e871c3435ec9b14a297b502cf4b70ca7e Mon Sep 17 00:00:00 2001 From: mtoof Date: Tue, 20 Aug 2024 21:54:31 +0300 Subject: [PATCH 15/22] added specific response messages for username and email in lines 275-280 --- Backend/user_service/user_service/user_app/views.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Backend/user_service/user_service/user_app/views.py b/Backend/user_service/user_service/user_app/views.py index e0af73f..9358436 100644 --- a/Backend/user_service/user_service/user_app/views.py +++ b/Backend/user_service/user_service/user_app/views.py @@ -272,7 +272,12 @@ def available_username_email(self, request) -> Response: response_message = {"error": "email is not available"} status_code = status.HTTP_400_BAD_REQUEST else: - response_message = {"detail": "username and email are available"} + if email is not None and username is not None: + response_message = {"detail": "username and email are available"} + elif username is not None: + response_message = {"detail": "username is available"} + elif email is not None: + response_message = {"detail": "email is available"} else: response_message = {'error':"username and email fields are required"} status_code = status.HTTP_400_BAD_REQUEST From 10cfcd7beb84ad40def9fc4cdd2f9b7409ab6b34 Mon Sep 17 00:00:00 2001 From: Abbas Toof Date: Wed, 21 Aug 2024 21:21:50 +0300 Subject: [PATCH 16/22] added test_user_register and test_users_list to the test_user_service.py --- .../user_service/test/test_user_service.py | 322 ++++++++++-------- 1 file changed, 172 insertions(+), 150 deletions(-) diff --git a/Backend/user_service/user_service/user_service/test/test_user_service.py b/Backend/user_service/user_service/user_service/test/test_user_service.py index 251c3be..84d1a3f 100644 --- a/Backend/user_service/user_service/user_service/test/test_user_service.py +++ b/Backend/user_service/user_service/user_service/test/test_user_service.py @@ -6,7 +6,8 @@ from user_app.views import UserViewSet, RegisterViewSet, FriendsViewSet, validate_token from user_app.user_session_views import UserLoginView, UserLogoutView from rest_framework import status -from user_app.models import UserProfileModel +from user_app.models import UserProfileModel, ConfirmEmail +from user_app.serializers import UserSerializer import os from dotenv import load_dotenv @@ -27,13 +28,26 @@ def user_data(): 'password': 'Test@123' } +def admin_data(): + return { + 'id': 1, + 'username': 'adminuser', + 'email': 'adminuser@123.com', + 'password': 'Admin@123' + } + @pytest.fixture def user(db): - return UserProfileModel.objects.create_user(username='testuser', email='testuser@123.com', password='Test@123') + email_obj = ConfirmEmail.objects.create(user_email = 'testuser@123.com', verify_status = True) + return UserProfileModel.objects.create(username='testuser', email=email_obj.pk, password='Test@123') @pytest.fixture def admin_user(db): - return UserProfileModel.objects.create_superuser(username='admin', email='admin@123.com', password='Admin@123') + admin_email = ConfirmEmail.objects.create(user_email = 'admintest@123.com', verify_status = True) + user_obj = UserSerializer(data = {"username":'adminuser', "email":admin_email.pk, "password":'Admin@123'}) + user_obj.is_valid(raise_exception=True) + user_obj.save() + return UserProfileModel.objects.get(username='adminuser') @pytest.fixture def user_token(user): @@ -52,7 +66,9 @@ def mock_validate_token(): @pytest.mark.django_db def test_user_register(api_client, user_data): + email_obj = ConfirmEmail.objects.create(user_email = 'testuser@123.com', verify_status = True) url = reverse('user-register') + user_data["email"] = email_obj.pk response = api_client.post(url, user_data, format='json') assert response.status_code == 201 assert response.data['id'] == 1 @@ -60,12 +76,18 @@ def test_user_register(api_client, user_data): assert response.data['email'] == 'testuser@123.com' @pytest.mark.django_db -def test_users_list(api_client, admin_user, admin_token, user_data): +def test_users_list(api_client, admin_user, admin_token): # Mock the RabbitMQ interactions - user1 = UserProfileModel.objects.create_user(username='testuser1',email='testuser1@123.com',password='Test@123') - user2 = UserProfileModel.objects.create_user(username='testuser2',email='testuser2@123.com',password='Test@123') + email1_obj = ConfirmEmail.objects.create(user_email = 'testuser1@123.com', verify_status = True) + user1 = UserSerializer(data = {'username':'testuser1','email' : email1_obj.pk,'password':'Test@123'}) + user1.is_valid(raise_exception=True) + user1.save() + email2_obj = ConfirmEmail.objects.create(user_email = 'testuser2@123.com', verify_status = True) + user2 = UserSerializer(data = {'username':'testuser2','email' : email2_obj.pk,'password':'Test@123'}) + user2.is_valid(raise_exception=True) + user2.save() - # Authenticate the request + # Authenticate the request token = admin_token api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {token}') @@ -77,146 +99,146 @@ def test_users_list(api_client, admin_user, admin_token, user_data): print("response data=", response.data) assert response.status_code == status.HTTP_200_OK -@pytest.mark.django_db -def test_user_login(api_client, admin_user): - data = { - "username":"admin", - "password":"Admin@123" - } - url = reverse("user-login") - response = api_client.post(url, data, format='json') - print("response_data", response.data) - assert response.status_code == 200 - -@pytest.mark.django_db -@patch('user_app.user_session_views.requests.post') -def test_user_logout(mock_post, api_client, admin_user, admin_token): - # Authenticate the request - mock_post.return_value.status_code = 200 - mock_post.return_value.json.return_value = {"detail": "User logged out successfully"} - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {admin_token}') - - url = reverse('user-logout', kwargs={'pk': admin_user.id}) - response = api_client.post(url) - assert response.status_code == 200 - assert response.data["detail"] == 'User logged out successfully' - mock_post.assert_called_once_with( - f"{TOEKNSERVICE}/auth/token/invalidate-tokens/", - data={"access": admin_token, 'id':admin_user.id} - ) - assert UserProfileModel.objects.filter(username=admin_user.username).exists() - -@pytest.mark.django_db -def test_retrieve_user(api_client, user, user_token): - # Authenticate the request - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - - url = reverse('user-detail', kwargs={'pk': user.id}) - response = api_client.get(url) - - assert response.status_code == status.HTTP_200_OK - assert response.data['id'] == user.id - assert response.data['username'] == user.username - assert response.data['email'] == user.email - -@pytest.mark.django_db -def test_update_user(api_client, user, user_token): - data = { - "username": "newuser", - "email": "newuser@123.com" - } - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - - url = reverse('user-detail', kwargs={'pk': user.id}) - response = api_client.patch(url, data, format='json') - - assert response.status_code == status.HTTP_202_ACCEPTED - assert response.data['id'] == user.id - assert response.data['username'] == 'newuser' - assert response.data['email'] == 'newuser@123.com' - -def test_destroy_user(api_client, user, user_token): - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - - url = reverse('user-detail', kwargs={'pk': user.id}) # Get the URL for the user object to be deleted - response = api_client.delete(url) # Call the API endpoint to delete the user object and assert the response status code - - assert response.status_code == status.HTTP_204_NO_CONTENT - assert not UserProfileModel.objects.filter(username=user.username).exists() - -@pytest.mark.django_db -def test_valid_data_friend_request_functions(api_client, admin_user, user, user_token, admin_token): - - print("\ntestuser sends a friend request to admin user") - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - url_request = reverse('send-request', kwargs={'user_pk': user.id}) - response_request = api_client.post(url_request, data={'username': 'admin'}, format='json') - assert response_request.status_code == status.HTTP_201_CREATED - assert response_request.data["detail"]=='Friend request sent' - - print("\nAdmin check the friend requests list") - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - url_request = reverse('friend-request-list', kwargs={'user_pk': admin_user.id}) - response_request = api_client.get(url_request, format='json') - assert response_request.data[0]["sender_username"] == 'testuser' - assert response_request.data[0]["receiver_username"] == 'admin' - assert response_request.data[0]["status"] == 'pending' - assert response_request.status_code == status.HTTP_200_OK - - print("\nAdmin accept the friend request") - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - url_request = reverse('accept-request', kwargs={'user_pk': admin_user.id, 'pk':user.id}) - response_request = api_client.put(url_request, format='json') - assert response_request.data["detail"]=='Request accepted' - assert response_request.status_code == status.HTTP_202_ACCEPTED - - print("\nAdmin user has testuser in its friends list") - url_request = reverse('friends-list', kwargs={'user_pk': admin_user.id}) - response_request = api_client.get(url_request, format='json') - assert response_request.data[0]["username"] == "testuser" - - print("\ntest user has admin in its friends list") - url_request = reverse('friends-list', kwargs={'user_pk': user.id}) - response_request = api_client.get(url_request, format='json') - assert response_request.data[0]["username"] == "admin" - assert response_request.status_code == 200 - - print("\ntestuser delete the admin user from its friends list") - url_request = reverse('remove-friend', kwargs={'user_pk': user.id, 'pk': admin_user.id}) - response_request = api_client.delete(url_request, format='json') - assert response_request.status_code == status.HTTP_204_NO_CONTENT - -@pytest.mark.django_db -def test_send_friend_request_invalid_user_id(api_client, admin_user, user, user_token, admin_token): - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - - url_request = reverse('send-request', kwargs={'user_pk': user.id}) - response_request = api_client.post(url_request, data={'username':'invalid_user'}, format='json') - assert response_request.status_code == 404 - assert response_request.data["error"]=="User does not exist" - -@pytest.mark.django_db -def test_reject_friend_request(api_client, admin_user, user, user_token, admin_token): - - print("\ntestuser sends a friend request to admin user") - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - url_request = reverse('send-request', kwargs={'user_pk': user.id}) - response_request = api_client.post(url_request, {'username': 'admin'}, format='json') - assert response_request.status_code == status.HTTP_201_CREATED - assert response_request.data["detail"]=='Friend request sent' - - print("\nAdmin check the friend requests list") - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - url_request = reverse('friend-request-list', kwargs={'user_pk': admin_user.id}) - response_request = api_client.get(url_request, format='json') - assert response_request.data[0]["sender_username"] == 'testuser' - assert response_request.data[0]["receiver_username"] == 'admin' - assert response_request.data[0]["status"] == 'pending' - assert response_request.status_code == status.HTTP_200_OK - - print("Admin reject the testuser request") - api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - url_request = reverse('reject-request', kwargs={'user_pk': admin_user.id,'pk':user.id}) - response_request = api_client.put(url_request, format='json') - assert response_request.data["detail"] == "Request rejected" - assert response_request.status_code == status.HTTP_202_ACCEPTED +# @pytest.mark.django_db +# def test_user_login(api_client, admin_user): +# data = { +# "username":"admin", +# "password":"Admin@123" +# } +# url = reverse("user-login") +# response = api_client.post(url, data, format='json') +# print("response_data", response.data) +# assert response.status_code == 200 + +# @pytest.mark.django_db +# @patch('user_app.user_session_views.requests.post') +# def test_user_logout(mock_post, api_client, admin_user, admin_token): +# # Authenticate the request +# mock_post.return_value.status_code = 200 +# mock_post.return_value.json.return_value = {"detail": "User logged out successfully"} +# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {admin_token}') + +# url = reverse('user-logout', kwargs={'pk': admin_user.id}) +# response = api_client.post(url) +# assert response.status_code == 200 +# assert response.data["detail"] == 'User logged out successfully' +# mock_post.assert_called_once_with( +# f"{TOEKNSERVICE}/auth/token/invalidate-tokens/", +# data={"access": admin_token, 'id':admin_user.id} +# ) +# assert UserProfileModel.objects.filter(username=admin_user.username).exists() + +# @pytest.mark.django_db +# def test_retrieve_user(api_client, user, user_token): +# # Authenticate the request +# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + +# url = reverse('user-detail', kwargs={'pk': user.id}) +# response = api_client.get(url) + +# assert response.status_code == status.HTTP_200_OK +# assert response.data['id'] == user.id +# assert response.data['username'] == user.username +# assert response.data['email'] == user.email + +# @pytest.mark.django_db +# def test_update_user(api_client, user, user_token): +# data = { +# "username": "newuser", +# "email": "newuser@123.com" +# } +# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + +# url = reverse('user-detail', kwargs={'pk': user.id}) +# response = api_client.patch(url, data, format='json') + +# assert response.status_code == status.HTTP_202_ACCEPTED +# assert response.data['id'] == user.id +# assert response.data['username'] == 'newuser' +# assert response.data['email'] == 'newuser@123.com' + +# def test_destroy_user(api_client, user, user_token): +# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + +# url = reverse('user-detail', kwargs={'pk': user.id}) # Get the URL for the user object to be deleted +# response = api_client.delete(url) # Call the API endpoint to delete the user object and assert the response status code + +# assert response.status_code == status.HTTP_204_NO_CONTENT +# assert not UserProfileModel.objects.filter(username=user.username).exists() + +# @pytest.mark.django_db +# def test_valid_data_friend_request_functions(api_client, admin_user, user, user_token, admin_token): + +# print("\ntestuser sends a friend request to admin user") +# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') +# url_request = reverse('send-request', kwargs={'user_pk': user.id}) +# response_request = api_client.post(url_request, data={'username': 'admin'}, format='json') +# assert response_request.status_code == status.HTTP_201_CREATED +# assert response_request.data["detail"]=='Friend request sent' + +# print("\nAdmin check the friend requests list") +# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') +# url_request = reverse('friend-request-list', kwargs={'user_pk': admin_user.id}) +# response_request = api_client.get(url_request, format='json') +# assert response_request.data[0]["sender_username"] == 'testuser' +# assert response_request.data[0]["receiver_username"] == 'admin' +# assert response_request.data[0]["status"] == 'pending' +# assert response_request.status_code == status.HTTP_200_OK + +# print("\nAdmin accept the friend request") +# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') +# url_request = reverse('accept-request', kwargs={'user_pk': admin_user.id, 'pk':user.id}) +# response_request = api_client.put(url_request, format='json') +# assert response_request.data["detail"]=='Request accepted' +# assert response_request.status_code == status.HTTP_202_ACCEPTED + +# print("\nAdmin user has testuser in its friends list") +# url_request = reverse('friends-list', kwargs={'user_pk': admin_user.id}) +# response_request = api_client.get(url_request, format='json') +# assert response_request.data[0]["username"] == "testuser" + +# print("\ntest user has admin in its friends list") +# url_request = reverse('friends-list', kwargs={'user_pk': user.id}) +# response_request = api_client.get(url_request, format='json') +# assert response_request.data[0]["username"] == "admin" +# assert response_request.status_code == 200 + +# print("\ntestuser delete the admin user from its friends list") +# url_request = reverse('remove-friend', kwargs={'user_pk': user.id, 'pk': admin_user.id}) +# response_request = api_client.delete(url_request, format='json') +# assert response_request.status_code == status.HTTP_204_NO_CONTENT + +# @pytest.mark.django_db +# def test_send_friend_request_invalid_user_id(api_client, admin_user, user, user_token, admin_token): +# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + +# url_request = reverse('send-request', kwargs={'user_pk': user.id}) +# response_request = api_client.post(url_request, data={'username':'invalid_user'}, format='json') +# assert response_request.status_code == 404 +# assert response_request.data["error"]=="User does not exist" + +# @pytest.mark.django_db +# def test_reject_friend_request(api_client, admin_user, user, user_token, admin_token): + +# print("\ntestuser sends a friend request to admin user") +# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') +# url_request = reverse('send-request', kwargs={'user_pk': user.id}) +# response_request = api_client.post(url_request, {'username': 'admin'}, format='json') +# assert response_request.status_code == status.HTTP_201_CREATED +# assert response_request.data["detail"]=='Friend request sent' + +# print("\nAdmin check the friend requests list") +# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') +# url_request = reverse('friend-request-list', kwargs={'user_pk': admin_user.id}) +# response_request = api_client.get(url_request, format='json') +# assert response_request.data[0]["sender_username"] == 'testuser' +# assert response_request.data[0]["receiver_username"] == 'admin' +# assert response_request.data[0]["status"] == 'pending' +# assert response_request.status_code == status.HTTP_200_OK + +# print("Admin reject the testuser request") +# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') +# url_request = reverse('reject-request', kwargs={'user_pk': admin_user.id,'pk':user.id}) +# response_request = api_client.put(url_request, format='json') +# assert response_request.data["detail"] == "Request rejected" +# assert response_request.status_code == status.HTTP_202_ACCEPTED From 107061691cba1d6fc5d32ee9ac012ad2fc33f9a1 Mon Sep 17 00:00:00 2001 From: Pooria Toof Date: Wed, 21 Aug 2024 23:49:35 +0300 Subject: [PATCH 17/22] added handle_email to user_update and delete the prev email after updating the email to new email --- .../user_service/user_app/views.py | 40 ++++++++++++++++--- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/Backend/user_service/user_service/user_app/views.py b/Backend/user_service/user_service/user_app/views.py index 9358436..05d1ce1 100644 --- a/Backend/user_service/user_service/user_app/views.py +++ b/Backend/user_service/user_service/user_app/views.py @@ -131,14 +131,25 @@ def update_user(self, request, pk=None) -> Response: Response: The response object containing the updated user data. """ try: + response_message = {} + status_code = status.HTTP_200_OK validate_token(request) - data = get_object_or_404(UserProfileModel, id=pk) - if data != request.user and not request.user.is_superuser: + user_obj = get_object_or_404(UserProfileModel, id=pk) + if user_obj != request.user and not request.user.is_superuser: return Response(status=status.HTTP_401_UNAUTHORIZED) - serializer = UserSerializer(instance=data, data=request.data, partial=True) - serializer.is_valid(raise_exception=True) - serializer.save() - return Response(serializer.data, status=status.HTTP_202_ACCEPTED) + data = request.data + current_email = user_obj.email + if "email" in data: + response_message, status_code = self.handle_email(data, user_obj) + if not response_message: + serializer = UserSerializer(instance=user_obj, data=data, partial=True) + serializer.is_valid(raise_exception=True) + serializer.save() + if serializer.data["email"] != current_email.user_email: + current_email.delete() + response_message = serializer.data + status_code = status.HTTP_202_ACCEPTED + return Response(response_message, status=status_code) except ValidationError as err: item_lists = [] for item in err.detail: @@ -146,6 +157,23 @@ def update_user(self, request, pk=None) -> Response: return Response({'error': item_lists}, status=status.HTTP_400_BAD_REQUEST) except Exception as err: return Response({"error": str(err)}, status=status.HTTP_400_BAD_REQUEST) + + def handle_email(self, data, user_obj): + response_message = {} + status_code = status.HTTP_200_OK + new_email = data["email"] + new_email_obj = ConfirmEmail.objects.filter(user_email=new_email).first() + if new_email_obj and new_email_obj.verify_status: + existing_user = UserProfileModel.objects.filter(email=new_email).exclude(id=user_obj.id).first() + if existing_user: + response_message = {"error": "Email already exists"} + status_code = status.HTTP_400_BAD_REQUEST + else: + data["email"] = new_email_obj.pk + else: + response_message = {"error": "You have not confirmed your email yet!"}, + status_code=status.HTTP_401_UNAUTHORIZED + return response_message, status_code def destroy_user(self, request, pk=None) -> Response: """ From 0890e1ae74e4a53041af4fe32e28fd3a0e6b77b9 Mon Sep 17 00:00:00 2001 From: Pooria Toof Date: Thu, 22 Aug 2024 14:05:25 +0300 Subject: [PATCH 18/22] fixed all tests for user-service --- .../user_service/test/test_user_service.py | 297 +++++++++--------- 1 file changed, 153 insertions(+), 144 deletions(-) diff --git a/Backend/user_service/user_service/user_service/test/test_user_service.py b/Backend/user_service/user_service/user_service/test/test_user_service.py index 84d1a3f..799ab16 100644 --- a/Backend/user_service/user_service/user_service/test/test_user_service.py +++ b/Backend/user_service/user_service/user_service/test/test_user_service.py @@ -10,6 +10,11 @@ from user_app.serializers import UserSerializer import os from dotenv import load_dotenv +from django.conf import settings + +headers = { + "X-SERVICE-SECRET": settings.SECRET_KEY +} load_dotenv() TOEKNSERVICE = os.environ.get('TOKEN_SERVICE') @@ -39,7 +44,10 @@ def admin_data(): @pytest.fixture def user(db): email_obj = ConfirmEmail.objects.create(user_email = 'testuser@123.com', verify_status = True) - return UserProfileModel.objects.create(username='testuser', email=email_obj.pk, password='Test@123') + user_serializer = UserSerializer(data = {"username":'testuser', "email":email_obj.pk, "password":'Test@123'}) + user_serializer.is_valid(raise_exception=True) + user_serializer.save() + return UserProfileModel.objects.get(username='testuser') @pytest.fixture def admin_user(db): @@ -99,146 +107,147 @@ def test_users_list(api_client, admin_user, admin_token): print("response data=", response.data) assert response.status_code == status.HTTP_200_OK -# @pytest.mark.django_db -# def test_user_login(api_client, admin_user): -# data = { -# "username":"admin", -# "password":"Admin@123" -# } -# url = reverse("user-login") -# response = api_client.post(url, data, format='json') -# print("response_data", response.data) -# assert response.status_code == 200 - -# @pytest.mark.django_db -# @patch('user_app.user_session_views.requests.post') -# def test_user_logout(mock_post, api_client, admin_user, admin_token): -# # Authenticate the request -# mock_post.return_value.status_code = 200 -# mock_post.return_value.json.return_value = {"detail": "User logged out successfully"} -# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {admin_token}') - -# url = reverse('user-logout', kwargs={'pk': admin_user.id}) -# response = api_client.post(url) -# assert response.status_code == 200 -# assert response.data["detail"] == 'User logged out successfully' -# mock_post.assert_called_once_with( -# f"{TOEKNSERVICE}/auth/token/invalidate-tokens/", -# data={"access": admin_token, 'id':admin_user.id} -# ) -# assert UserProfileModel.objects.filter(username=admin_user.username).exists() - -# @pytest.mark.django_db -# def test_retrieve_user(api_client, user, user_token): -# # Authenticate the request -# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - -# url = reverse('user-detail', kwargs={'pk': user.id}) -# response = api_client.get(url) - -# assert response.status_code == status.HTTP_200_OK -# assert response.data['id'] == user.id -# assert response.data['username'] == user.username -# assert response.data['email'] == user.email - -# @pytest.mark.django_db -# def test_update_user(api_client, user, user_token): -# data = { -# "username": "newuser", -# "email": "newuser@123.com" -# } -# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - -# url = reverse('user-detail', kwargs={'pk': user.id}) -# response = api_client.patch(url, data, format='json') - -# assert response.status_code == status.HTTP_202_ACCEPTED -# assert response.data['id'] == user.id -# assert response.data['username'] == 'newuser' -# assert response.data['email'] == 'newuser@123.com' - -# def test_destroy_user(api_client, user, user_token): -# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - -# url = reverse('user-detail', kwargs={'pk': user.id}) # Get the URL for the user object to be deleted -# response = api_client.delete(url) # Call the API endpoint to delete the user object and assert the response status code - -# assert response.status_code == status.HTTP_204_NO_CONTENT -# assert not UserProfileModel.objects.filter(username=user.username).exists() - -# @pytest.mark.django_db -# def test_valid_data_friend_request_functions(api_client, admin_user, user, user_token, admin_token): - -# print("\ntestuser sends a friend request to admin user") -# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') -# url_request = reverse('send-request', kwargs={'user_pk': user.id}) -# response_request = api_client.post(url_request, data={'username': 'admin'}, format='json') -# assert response_request.status_code == status.HTTP_201_CREATED -# assert response_request.data["detail"]=='Friend request sent' - -# print("\nAdmin check the friend requests list") -# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') -# url_request = reverse('friend-request-list', kwargs={'user_pk': admin_user.id}) -# response_request = api_client.get(url_request, format='json') -# assert response_request.data[0]["sender_username"] == 'testuser' -# assert response_request.data[0]["receiver_username"] == 'admin' -# assert response_request.data[0]["status"] == 'pending' -# assert response_request.status_code == status.HTTP_200_OK - -# print("\nAdmin accept the friend request") -# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') -# url_request = reverse('accept-request', kwargs={'user_pk': admin_user.id, 'pk':user.id}) -# response_request = api_client.put(url_request, format='json') -# assert response_request.data["detail"]=='Request accepted' -# assert response_request.status_code == status.HTTP_202_ACCEPTED - -# print("\nAdmin user has testuser in its friends list") -# url_request = reverse('friends-list', kwargs={'user_pk': admin_user.id}) -# response_request = api_client.get(url_request, format='json') -# assert response_request.data[0]["username"] == "testuser" - -# print("\ntest user has admin in its friends list") -# url_request = reverse('friends-list', kwargs={'user_pk': user.id}) -# response_request = api_client.get(url_request, format='json') -# assert response_request.data[0]["username"] == "admin" -# assert response_request.status_code == 200 - -# print("\ntestuser delete the admin user from its friends list") -# url_request = reverse('remove-friend', kwargs={'user_pk': user.id, 'pk': admin_user.id}) -# response_request = api_client.delete(url_request, format='json') -# assert response_request.status_code == status.HTTP_204_NO_CONTENT - -# @pytest.mark.django_db -# def test_send_friend_request_invalid_user_id(api_client, admin_user, user, user_token, admin_token): -# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') - -# url_request = reverse('send-request', kwargs={'user_pk': user.id}) -# response_request = api_client.post(url_request, data={'username':'invalid_user'}, format='json') -# assert response_request.status_code == 404 -# assert response_request.data["error"]=="User does not exist" - -# @pytest.mark.django_db -# def test_reject_friend_request(api_client, admin_user, user, user_token, admin_token): - -# print("\ntestuser sends a friend request to admin user") -# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') -# url_request = reverse('send-request', kwargs={'user_pk': user.id}) -# response_request = api_client.post(url_request, {'username': 'admin'}, format='json') -# assert response_request.status_code == status.HTTP_201_CREATED -# assert response_request.data["detail"]=='Friend request sent' - -# print("\nAdmin check the friend requests list") -# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') -# url_request = reverse('friend-request-list', kwargs={'user_pk': admin_user.id}) -# response_request = api_client.get(url_request, format='json') -# assert response_request.data[0]["sender_username"] == 'testuser' -# assert response_request.data[0]["receiver_username"] == 'admin' -# assert response_request.data[0]["status"] == 'pending' -# assert response_request.status_code == status.HTTP_200_OK - -# print("Admin reject the testuser request") -# api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') -# url_request = reverse('reject-request', kwargs={'user_pk': admin_user.id,'pk':user.id}) -# response_request = api_client.put(url_request, format='json') -# assert response_request.data["detail"] == "Request rejected" -# assert response_request.status_code == status.HTTP_202_ACCEPTED +@pytest.mark.django_db +def test_user_login(api_client, admin_user): + data = { + "username":"adminuser", + "password":"Admin@123" + } + url = reverse("user-login") + response = api_client.post(url, data, format='json') + print("response_data", response.data) + assert response.status_code == 200 + +@pytest.mark.django_db +@patch('user_app.user_session_views.requests.post') +def test_user_logout(mock_post, api_client, admin_user, admin_token): + # Authenticate the request + mock_post.return_value.status_code = 200 + mock_post.return_value.json.return_value = {"detail": "User logged out successfully"} + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {admin_token}') + + url = reverse('user-logout', kwargs={'pk': admin_user.id}) + response = api_client.post(url) + assert response.status_code == 200 + assert response.data["detail"] == 'User logged out successfully' + mock_post.assert_called_once_with( + f"{TOEKNSERVICE}/auth/token/invalidate-tokens/", + data={"access": admin_token, 'id':admin_user.id}, headers=headers + ) + assert UserProfileModel.objects.filter(username=admin_user.username).exists() + +@pytest.mark.django_db +def test_retrieve_user(api_client, user, user_token): + # Authenticate the request + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + + url = reverse('user-detail', kwargs={'pk': user.id}) + response = api_client.get(url) + + assert response.status_code == status.HTTP_200_OK + assert response.data['id'] == user.id + assert response.data['username'] == user.username + assert response.data['email'] == user.email.user_email + +@pytest.mark.django_db +def test_update_user(api_client, user, user_token): + email_obj = ConfirmEmail.objects.create(user_email = "newuser@123.com", verify_status=True) + data = { + "username": "newuser", + "email": email_obj.pk + } + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + + url = reverse('user-detail', kwargs={'pk': user.id}) + response = api_client.patch(url, data, format='json') + + assert response.status_code == status.HTTP_202_ACCEPTED + assert response.data['id'] == user.id + assert response.data['username'] == 'newuser' + assert response.data['email'] == 'newuser@123.com' + +def test_destroy_user(api_client, user, user_token): + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + + url = reverse('user-detail', kwargs={'pk': user.id}) # Get the URL for the user object to be deleted + response = api_client.delete(url) # Call the API endpoint to delete the user object and assert the response status code + + assert response.status_code == status.HTTP_204_NO_CONTENT + assert not UserProfileModel.objects.filter(username=user.username).exists() + +@pytest.mark.django_db +def test_valid_data_friend_request_functions(api_client, admin_user, user, user_token, admin_token): + + print("\ntestuser sends a friend request to admin user") + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + url_request = reverse('send-request', kwargs={'user_pk': user.id}) + response_request = api_client.post(url_request, data={'username': 'adminuser'}, format='json') + assert response_request.status_code == status.HTTP_201_CREATED + assert response_request.data["detail"]=='Friend request sent' + + print("\nAdmin check the friend requests list") + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + url_request = reverse('friend-request-list', kwargs={'user_pk': admin_user.id}) + response_request = api_client.get(url_request, format='json') + assert response_request.data[0]["sender_username"] == 'testuser' + assert response_request.data[0]["receiver_username"] == 'adminuser' + assert response_request.data[0]["status"] == 'pending' + assert response_request.status_code == status.HTTP_200_OK + + print("\nAdmin accept the friend request") + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + url_request = reverse('accept-request', kwargs={'user_pk': admin_user.id, 'pk':user.id}) + response_request = api_client.put(url_request, format='json') + assert response_request.data["detail"]=='Request accepted' + assert response_request.status_code == status.HTTP_202_ACCEPTED + + print("\nAdmin user has testuser in its friends list") + url_request = reverse('friends-list', kwargs={'user_pk': admin_user.id}) + response_request = api_client.get(url_request, format='json') + assert response_request.data[0]["username"] == "testuser" + + print("\ntest user has admin in its friends list") + url_request = reverse('friends-list', kwargs={'user_pk': user.id}) + response_request = api_client.get(url_request, format='json') + assert response_request.data[0]["username"] == "adminuser" + assert response_request.status_code == 200 + + print("\ntestuser delete the admin user from its friends list") + url_request = reverse('remove-friend', kwargs={'user_pk': user.id, 'pk': admin_user.id}) + response_request = api_client.delete(url_request, format='json') + assert response_request.status_code == status.HTTP_204_NO_CONTENT + +@pytest.mark.django_db +def test_send_friend_request_invalid_user_id(api_client, admin_user, user, user_token, admin_token): + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + + url_request = reverse('send-request', kwargs={'user_pk': user.id}) + response_request = api_client.post(url_request, data={'username':'invalid_user'}, format='json') + assert response_request.status_code == 404 + assert response_request.data["error"]=="User does not exist" + +@pytest.mark.django_db +def test_reject_friend_request(api_client, admin_user, user, user_token, admin_token): + + print("\ntestuser sends a friend request to admin user") + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + url_request = reverse('send-request', kwargs={'user_pk': user.id}) + response_request = api_client.post(url_request, {'username': 'adminuser'}, format='json') + assert response_request.status_code == status.HTTP_201_CREATED + assert response_request.data["detail"]=='Friend request sent' + + print("\nAdmin check the friend requests list") + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + url_request = reverse('friend-request-list', kwargs={'user_pk': admin_user.id}) + response_request = api_client.get(url_request, format='json') + assert response_request.data[0]["sender_username"] == 'testuser' + assert response_request.data[0]["receiver_username"] == 'adminuser' + assert response_request.data[0]["status"] == 'pending' + assert response_request.status_code == status.HTTP_200_OK + + print("Admin reject the testuser request") + api_client.credentials(HTTP_AUTHORIZATION=f'Bearer {user_token}') + url_request = reverse('reject-request', kwargs={'user_pk': admin_user.id,'pk':user.id}) + response_request = api_client.put(url_request, format='json') + assert response_request.data["detail"] == "Request rejected" + assert response_request.status_code == status.HTTP_202_ACCEPTED From 934c20344f6cd11bd1e521dceb14007bdcbc8cd4 Mon Sep 17 00:00:00 2001 From: Pooria Toof Date: Thu, 22 Aug 2024 14:43:02 +0300 Subject: [PATCH 19/22] feat(token_service): add user ID validation and token refresh logic - Added validation to ensure user ID is provided in the request. - Implemented retrieval of using the provided user ID. - Handled errors for missing user or token data. - Updated token data after refreshing access token. --- Backend/token_service/token_service/token_app/views.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Backend/token_service/token_service/token_app/views.py b/Backend/token_service/token_service/token_app/views.py index 2c06798..eb82d1a 100644 --- a/Backend/token_service/token_service/token_app/views.py +++ b/Backend/token_service/token_service/token_app/views.py @@ -124,9 +124,19 @@ def post(self, request, *args, **kwargs) -> Response: {"error": "Refresh token is required"}, status=status.HTTP_400_BAD_REQUEST ) try: + user_id = request.data.get("id") + if not user_id: + return Response({"error": "User id is required"}, status=status.HTTP_400_BAD_REQUEST) + user_object = UserTokens.objects.filter(id=user_id) + if not user_object: + return Response({"error": "User not found"}, status=status.HTTP_404_NOT_FOUND) + token_data = user_object.token_data refresh_token = bearer.split(' ')[1] refresh = RefreshToken(refresh_token) access_token = str(refresh.access_token) + token_data["access"] = str(refresh.access_token) + user_object.token_data=token_data + user_object.save() return Response({"access": access_token}, status=status.HTTP_200_OK) except Exception as err: return Response({"error": "Could not generate access token", "details": str(err)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) From 82eecc64e3065383a4141e0946f9252089f6b651 Mon Sep 17 00:00:00 2001 From: Pooria Toof Date: Fri, 23 Aug 2024 13:32:18 +0300 Subject: [PATCH 20/22] modified validate_token_for_user in token-sevice to receive token from frontend --- Backend/token_service/token_service/token_app/views.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Backend/token_service/token_service/token_app/views.py b/Backend/token_service/token_service/token_app/views.py index eb82d1a..1faf4f4 100644 --- a/Backend/token_service/token_service/token_app/views.py +++ b/Backend/token_service/token_service/token_app/views.py @@ -168,7 +168,9 @@ def validate_token(self, access_token) -> bool: def validate_token_for_user(self, request, *args, **kwargs): response_message = {} status_code = status.HTTP_201_CREATED - response_message, status_code = check_secret(request, response_message, status_code) + isfrontend = request.data.get("is_frontend") + if isfrontend is None: + response_message, status_code = check_secret(request, response_message, status_code) if "error" not in response_message and status_code == status.HTTP_201_CREATED: try: status_code = status.HTTP_200_OK From 0019584a0803361052121a07dcbca66224212646 Mon Sep 17 00:00:00 2001 From: Pooria Toof Date: Fri, 23 Aug 2024 13:55:16 +0300 Subject: [PATCH 21/22] changed status_code in lines 170,174,213, 215 to 200_OK --- Backend/token_service/token_service/token_app/views.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Backend/token_service/token_service/token_app/views.py b/Backend/token_service/token_service/token_app/views.py index 1faf4f4..2f6b98a 100644 --- a/Backend/token_service/token_service/token_app/views.py +++ b/Backend/token_service/token_service/token_app/views.py @@ -167,11 +167,11 @@ def validate_token(self, access_token) -> bool: def validate_token_for_user(self, request, *args, **kwargs): response_message = {} - status_code = status.HTTP_201_CREATED + status_code = status.HTTP_200_OK isfrontend = request.data.get("is_frontend") if isfrontend is None: response_message, status_code = check_secret(request, response_message, status_code) - if "error" not in response_message and status_code == status.HTTP_201_CREATED: + if "error" not in response_message and status_code == status.HTTP_200_OK: try: status_code = status.HTTP_200_OK response_message = {} @@ -210,9 +210,9 @@ def validate_token_for_user(self, request, *args, **kwargs): class InvalidateToken(viewsets.ViewSet): def invalidate_token_for_user(self, request, *args, **kwargs) -> Response: response_message = {} - status_code = status.HTTP_201_CREATED + status_code = status.HTTP_200_OK response_message, status_code = check_secret(request, response_message, status_code) - if "error" not in response_message and status_code == status.HTTP_201_CREATED: + if "error" not in response_message and status_code == status.HTTP_200_OK: try: access = request.data.get("access") id = request.data.get("id") From 5a882558641c6bd34bc2589ebde2ed789416aad5 Mon Sep 17 00:00:00 2001 From: Pooria Toof Date: Fri, 23 Aug 2024 15:30:53 +0300 Subject: [PATCH 22/22] updated readme for verify-token and refresh-token --- Backend/token_service/README.md | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/Backend/token_service/README.md b/Backend/token_service/README.md index efd60c0..8cf6d96 100644 --- a/Backend/token_service/README.md +++ b/Backend/token_service/README.md @@ -16,11 +16,24 @@ The API runs on port 8000 and exposed to 8001. ## Tutorial to use the token_service There are three endpoints in the token_service. The endpoints are: -- `auth/token/refresh/` - This endpoint is used to refresh the access token. +- `auth/token/refresh/` - This endpoint is used to refresh the access token. to refresh the access token you need to send a request to this endpoint with the refresh token in the request body. The request will be like this: +```json +{ + "id": "user_id", + "refresh": "your refresh token" +} +``` + - `auth/token/gen-tokens/` - This endpoint is used to generate the refresh and access tokens. - `auth/token/invalidate-tokens/` - This endpoint is used by user-service logout or delete user to invalidate the refresh and access tokens. -- `auth/token/validate-token/` - This endpoint is used to validate the access token. - +- `auth/token/validate-token/` - This endpoint is used to validate the access token. If you send a request from frontend to this API your request will be like this: +```json +{ + "id": "user_id", + "access": "your access token", + "is_frontend": true +} +``` ## The UserTokens model The UserTokens model is used to store the refresh and access tokens. The UserTokens model has the following fields: