Added updatePassword view, added exception handler, added friends view

This commit is contained in:
ITQ
2024-03-03 18:13:59 +03:00
parent 18f66344bb
commit 20e6e512d2
9 changed files with 169 additions and 57 deletions
+4 -6
View File
@@ -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'),
),
]
+17 -3
View File
@@ -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}"
+32 -1
View File
@@ -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}$"),
],
)
+13 -3
View File
@@ -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",
)
]
+68 -21
View File
@@ -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)
-14
View File
@@ -1,14 +0,0 @@
from django.http import JsonResponse
from rest_framework import status
class ErrorResponseMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
if response.status_code >= status.HTTP_400_BAD_REQUEST:
response.data = {"reason": response.data}
response = JsonResponse(response.data, status=response.status_code)
return response
+2 -5
View File
@@ -38,8 +38,6 @@ MIDDLEWARE = [
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
# Developed middleware
"pulse.middleware.ErrorResponseMiddleware",
]
ROOT_URLCONF = "pulse.urls"
@@ -114,15 +112,14 @@ STATIC_ROOT = BASE_DIR / "static"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
REST_FRAMEWORK = {
"DEFAULT_RENDERER_CLASSES": [
"rest_framework.renderers.JSONRenderer",
],
"DEFAULT_FILTER_BACKENDS": [
"django_filters.rest_framework.DjangoFilterBackend"
],
"DEFAULT_AUTHENTICATION_CLASSES": (
"api.users.authentication.JWTAuthentication",
),
"EXCEPTION_HANDLER": "pulse.utils.wrap_error_into_reason",
"DATETIME_FORMAT": "%Y-%m-%dT%H:%M:%SZ",
}
APPEND_SLASH = False
+11
View File
@@ -0,0 +1,11 @@
from rest_framework.views import exception_handler
def wrap_error_into_reason(exc, context):
response = exception_handler(exc, context)
if response is not None:
custom_response = {"reason": str(response.data)}
response.data = custom_response
return response