Skip to content

Commit

Permalink
Merge pull request #6 from khodealib/feat/implement-user-auth-views
Browse files Browse the repository at this point in the history
Feat/implement user auth views
  • Loading branch information
khodealib authored Jun 27, 2024
2 parents 88d9b01 + 2483f59 commit 7cace73
Show file tree
Hide file tree
Showing 16 changed files with 282 additions and 34 deletions.
5 changes: 2 additions & 3 deletions accounts/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ class UserAdmin(BaseUserAdmin):
add_form = UserCreationForm
form = UserChangeForm
list_display = (
"uuid",
"email",
"first_name",
"last_name",
Expand All @@ -20,7 +19,7 @@ class UserAdmin(BaseUserAdmin):
"last_login",
)
ordering = ("-date_joined",)
search_fields = ("uuid", "email", "first_name", "last_name")
search_fields = ("email", "first_name", "last_name")
list_filter = ("is_staff", "is_active", "date_joined", "last_login")
fieldsets = (
(None, {"fields": ("email", "password")}),
Expand All @@ -45,7 +44,7 @@ class UserAdmin(BaseUserAdmin):
"fields": ("email", "first_name", "last_name", "password1", "password2"),
},
)
readonly_fields = ("uuid", "date_joined", "last_login")
readonly_fields = ("date_joined", "last_login")

def get_form(self, request, obj=None, **kwargs):
form = super().get_form(request, obj, **kwargs)
Expand Down
8 changes: 7 additions & 1 deletion accounts/managers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from django.contrib.auth.models import BaseUserManager
from django.contrib.auth.password_validation import validate_password
from django.forms import ValidationError


class UserManager(BaseUserManager):
Expand All @@ -7,7 +9,11 @@ def _create_user(self, email, password, **extra_fields):
raise ValueError("Email must be provided")
email = self.normalize_email(email)
user = self.model(email=email, **extra_fields)
user.set_password(password)
try:
validate_password(password)
user.set_password(password)
except ValidationError as e:
raise e
user.save(using=self._db)
return user

Expand Down
5 changes: 2 additions & 3 deletions accounts/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Generated by Django 5.0.6 on 2024-06-27 00:38
# Generated by Django 5.0.6 on 2024-06-27 05:33

import uuid
from django.db import migrations, models


Expand All @@ -16,9 +15,9 @@ class Migration(migrations.Migration):
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('email', models.EmailField(max_length=255, unique=True)),
('first_name', models.CharField(max_length=255)),
('last_name', models.CharField(max_length=255)),
Expand Down
18 changes: 0 additions & 18 deletions accounts/migrations/0002_rename_username_user_uuid.py

This file was deleted.

