diff --git a/solution/pulse/pulse/settings.py b/solution/pulse/pulse/settings.py index 63e2699..db8fead 100644 --- a/solution/pulse/pulse/settings.py +++ b/solution/pulse/pulse/settings.py @@ -112,7 +112,12 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" REST_FRAMEWORK = { "DEFAULT_FILTER_BACKENDS": [ "django_filters.rest_framework.DjangoFilterBackend" - ] + ], + "DEFAULT_AUTHENTICATION_CLASSES": ( + "users.authentication.JWTAuthentication", + "rest_framework.authentication.SessionAuthentication", + "rest_framework.authentication.BasicAuthentication", + ), } APPEND_SLASH = False diff --git a/solution/pulse/users/authentication.py b/solution/pulse/users/authentication.py new file mode 100644 index 0000000..a460d73 --- /dev/null +++ b/solution/pulse/users/authentication.py @@ -0,0 +1,29 @@ +import jwt +from django.conf import settings +from rest_framework.authentication import BaseAuthentication +from rest_framework.exceptions import AuthenticationFailed + +from users.models import Profile + + +class JWTAuthentication(BaseAuthentication): + def authenticate(self, request): + token = request.headers.get("Authorization", "").split("Bearer ")[-1] + + if not token: + return None + + try: + payload = jwt.decode( + token, settings.SECRET_KEY, algorithms=["HS256"] + ) + + user = Profile.objects.get(login=payload["login"]) + + return (user, None) + except Profile.DoesNotExist: + raise AuthenticationFailed("Invalid token") + except jwt.ExpiredSignatureError: + raise AuthenticationFailed("Token has expired") + except jwt.InvalidTokenError: + raise AuthenticationFailed("Invalid token") diff --git a/solution/pulse/users/models.py b/solution/pulse/users/models.py index 217e351..ad878d7 100644 --- a/solution/pulse/users/models.py +++ b/solution/pulse/users/models.py @@ -34,5 +34,8 @@ class Profile(models.Model): ) image = models.URLField(max_length=200, blank=True, null=True) + def is_authenticated(self): + return True + def __str__(self): return self.login diff --git a/solution/pulse/users/urls.py b/solution/pulse/users/urls.py index cc4ddbf..6b643d9 100644 --- a/solution/pulse/users/urls.py +++ b/solution/pulse/users/urls.py @@ -5,7 +5,16 @@ import users.views urlpatterns = [ path( "register", - users.views.register_user, + users.views.RegisterUserApiView.as_view(), name="register", ), + path( + "sign-in", + users.views.SigninUserApiView.as_view(), + name="sign-in", + ), + path( + "protected-view", + users.views.ProtectedView.as_view(), + ) ] diff --git a/solution/pulse/users/views.py b/solution/pulse/users/views.py index add64ba..9c9ef99 100644 --- a/solution/pulse/users/views.py +++ b/solution/pulse/users/views.py @@ -1,9 +1,14 @@ import re +from datetime import timedelta import bcrypt +import jwt +from django.conf import settings +from django.utils import timezone from rest_framework import status -from rest_framework.decorators import api_view +from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response +from rest_framework.views import APIView from users.models import Profile from users.serializers import UserSerializer @@ -12,76 +17,117 @@ MIN_PASSWORD_LEN = 6 MAX_PASSWORD_LEN = 100 -@api_view(["POST"]) -def register_user(request): - serializer = UserSerializer(data=request.data) +class RegisterUserApiView(APIView): + def post(self, request): + serializer = UserSerializer(data=request.data) - if serializer.is_valid(): - if ( - Profile.objects.filter( - login=serializer.validated_data["login"] - ).first() - is not None - ): - return Response( - {"error": "User with this login already exists"}, - status=status.HTTP_409_CONFLICT, - ) - if ( - Profile.objects.filter( - email=serializer.validated_data["email"] - ).first() - is not None - ): - return Response( - {"error": "User with this email already exists"}, - status=status.HTTP_409_CONFLICT, - ) - if ( - Profile.objects.filter( - phone=serializer.validated_data["phone"] - ).first() - is not None - ): - return Response( - {"error": "User with this phone already exists"}, - status=status.HTTP_409_CONFLICT, + if serializer.is_valid(): + if ( + Profile.objects.filter( + login=serializer.validated_data["login"] + ).first() + is not None + ): + return Response( + {"error": "User with this login already exists"}, + status=status.HTTP_409_CONFLICT, + ) + if ( + Profile.objects.filter( + email=serializer.validated_data["email"] + ).first() + is not None + ): + return Response( + {"error": "User with this email already exists"}, + status=status.HTTP_409_CONFLICT, + ) + if ( + Profile.objects.filter( + phone=serializer.validated_data["phone"] + ).first() + is not None + ): + return Response( + {"error": "User with this phone already exists"}, + status=status.HTTP_409_CONFLICT, + ) + + password = serializer.validated_data["password"] + password_pattern = re.compile( + r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{6,100}$" ) - password = serializer.validated_data["password"] - password_pattern = re.compile( - r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{6,100}$" + if not (bool(re.match(password_pattern, password))): + error = { + "message": "Your password does not meet our requirements" + } + return Response( + error, + status=status.HTTP_400_BAD_REQUEST, + ) + + password_hash = bcrypt.hashpw( + password.encode("utf-8"), bcrypt.gensalt() + ).decode("utf-8") + serializer.validated_data["password"] = password_hash + + user = serializer.save() + + profile = { + "profile": { + "login": user.login, + "email": user.email, + "countryCode": user.countryCode, + "isPublic": user.isPublic, + } + } + if user.phone is not None: + profile["profile"]["phone"] = user.phone + if user.image is not None: + profile["profile"]["image"] = user.image + + return Response(profile, status=status.HTTP_201_CREATED) + + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class SigninUserApiView(APIView): + def post(self, request): + login = request.data.get("login") + password = request.data.get("password") + user = Profile.objects.filter(login=login).first() + + if user is not None: + if not bcrypt.checkpw( + password.encode("utf-8"), user.password.encode("utf-8") + ): + return Response( + {"error": "Invalid credentials"}, + status=status.HTTP_401_UNAUTHORIZED, + ) + else: + return Response( + {"error": "Invalid credentials"}, + status=status.HTTP_401_UNAUTHORIZED, + ) + + token = jwt.encode( + { + "login": login, + "password": password, + "exp": timezone.now() + timedelta(hours=24), + }, + settings.SECRET_KEY, + algorithm="HS256", ) - if not ( - bool(re.match(password_pattern, password)) - ): - error = {"message": "Your password does not meet our requirements"} - return Response( - error, - status=status.HTTP_400_BAD_REQUEST, - ) + return Response({"token": token}) - password_hash = bcrypt.hashpw( - password.encode("utf-8"), bcrypt.gensalt() - ).decode("utf-8") - serializer.validated_data["password"] = password_hash - user = serializer.save() +class ProtectedView(APIView): + permission_classes = [IsAuthenticated] - profile = { - "profile": { - "login": user.login, - "email": user.email, - "countryCode": user.countryCode, - "isPublic": user.isPublic, - } - } - if user.phone is not None: - profile["profile"]["phone"] = user.phone - if user.image is not None: - profile["profile"]["image"] = user.image - - return Response(profile, status=status.HTTP_201_CREATED) - - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def get(self, request): + user = request.user + return Response({"message": "Authenticated", "user": str(user)}) diff --git a/solution/requirements/prod.txt b/solution/requirements/prod.txt index 0a7dec6..570a344 100644 --- a/solution/requirements/prod.txt +++ b/solution/requirements/prod.txt @@ -5,3 +5,4 @@ psycopg2-binary==2.9.9 dj-database-url==2.1.0 django-filter==23.5 bcrypt==4.1.2 +djangorestframework-jwt==1.11.0