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"
|
return "Provide a valid token in the 'Authorization' header"
|
||||||
|
|
||||||
def authenticate(self, request):
|
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]
|
token = request.headers.get("Authorization", "").split("Bearer ")[-1]
|
||||||
|
|
||||||
if not token:
|
if not token:
|
||||||
if IsAuthenticated in getattr(
|
raise NotAuthenticated
|
||||||
request.resolver_match.func.cls, "permission_classes", []
|
|
||||||
):
|
|
||||||
raise NotAuthenticated
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
payload = jwt.decode(
|
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 api.users.validators
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
import django.db.models.deletion
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
@@ -13,6 +14,13 @@ class Migration(migrations.Migration):
|
|||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
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(
|
migrations.CreateModel(
|
||||||
name='Profile',
|
name='Profile',
|
||||||
fields=[
|
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}$')])),
|
('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()])),
|
('countryCode', models.CharField(max_length=2, validators=[django.core.validators.RegexValidator('[a-zA-Z]{2}'), api.users.validators.CountryCodeValidator()])),
|
||||||
('isPublic', models.BooleanField()),
|
('isPublic', models.BooleanField()),
|
||||||
('phone', models.CharField(blank=True, max_length=20, null=True, validators=[django.core.validators.MaxLengthValidator(20), django.core.validators.RegexValidator('\\+[\\d]+')])),
|
('phone', models.CharField(max_length=20, null=True, validators=[django.core.validators.MaxLengthValidator(20), django.core.validators.RegexValidator('\\+[\\d]+')])),
|
||||||
('image', models.URLField(blank=True, null=True)),
|
('image', models.URLField(null=True)),
|
||||||
('friends', models.ManyToManyField(blank=True, to='users.profile')),
|
('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(
|
phone = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
validators=[MaxLengthValidator(20), RegexValidator(r"\+[\d]+")],
|
validators=[MaxLengthValidator(20), RegexValidator(r"\+[\d]+")],
|
||||||
blank=True,
|
|
||||||
null=True,
|
null=True,
|
||||||
)
|
)
|
||||||
image = models.URLField(max_length=200, blank=True, null=True)
|
image = models.URLField(max_length=200, null=True)
|
||||||
friends = models.ManyToManyField("self", blank=True, symmetrical=False)
|
friends = models.ManyToManyField(
|
||||||
|
"self", through="Friendship", blank=True, symmetrical=False
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return self.login
|
return self.login
|
||||||
@@ -76,3 +77,16 @@ class Profile(models.Model):
|
|||||||
errors["phone"] = {"User with this phone already exists"}
|
errors["phone"] = {"User with this phone already exists"}
|
||||||
|
|
||||||
return errors
|
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 rest_framework import serializers
|
||||||
|
|
||||||
from api.users.models import Profile
|
from api.users.models import Friendship, Profile
|
||||||
|
|
||||||
|
|
||||||
class ProfileSerializer(serializers.ModelSerializer):
|
class ProfileSerializer(serializers.ModelSerializer):
|
||||||
@@ -29,6 +30,14 @@ class UpdateProfileSerializer(serializers.ModelSerializer):
|
|||||||
"image",
|
"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 PublicProfileSerializer(serializers.ModelSerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
@@ -49,3 +58,25 @@ class PublicProfileSerializer(serializers.ModelSerializer):
|
|||||||
if data["phone"] is None:
|
if data["phone"] is None:
|
||||||
del data["phone"]
|
del data["phone"]
|
||||||
return data
|
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(
|
path(
|
||||||
"profiles/<str:login>",
|
"profiles/<str:login>",
|
||||||
api.users.views.ProfileAPIView.as_view(),
|
api.users.views.ProfilesApiView.as_view(),
|
||||||
name="profiles",
|
name="profiles",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"friends/add",
|
"friends/add",
|
||||||
api.users.views.AddFriendAPIView.as_view(),
|
api.users.views.AddFriendApiView.as_view(),
|
||||||
name="add-friend",
|
name="add-friend",
|
||||||
),
|
),
|
||||||
path(
|
path(
|
||||||
"friends/remove",
|
"friends/remove",
|
||||||
api.users.views.RemoveFriendAPIView.as_view(),
|
api.users.views.RemoveFriendApiView.as_view(),
|
||||||
name="remove-friend",
|
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.conf import settings
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from rest_framework import status
|
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.permissions import IsAuthenticated
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.views import APIView
|
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.permissions import CanAccessProfile
|
||||||
from api.users.serializers import (
|
from api.users.serializers import (
|
||||||
|
FriendshipSerializer,
|
||||||
|
PasswordChangeSerializer,
|
||||||
ProfileSerializer,
|
ProfileSerializer,
|
||||||
PublicProfileSerializer,
|
PublicProfileSerializer,
|
||||||
UpdateProfileSerializer,
|
UpdateProfileSerializer,
|
||||||
@@ -25,7 +34,9 @@ class RegisterUserApiView(APIView):
|
|||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
errors = Profile.check_unique(None, serializer.validated_data)
|
errors = Profile.check_unique(None, serializer.validated_data)
|
||||||
if errors:
|
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 = serializer.validated_data["password"]
|
||||||
password_hash = bcrypt.hashpw(
|
password_hash = bcrypt.hashpw(
|
||||||
@@ -50,7 +61,7 @@ class RegisterUserApiView(APIView):
|
|||||||
|
|
||||||
return Response(profile, status=status.HTTP_201_CREATED)
|
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):
|
class SigninUserApiView(APIView):
|
||||||
@@ -63,14 +74,12 @@ class SigninUserApiView(APIView):
|
|||||||
if not bcrypt.checkpw(
|
if not bcrypt.checkpw(
|
||||||
password.encode("utf-8"), user.password.encode("utf-8")
|
password.encode("utf-8"), user.password.encode("utf-8")
|
||||||
):
|
):
|
||||||
return Response(
|
raise NotAuthenticated(
|
||||||
{"error": "Invalid credentials"},
|
{"error": "Invalid credentials"},
|
||||||
status=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return Response(
|
raise NotAuthenticated(
|
||||||
{"error": "Invalid credentials"},
|
{"error": "Invalid credentials"},
|
||||||
status=status.HTTP_401_UNAUTHORIZED,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
token = jwt.encode(
|
token = jwt.encode(
|
||||||
@@ -102,10 +111,14 @@ class ProfileMeApiView(APIView):
|
|||||||
if serializer.is_valid():
|
if serializer.is_valid():
|
||||||
errors = Profile.check_unique(user.id, serializer.validated_data)
|
errors = Profile.check_unique(user.id, serializer.validated_data)
|
||||||
if errors:
|
if errors:
|
||||||
return Response(errors, status=status.HTTP_409_CONFLICT)
|
return Response(
|
||||||
|
{"reason:": str(errors)}, status=status.HTTP_409_CONFLICT
|
||||||
|
)
|
||||||
serializer.save()
|
serializer.save()
|
||||||
|
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
||||||
|
raise ValidationError(serializer.errors)
|
||||||
|
|
||||||
def _get_profile_data(self, user):
|
def _get_profile_data(self, user):
|
||||||
profile = {
|
profile = {
|
||||||
@@ -123,7 +136,7 @@ class ProfileMeApiView(APIView):
|
|||||||
return profile
|
return profile
|
||||||
|
|
||||||
|
|
||||||
class ProfileAPIView(APIView):
|
class ProfilesApiView(APIView):
|
||||||
permission_classes = [IsAuthenticated, CanAccessProfile]
|
permission_classes = [IsAuthenticated, CanAccessProfile]
|
||||||
|
|
||||||
def get(self, request, login):
|
def get(self, request, login):
|
||||||
@@ -133,13 +146,12 @@ class ProfileAPIView(APIView):
|
|||||||
serializer = PublicProfileSerializer(profile)
|
serializer = PublicProfileSerializer(profile)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
except Profile.DoesNotExist:
|
except Profile.DoesNotExist:
|
||||||
return Response(
|
raise PermissionDenied(
|
||||||
{"detail": "Profile not found."},
|
{"detail": "Profile not found."},
|
||||||
status=status.HTTP_403_FORBIDDEN,
|
) from None
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class AddFriendAPIView(APIView):
|
class AddFriendApiView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
@@ -152,13 +164,12 @@ class AddFriendAPIView(APIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
except Profile.DoesNotExist:
|
except Profile.DoesNotExist:
|
||||||
return Response(
|
raise NotFound(
|
||||||
{"detail": "Profile not found."},
|
{"detail": "Profile not found."},
|
||||||
status=status.HTTP_404_NOT_FOUND,
|
) from None
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class RemoveFriendAPIView(APIView):
|
class RemoveFriendApiView(APIView):
|
||||||
permission_classes = [IsAuthenticated]
|
permission_classes = [IsAuthenticated]
|
||||||
|
|
||||||
def post(self, request):
|
def post(self, request):
|
||||||
@@ -171,7 +182,43 @@ class RemoveFriendAPIView(APIView):
|
|||||||
status=status.HTTP_200_OK,
|
status=status.HTTP_200_OK,
|
||||||
)
|
)
|
||||||
except Profile.DoesNotExist:
|
except Profile.DoesNotExist:
|
||||||
return Response(
|
raise NotFound(
|
||||||
{"detail": "Profile not found."},
|
{"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)
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -38,8 +38,6 @@ MIDDLEWARE = [
|
|||||||
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
"django.contrib.auth.middleware.AuthenticationMiddleware",
|
||||||
"django.contrib.messages.middleware.MessageMiddleware",
|
"django.contrib.messages.middleware.MessageMiddleware",
|
||||||
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
"django.middleware.clickjacking.XFrameOptionsMiddleware",
|
||||||
# Developed middleware
|
|
||||||
"pulse.middleware.ErrorResponseMiddleware",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
ROOT_URLCONF = "pulse.urls"
|
ROOT_URLCONF = "pulse.urls"
|
||||||
@@ -114,15 +112,14 @@ STATIC_ROOT = BASE_DIR / "static"
|
|||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
REST_FRAMEWORK = {
|
REST_FRAMEWORK = {
|
||||||
"DEFAULT_RENDERER_CLASSES": [
|
|
||||||
"rest_framework.renderers.JSONRenderer",
|
|
||||||
],
|
|
||||||
"DEFAULT_FILTER_BACKENDS": [
|
"DEFAULT_FILTER_BACKENDS": [
|
||||||
"django_filters.rest_framework.DjangoFilterBackend"
|
"django_filters.rest_framework.DjangoFilterBackend"
|
||||||
],
|
],
|
||||||
"DEFAULT_AUTHENTICATION_CLASSES": (
|
"DEFAULT_AUTHENTICATION_CLASSES": (
|
||||||
"api.users.authentication.JWTAuthentication",
|
"api.users.authentication.JWTAuthentication",
|
||||||
),
|
),
|
||||||
|
"EXCEPTION_HANDLER": "pulse.utils.wrap_error_into_reason",
|
||||||
|
"DATETIME_FORMAT": "%Y-%m-%dT%H:%M:%SZ",
|
||||||
}
|
}
|
||||||
|
|
||||||
APPEND_SLASH = False
|
APPEND_SLASH = False
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user