Added updatePassword view, added exception handler, added friends view
This commit is contained in:
@@ -15,15 +15,13 @@ class JWTAuthentication(BaseAuthentication):
|
||||
return "Provide a valid token in the 'Authorization' header"
|
||||
|
||||
def authenticate(self, request):
|
||||
if not IsAuthenticated in getattr(request.resolver_match.func.cls, "permission_classes", []):
|
||||
return None
|
||||
|
||||
token = request.headers.get("Authorization", "").split("Bearer ")[-1]
|
||||
|
||||
if not token:
|
||||
if IsAuthenticated in getattr(
|
||||
request.resolver_match.func.cls, "permission_classes", []
|
||||
):
|
||||
raise NotAuthenticated
|
||||
|
||||
return None
|
||||
raise NotAuthenticated
|
||||
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Generated by Django 4.2.10 on 2024-03-03 06:12
|
||||
# Generated by Django 4.2.10 on 2024-03-03 15:01
|
||||
|
||||
import api.users.validators
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
@@ -13,6 +14,13 @@ class Migration(migrations.Migration):
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Friendship',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('addedAt', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Profile',
|
||||
fields=[
|
||||
@@ -22,9 +30,19 @@ class Migration(migrations.Migration):
|
||||
('password', models.CharField(max_length=100, validators=[django.core.validators.RegexValidator('^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{6,100}$')])),
|
||||
('countryCode', models.CharField(max_length=2, validators=[django.core.validators.RegexValidator('[a-zA-Z]{2}'), api.users.validators.CountryCodeValidator()])),
|
||||
('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')),
|
||||
('phone', models.CharField(max_length=20, null=True, validators=[django.core.validators.MaxLengthValidator(20), django.core.validators.RegexValidator('\\+[\\d]+')])),
|
||||
('image', models.URLField(null=True)),
|
||||
('friends', models.ManyToManyField(blank=True, through='users.Friendship', to='users.profile')),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='friendship',
|
||||
name='from_profile',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='friendship_from', to='users.profile'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='friendship',
|
||||
name='to_profile',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='friendship_to', to='users.profile'),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -28,11 +28,12 @@ class Profile(models.Model):
|
||||
phone = models.CharField(
|
||||
max_length=20,
|
||||
validators=[MaxLengthValidator(20), RegexValidator(r"\+[\d]+")],
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
image = models.URLField(max_length=200, blank=True, null=True)
|
||||
friends = models.ManyToManyField("self", blank=True, symmetrical=False)
|
||||
image = models.URLField(max_length=200, null=True)
|
||||
friends = models.ManyToManyField(
|
||||
"self", through="Friendship", blank=True, symmetrical=False
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.login
|
||||
@@ -76,3 +77,16 @@ class Profile(models.Model):
|
||||
errors["phone"] = {"User with this phone already exists"}
|
||||
|
||||
return errors
|
||||
|
||||
|
||||
class Friendship(models.Model):
|
||||
from_profile = models.ForeignKey(
|
||||
Profile, related_name="friendship_from", on_delete=models.CASCADE
|
||||
)
|
||||
to_profile = models.ForeignKey(
|
||||
Profile, related_name="friendship_to", on_delete=models.CASCADE
|
||||
)
|
||||
addedAt = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.from_profile} -> {self.to_profile}"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from django.core.validators import RegexValidator
|
||||
from rest_framework import serializers
|
||||
|
||||
from api.users.models import Profile
|
||||
from api.users.models import Friendship, Profile
|
||||
|
||||
|
||||
class ProfileSerializer(serializers.ModelSerializer):
|
||||
@@ -29,6 +30,14 @@ class UpdateProfileSerializer(serializers.ModelSerializer):
|
||||
"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
|
||||
|
||||
|
||||
class PublicProfileSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
@@ -49,3 +58,25 @@ class PublicProfileSerializer(serializers.ModelSerializer):
|
||||
if data["phone"] is None:
|
||||
del data["phone"]
|
||||
return data
|
||||
|
||||
|
||||
class FriendshipSerializer(serializers.ModelSerializer):
|
||||
login = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Friendship
|
||||
fields = ["login", "addedAt"]
|
||||
|
||||
def get_login(self, obj):
|
||||
return obj.to_profile.login
|
||||
|
||||
|
||||
class PasswordChangeSerializer(serializers.Serializer):
|
||||
# ruff: noqa: N815
|
||||
oldPassword = serializers.CharField(required=True)
|
||||
newPassword = serializers.CharField(
|
||||
required=True,
|
||||
validators=[
|
||||
RegexValidator(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9]).{6,100}$"),
|
||||
],
|
||||
)
|
||||
|
||||
@@ -20,17 +20,27 @@ urlpatterns = [
|
||||
),
|
||||
path(
|
||||
"profiles/<str:login>",
|
||||
api.users.views.ProfileAPIView.as_view(),
|
||||
api.users.views.ProfilesApiView.as_view(),
|
||||
name="profiles",
|
||||
),
|
||||
path(
|
||||
"friends/add",
|
||||
api.users.views.AddFriendAPIView.as_view(),
|
||||
api.users.views.AddFriendApiView.as_view(),
|
||||
name="add-friend",
|
||||
),
|
||||
path(
|
||||
"friends/remove",
|
||||
api.users.views.RemoveFriendAPIView.as_view(),
|
||||
api.users.views.RemoveFriendApiView.as_view(),
|
||||
name="remove-friend",
|
||||
),
|
||||
path(
|
||||
"friends",
|
||||
api.users.views.FriendsListApiView.as_view(),
|
||||
name="friends-list",
|
||||
),
|
||||
path(
|
||||
"me/updatePassword",
|
||||
api.users.views.PasswordChangeApiView.as_view(),
|
||||
name="password-change",
|
||||
)
|
||||
]
|
||||
|
||||
@@ -5,13 +5,22 @@ import jwt
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import (
|
||||
NotAuthenticated,
|
||||
NotFound,
|
||||
PermissionDenied,
|
||||
ValidationError,
|
||||
)
|
||||
from rest_framework.generics import ListAPIView
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from api.users.models import Profile
|
||||
from api.users.models import Friendship, Profile
|
||||
from api.users.permissions import CanAccessProfile
|
||||
from api.users.serializers import (
|
||||
FriendshipSerializer,
|
||||
PasswordChangeSerializer,
|
||||
ProfileSerializer,
|
||||
PublicProfileSerializer,
|
||||
UpdateProfileSerializer,
|
||||
@@ -25,7 +34,9 @@ class RegisterUserApiView(APIView):
|
||||
if serializer.is_valid():
|
||||
errors = Profile.check_unique(None, serializer.validated_data)
|
||||
if errors:
|
||||
return Response(errors, status=status.HTTP_409_CONFLICT)
|
||||
return Response(
|
||||
{"reason:": str(errors)}, status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
|
||||
password = serializer.validated_data["password"]
|
||||
password_hash = bcrypt.hashpw(
|
||||
@@ -50,7 +61,7 @@ class RegisterUserApiView(APIView):
|
||||
|
||||
return Response(profile, status=status.HTTP_201_CREATED)
|
||||
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
raise ValidationError(serializer.errors)
|
||||
|
||||
|
||||
class SigninUserApiView(APIView):
|
||||
@@ -63,14 +74,12 @@ class SigninUserApiView(APIView):
|
||||
if not bcrypt.checkpw(
|
||||
password.encode("utf-8"), user.password.encode("utf-8")
|
||||
):
|
||||
return Response(
|
||||
raise NotAuthenticated(
|
||||
{"error": "Invalid credentials"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
raise NotAuthenticated(
|
||||
{"error": "Invalid credentials"},
|
||||
status=status.HTTP_401_UNAUTHORIZED,
|
||||
)
|
||||
|
||||
token = jwt.encode(
|
||||
@@ -102,10 +111,14 @@ class ProfileMeApiView(APIView):
|
||||
if serializer.is_valid():
|
||||
errors = Profile.check_unique(user.id, serializer.validated_data)
|
||||
if errors:
|
||||
return Response(errors, status=status.HTTP_409_CONFLICT)
|
||||
return Response(
|
||||
{"reason:": str(errors)}, status=status.HTTP_409_CONFLICT
|
||||
)
|
||||
serializer.save()
|
||||
|
||||
return Response(serializer.data)
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
raise ValidationError(serializer.errors)
|
||||
|
||||
def _get_profile_data(self, user):
|
||||
profile = {
|
||||
@@ -123,7 +136,7 @@ class ProfileMeApiView(APIView):
|
||||
return profile
|
||||
|
||||
|
||||
class ProfileAPIView(APIView):
|
||||
class ProfilesApiView(APIView):
|
||||
permission_classes = [IsAuthenticated, CanAccessProfile]
|
||||
|
||||
def get(self, request, login):
|
||||
@@ -133,13 +146,12 @@ class ProfileAPIView(APIView):
|
||||
serializer = PublicProfileSerializer(profile)
|
||||
return Response(serializer.data)
|
||||
except Profile.DoesNotExist:
|
||||
return Response(
|
||||
raise PermissionDenied(
|
||||
{"detail": "Profile not found."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
)
|
||||
) from None
|
||||
|
||||
|
||||
class AddFriendAPIView(APIView):
|
||||
class AddFriendApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
@@ -152,13 +164,12 @@ class AddFriendAPIView(APIView):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except Profile.DoesNotExist:
|
||||
return Response(
|
||||
raise NotFound(
|
||||
{"detail": "Profile not found."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
) from None
|
||||
|
||||
|
||||
class RemoveFriendAPIView(APIView):
|
||||
class RemoveFriendApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
@@ -171,7 +182,43 @@ class RemoveFriendAPIView(APIView):
|
||||
status=status.HTTP_200_OK,
|
||||
)
|
||||
except Profile.DoesNotExist:
|
||||
return Response(
|
||||
raise NotFound(
|
||||
{"detail": "Profile not found."},
|
||||
status=status.HTTP_404_NOT_FOUND,
|
||||
)
|
||||
) from None
|
||||
|
||||
|
||||
class FriendsListApiView(ListAPIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = FriendshipSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
limit = int(self.request.query_params.get("limit", 5))
|
||||
offset = int(self.request.query_params.get("offset", 0))
|
||||
|
||||
return Friendship.objects.filter(from_profile=self.request.user)[
|
||||
offset : offset + limit
|
||||
]
|
||||
|
||||
|
||||
class PasswordChangeApiView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
serializer = PasswordChangeSerializer(data=request.data)
|
||||
|
||||
if serializer.is_valid():
|
||||
old_password = serializer.validated_data.get("oldPassword")
|
||||
new_password = serializer.validated_data.get("newPassword")
|
||||
|
||||
if bcrypt.checkpw(old_password.encode("utf-8"), request.user.password.encode("utf-8")):
|
||||
password_hash = bcrypt.hashpw(
|
||||
new_password.encode("utf-8"), bcrypt.gensalt()
|
||||
).decode("utf-8")
|
||||
request.user.password = password_hash
|
||||
request.user.save()
|
||||
|
||||
return Response({"status": "ok"}, status=status.HTTP_200_OK)
|
||||
|
||||
raise PermissionDenied({"error": "Invalid old password"})
|
||||
|
||||
raise ValidationError(serializer.errors)
|
||||
|
||||
Reference in New Issue
Block a user