1 change: 0 additions & 1 deletion accounts/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ class User(AbstractBaseUser, PermissionsMixin):
Custom user model with fields for email and password.
"""

uuid = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
email = models.EmailField(max_length=255, unique=True)
first_name = models.CharField(max_length=255)
last_name = models.CharField(max_length=255)
Expand Down
49 changes: 49 additions & 0 deletions accounts/serializers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from django.core.exceptions import ValidationError
from rest_framework import serializers, status

from accounts.models import User


class UserRegistarionSerializer(serializers.ModelSerializer):
first_name = serializers.CharField(required=False, max_length=255)
last_name = serializers.CharField(required=False, max_length=255)
password = serializers.CharField(write_only=True)

class Meta:
model = User
fields = (
"id",
"email",
"first_name",
"last_name",
"password",
)
read_only_fields = ("id",)

def create(self, validated_data):
try:
user = User.objects.create_user(**validated_data)
except ValidationError as e:
raise serializers.ValidationError(
detail=e.messages, code=status.HTTP_400_BAD_REQUEST
)
return user


class UserChangePasswordSerializer(serializers.Serializer):
old_password = serializers.CharField(write_only=True)
new_password = serializers.CharField(write_only=True)
confirm_password = serializers.CharField(write_only=True)

def validate(self, data):
if data["new_password"] != data["confirm_password"]:
raise serializers.ValidationError(
{"confirm_password": ["Passwords do not match."]},
code=status.HTTP_400_BAD_REQUEST,
)
return data

def update(self, instance, validated_data):
instance.set_password(validated_data["new_password"])
instance.save()
return instance
4 changes: 4 additions & 0 deletions accounts/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from .user_model_tests import *
from .jwt_views_tests import *
from .register_view_tests import *
from .change_password_view_tests import *
35 changes: 35 additions & 0 deletions accounts/tests/change_password_view_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from rest_framework.test import APIClient, APITestCase


class UserChangePasswordTest(APITestCase):
def setUp(self):
self.client = APIClient()
self.user = self.client.post(
"/accounts/register/",
{
"email": "[email protected]",
"password": "Admin@12345678",
"first_name": "Test",
"last_name": "Test",
},
format="json",
)
self.token = self.client.post(
"/accounts/token/",
{"email": "[email protected]", "password": "Admin@12345678"},
format="json",
).json()["access"]

def test_change_password(self):
data = {
"old_password": "Admin@12345678",
"new_password": "Admin@12345679",
"confirm_password": "Admin@12345679",
}
response = self.client.patch(
"/accounts/change-password/",
data,
format="json",
HTTP_AUTHORIZATION="Bearer " + self.token,
)
self.assertEqual(response.status_code, 200)
44 changes: 44 additions & 0 deletions accounts/tests/jwt_views_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from rest_framework.test import APIClient, APITestCase

from accounts.models import User


class UserJWTViewsTest(APITestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(
email="[email protected]",
password="Admin@12345678",
first_name="Test",
last_name="Test",
)

def test_obtain_token(self):
data = {"email": "[email protected]", "password": "Admin@12345678"}
response = self.client.post("/accounts/token/", data, format="json")
self.assertEqual(response.status_code, 200)
self.assertIn("access", response.data)
self.assertIn("refresh", response.data)

def test_obtain_token_invalid_credentials(self):
data = {"email": "[email protected]", "password": "wrong_password"}
response = self.client.post("/accounts/token/", data, format="json")
self.assertEqual(response.status_code, 401)
self.assertNotIn("access", response.data)
self.assertNotIn("refresh", response.data)

def test_verify_token(self):
data = {"email": "[email protected]", "password": "Admin@12345678"}
access_token = self.client.post("/accounts/token/", data, format="json").data.get("access")
data = {"token": access_token}
response = self.client.post("/accounts/token/verify/", data, format="json")
self.assertEqual(response.status_code, 200)

def test_refresh_token(self):
data = {"email": "[email protected]", "password": "Admin@12345678"}
tokens = self.client.post("/accounts/token/", data, format="json").data
data = {"refresh": tokens.get("refresh")}
response = self.client.post("/accounts/token/refresh/", data, format="json")
self.assertEqual(response.status_code, 200)
self.assertIn("access", response.data)
self.assertNotIn("refresh", response.data)
49 changes: 49 additions & 0 deletions accounts/tests/register_view_tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
from rest_framework.test import APIClient, APITestCase

from accounts.models import User


class UserRegisterTest(APITestCase):
def setUp(self):
self.client = APIClient()
self.user = User.objects.create_user(
email="[email protected]",
password="Admin@1234567",
first_name="Test",
last_name="Test",
)

def test_register_user(self):
data = {
"email": "[email protected]",
"password": "Admin@12345678",
"first_name": "Test",
"last_name": "Test",
}
response = self.client.post("/accounts/register/", data)
self.assertEqual(response.status_code, 201)

def test_register_user_with_existing_email(self):
data = {
"email": "[email protected]",
"password": "Admin@12345678",
"first_name": "Test",
"last_name": "Test",
}
response = self.client.post("/accounts/register/", data)
self.assertEqual(response.status_code, 400)

def test_register_user_with_short_password(self):
data = {
"email": "[email protected]",
"password": "123",
"first_name": "Test",
"last_name": "Test",
}
response = self.client.post("/accounts/register/", data)
self.assertEqual(response.status_code, 400)

def test_register_user_with_no_password(self):
data = {"email": "[email protected]", "first_name": "Test", "last_name": "Test"}
response = self.client.post("/accounts/register/", data)
self.assertEqual(response.status_code, 400)
9 changes: 4 additions & 5 deletions accounts/tests.py → accounts/tests/user_model_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,17 @@
from accounts.models import User


# Create your tests here.
class UserModelTest(TestCase):
def setUp(self):
self.user = User.objects.create_user(
email="[email protected]",
password="test123",
password="Admin@12345678",
first_name="Test",
last_name="Test",
)
self.superuser = User.objects.create_superuser(
email="[email protected]",
password="admin123",
password="Admin@12345678",
first_name="Admin",
last_name="Admin",
)
Expand All @@ -33,7 +32,7 @@ def test_user_email_uniqueness(self):
with self.assertRaises(IntegrityError):
User.objects.create_user(
email="[email protected]",
password="test123",
password="Admin@12345678",
first_name="Test",
last_name="Test",
)
)
18 changes: 18 additions & 0 deletions accounts/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from django.urls import path
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
TokenVerifyView,
)

from accounts.views import UserChangePasswordView, UserRegisterView

app_name = "accounts"
urlpatterns = [
path("register/", UserRegisterView.as_view(), name="register"),
path("change-password/", UserChangePasswordView.as_view(), name="change_password"),
# JWT authentication endpoints
path("token/", TokenObtainPairView.as_view(), name="token_obtain_pair"),
path("token/verify/", TokenVerifyView.as_view(), name="token_verify"),
path("token/refresh/", TokenRefreshView.as_view(), name="token_refresh"),
]
37 changes: 35 additions & 2 deletions accounts/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,36 @@
from django.shortcuts import render
from rest_framework import generics, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response

# Create your views here.
from accounts.models import User
from accounts.serializers import UserChangePasswordSerializer, UserRegistarionSerializer


class UserRegisterView(generics.CreateAPIView):
"""Register a new user
Args:
UserRegisterView (generics.CreateAPIView): Register a new user.
"""

serializer_class = UserRegistarionSerializer
queryset = User.objects.all()


class UserChangePasswordView(generics.UpdateAPIView):
"""Update user password
Args:
UserChangePasswordView (generics.UpdateAPIView): Update user password.
"""

permission_classes = (IsAuthenticated,)
serializer_class = UserChangePasswordSerializer

def update(self, request, *args, **kwargs):
instance = request.user
serializer = self.get_serializer(instance, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.update(instance, serializer.validated_data)
return Response(status=status.HTTP_200_OK)

Loading

0 comments on commit 7cace73

Please sign in to comment.