diff --git a/solution/pulse/api/users/authentication.py b/solution/pulse/api/users/authentication.py index 50ba8b1..d9841c9 100644 --- a/solution/pulse/api/users/authentication.py +++ b/solution/pulse/api/users/authentication.py @@ -1,3 +1,4 @@ +import bcrypt import jwt from django.conf import settings from rest_framework.authentication import ( @@ -30,6 +31,13 @@ class JWTAuthentication(BaseAuthentication): ) user = Profile.objects.get(id=payload["id"]) + + if not bcrypt.checkpw( + payload["password"].encode("utf-8"), + user.password.encode("utf-8"), + ): + error = "Token has expired" + raise AuthenticationFailed(error) except Profile.DoesNotExist: error = "Invalid token" raise AuthenticationFailed(error) from None diff --git a/solution/pulse/api/users/migrations/0001_initial.py b/solution/pulse/api/users/migrations/0001_initial.py index 7602951..cc77166 100644 --- a/solution/pulse/api/users/migrations/0001_initial.py +++ b/solution/pulse/api/users/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-03-02 10:14 +# Generated by Django 4.2.10 on 2024-03-03 06:12 import api.users.validators import django.core.validators @@ -24,6 +24,7 @@ class Migration(migrations.Migration): ('isPublic', models.BooleanField()), ('phone', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.MaxLengthValidator(20), django.core.validators.RegexValidator('\\+[\\d]+')])), ('image', models.URLField(blank=True, null=True)), + ('friends', models.ManyToManyField(blank=True, to='users.profile')), ], ), ] diff --git a/solution/pulse/api/users/models.py b/solution/pulse/api/users/models.py index cd333a0..80ad911 100644 --- a/solution/pulse/api/users/models.py +++ b/solution/pulse/api/users/models.py @@ -32,6 +32,7 @@ class Profile(models.Model): null=True, ) image = models.URLField(max_length=200, blank=True, null=True) + friends = models.ManyToManyField("self", blank=True, symmetrical=False) def __str__(self): return self.login @@ -39,6 +40,16 @@ class Profile(models.Model): def is_authenticated(self): return True + def add_friend(self, user): + if self != user: + self.friends.add(user) + + def remove_friend(self, user): + self.friends.remove(user) + + def check_for_friendship(self, user): + return self.friends.filter(pk=user.pk).exists() + @classmethod def check_unique(cls, user_id, validated_data): errors = {} diff --git a/solution/pulse/api/users/permissions.py b/solution/pulse/api/users/permissions.py new file mode 100644 index 0000000..0bbd8a4 --- /dev/null +++ b/solution/pulse/api/users/permissions.py @@ -0,0 +1,13 @@ +from rest_framework.permissions import BasePermission + + +class CanAccessProfile(BasePermission): + def has_object_permission(self, request, view, obj): + if ( + obj.isPublic + or obj.check_for_friendship(request.user) + or obj == request.user + ): + return True + + return False diff --git a/solution/pulse/api/users/serializers.py b/solution/pulse/api/users/serializers.py index 674ba39..00f23ba 100644 --- a/solution/pulse/api/users/serializers.py +++ b/solution/pulse/api/users/serializers.py @@ -28,3 +28,24 @@ class UpdateProfileSerializer(serializers.ModelSerializer): "phone", "image", ] + + +class PublicProfileSerializer(serializers.ModelSerializer): + class Meta: + model = Profile + fields = [ + "login", + "email", + "countryCode", + "isPublic", + "phone", + "image", + ] + + def to_representation(self, instance): + data = super().to_representation(instance) + if data["image"] is None: + del data["image"] + if data["phone"] is None: + del data["phone"] + return data diff --git a/solution/pulse/api/users/urls.py b/solution/pulse/api/users/urls.py index ba6320c..6e97349 100644 --- a/solution/pulse/api/users/urls.py +++ b/solution/pulse/api/users/urls.py @@ -16,5 +16,21 @@ urlpatterns = [ path( "me/profile", api.users.views.ProfileMeApiView.as_view(), - ) + name="profile-me", + ), + path( + "profiles/", + api.users.views.ProfileAPIView.as_view(), + name="profiles", + ), + path( + "friends/add", + api.users.views.AddFriendAPIView.as_view(), + name="add-friend", + ), + path( + "friends/remove", + api.users.views.RemoveFriendAPIView.as_view(), + name="remove-friend", + ), ] diff --git a/solution/pulse/api/users/views.py b/solution/pulse/api/users/views.py index f93e967..31f7ab3 100644 --- a/solution/pulse/api/users/views.py +++ b/solution/pulse/api/users/views.py @@ -10,7 +10,12 @@ from rest_framework.response import Response from rest_framework.views import APIView from api.users.models import Profile -from api.users.serializers import ProfileSerializer, UpdateProfileSerializer +from api.users.permissions import CanAccessProfile +from api.users.serializers import ( + ProfileSerializer, + PublicProfileSerializer, + UpdateProfileSerializer, +) class RegisterUserApiView(APIView): @@ -116,3 +121,57 @@ class ProfileMeApiView(APIView): profile["image"] = user.image return profile + + +class ProfileAPIView(APIView): + permission_classes = [IsAuthenticated, CanAccessProfile] + + def get(self, request, login): + try: + profile = Profile.objects.get(login=login) + self.check_object_permissions(request, profile) + serializer = PublicProfileSerializer(profile) + return Response(serializer.data) + except Profile.DoesNotExist: + return Response( + {"detail": "Profile not found."}, + status=status.HTTP_403_FORBIDDEN, + ) + + +class AddFriendAPIView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + login = request.data.get("login") + profile = Profile.objects.get(login=login) + request.user.add_friend(profile) + return Response( + {"status": "ok"}, + status=status.HTTP_200_OK, + ) + except Profile.DoesNotExist: + return Response( + {"detail": "Profile not found."}, + status=status.HTTP_404_NOT_FOUND, + ) + + +class RemoveFriendAPIView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request): + try: + login = request.data.get("login") + profile = Profile.objects.get(login=login) + request.user.remove_friend(profile) + return Response( + {"status": "ok"}, + status=status.HTTP_200_OK, + ) + except Profile.DoesNotExist: + return Response( + {"detail": "Profile not found."}, + status=status.HTTP_404_NOT_FOUND, + )