From 9bce35b42d0fc05b25b33d6e27036823cbd25046 Mon Sep 17 00:00:00 2001 From: moonlitgrace Date: Wed, 11 Dec 2024 08:20:57 +0530 Subject: [PATCH 1/3] chore: remove profile model and custom user --- backend/apps/api/__init__.py | 0 backend/apps/api/admin.py | 3 + backend/apps/api/apps.py | 6 + backend/apps/api/migrations/__init__.py | 0 backend/apps/api/models.py | 3 + backend/apps/api/v1/__init__.py | 0 backend/apps/api/v1/urls.py | 16 +++ backend/apps/quib/migrations/0001_initial.py | 44 +----- backend/apps/quib/migrations/0002_initial.py | 59 ++++++++ backend/apps/quib/models.py | 8 +- .../apps/quiblet/migrations/0001_initial.py | 38 +----- .../apps/quiblet/migrations/0002_initial.py | 50 +------ .../quiblet/migrations/0003_delete_quib.py | 16 --- backend/apps/quiblet/models.py | 6 +- backend/apps/user/admin.py | 24 +--- backend/apps/user/api/v1/serializers.py | 26 +--- backend/apps/user/api/v1/urls.py | 35 ++--- backend/apps/user/api/v1/views.py | 78 ----------- backend/apps/user/api/v1/viewsets.py | 56 -------- backend/apps/user/apps.py | 3 - backend/apps/user/auth.py | 35 ----- backend/apps/user/backends.py | 27 ---- backend/apps/user/forms.py | 23 +--- backend/apps/user/managers.py | 43 ------ backend/apps/user/migrations/0001_initial.py | 126 ++++++++---------- backend/apps/user/models.py | 47 ++----- backend/apps/user/signals.py | 11 -- backend/config/settings.py | 28 ++-- backend/config/urls.py | 13 +- backend/poetry.lock | 43 +++++- backend/pyproject.toml | 1 + backend/tests/conftest.py | 4 +- 32 files changed, 249 insertions(+), 623 deletions(-) create mode 100644 backend/apps/api/__init__.py create mode 100644 backend/apps/api/admin.py create mode 100644 backend/apps/api/apps.py create mode 100644 backend/apps/api/migrations/__init__.py create mode 100644 backend/apps/api/models.py create mode 100644 backend/apps/api/v1/__init__.py create mode 100644 backend/apps/api/v1/urls.py create mode 100644 backend/apps/quib/migrations/0002_initial.py delete mode 100644 backend/apps/quiblet/migrations/0003_delete_quib.py delete mode 100644 backend/apps/user/api/v1/views.py delete mode 100644 backend/apps/user/api/v1/viewsets.py delete mode 100644 backend/apps/user/auth.py delete mode 100644 backend/apps/user/backends.py delete mode 100644 backend/apps/user/managers.py delete mode 100644 backend/apps/user/signals.py diff --git a/backend/apps/api/__init__.py b/backend/apps/api/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/api/admin.py b/backend/apps/api/admin.py new file mode 100644 index 00000000..8c38f3f3 --- /dev/null +++ b/backend/apps/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/backend/apps/api/apps.py b/backend/apps/api/apps.py new file mode 100644 index 00000000..ae752015 --- /dev/null +++ b/backend/apps/api/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'apps.api' diff --git a/backend/apps/api/migrations/__init__.py b/backend/apps/api/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/api/models.py b/backend/apps/api/models.py new file mode 100644 index 00000000..71a83623 --- /dev/null +++ b/backend/apps/api/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/backend/apps/api/v1/__init__.py b/backend/apps/api/v1/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/apps/api/v1/urls.py b/backend/apps/api/v1/urls.py new file mode 100644 index 00000000..b701bc8f --- /dev/null +++ b/backend/apps/api/v1/urls.py @@ -0,0 +1,16 @@ +from django.urls import include, path +from rest_framework_simplejwt.views import ( + TokenObtainPairView, + TokenRefreshView, + TokenVerifyView, +) + +urlpatterns = [ + path('users/', include('apps.user.api.v1.urls')), + path('quiblets/', include('apps.quiblet.api.v1.urls')), + path('quibs/', include('apps.quib.api.v1.urls')), + # jwt auth + path('auth/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('auth/token/verify/', TokenVerifyView.as_view(), name='token_verify'), +] diff --git a/backend/apps/quib/migrations/0001_initial.py b/backend/apps/quib/migrations/0001_initial.py index 59a59197..9996a0c0 100644 --- a/backend/apps/quib/migrations/0001_initial.py +++ b/backend/apps/quib/migrations/0001_initial.py @@ -1,6 +1,5 @@ -# Generated by Django 5.1.3 on 2024-12-07 04:12 +# Generated by Django 5.1.4 on 2024-12-11 02:49 -import django.db.models.deletion import shortuuid.django_fields from django.db import migrations, models @@ -9,10 +8,7 @@ class Migration(migrations.Migration): initial = True - dependencies = [ - ('quiblet', '0003_delete_quib'), - ('user', '0001_initial'), - ] + dependencies = [] operations = [ migrations.CreateModel( @@ -44,42 +40,6 @@ class Migration(migrations.Migration): ), ), ('content', models.TextField(verbose_name='content')), - ( - 'dislikes', - models.ManyToManyField( - blank=True, - related_name='disliked_quibs', - to='user.profile', - verbose_name='dislikes', - ), - ), - ( - 'likes', - models.ManyToManyField( - blank=True, - related_name='liked_quibs', - to='user.profile', - verbose_name='likes', - ), - ), - ( - 'quibber', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='quibs', - to='user.profile', - verbose_name='quibber', - ), - ), - ( - 'quiblet', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='quibs', - to='quiblet.quiblet', - verbose_name='quiblet', - ), - ), ], options={ 'verbose_name': 'Quib', diff --git a/backend/apps/quib/migrations/0002_initial.py b/backend/apps/quib/migrations/0002_initial.py new file mode 100644 index 00000000..46143a2b --- /dev/null +++ b/backend/apps/quib/migrations/0002_initial.py @@ -0,0 +1,59 @@ +# Generated by Django 5.1.4 on 2024-12-11 02:49 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('quib', '0001_initial'), + ('quiblet', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AddField( + model_name='quib', + name='dislikes', + field=models.ManyToManyField( + blank=True, + related_name='disliked_quibs', + to=settings.AUTH_USER_MODEL, + verbose_name='dislikes', + ), + ), + migrations.AddField( + model_name='quib', + name='likes', + field=models.ManyToManyField( + blank=True, + related_name='liked_quibs', + to=settings.AUTH_USER_MODEL, + verbose_name='likes', + ), + ), + migrations.AddField( + model_name='quib', + name='quibber', + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='quibs', + to=settings.AUTH_USER_MODEL, + verbose_name='quibber', + ), + ), + migrations.AddField( + model_name='quib', + name='quiblet', + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name='quibs', + to='quiblet.quiblet', + verbose_name='quiblet', + ), + ), + ] diff --git a/backend/apps/quib/models.py b/backend/apps/quib/models.py index 07924f15..56f2faad 100644 --- a/backend/apps/quib/models.py +++ b/backend/apps/quib/models.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _ from apps.quiblet.models import Quiblet -from apps.user.models import Profile +from apps.user.models import CustomUser from common.mixins import CreatedAtMixin, IsPublicMixin, ShortUUIDIdMixin @@ -15,7 +15,7 @@ class Quib(CreatedAtMixin, IsPublicMixin, ShortUUIDIdMixin): on_delete=models.CASCADE, ) quibber = models.ForeignKey( - Profile, + CustomUser, related_name='quibs', verbose_name=_('quibber'), on_delete=models.CASCADE, @@ -24,10 +24,10 @@ class Quib(CreatedAtMixin, IsPublicMixin, ShortUUIDIdMixin): slug = models.SlugField(_('slug'), editable=False, max_length=25, blank=True) content = models.TextField(_('content')) likes = models.ManyToManyField( - Profile, related_name='liked_quibs', blank=True, verbose_name=_('likes') + CustomUser, related_name='liked_quibs', blank=True, verbose_name=_('likes') ) dislikes = models.ManyToManyField( - Profile, related_name='disliked_quibs', blank=True, verbose_name=_('dislikes') + CustomUser, related_name='disliked_quibs', blank=True, verbose_name=_('dislikes') ) def save(self, *args, **kwargs): diff --git a/backend/apps/quiblet/migrations/0001_initial.py b/backend/apps/quiblet/migrations/0001_initial.py index ef76e101..9b5d997f 100644 --- a/backend/apps/quiblet/migrations/0001_initial.py +++ b/backend/apps/quiblet/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.1.3 on 2024-12-06 15:53 +# Generated by Django 5.1.4 on 2024-12-11 02:49 import dynamic_filenames import shortuuid.django_fields @@ -12,42 +12,6 @@ class Migration(migrations.Migration): dependencies = [] operations = [ - migrations.CreateModel( - name='Quib', - fields=[ - ( - 'created_at', - models.DateTimeField(auto_now_add=True, verbose_name='create at'), - ), - ('is_public', models.BooleanField(default=True, verbose_name='is public')), - ( - 'id', - shortuuid.django_fields.ShortUUIDField( - alphabet='abcdefghijklmnopqrstuvwxyz0123456789', - editable=False, - length=7, - max_length=7, - prefix='', - primary_key=True, - serialize=False, - verbose_name='id', - ), - ), - ('title', models.CharField(max_length=255, verbose_name='title')), - ( - 'slug', - models.SlugField( - blank=True, editable=False, max_length=25, verbose_name='slug' - ), - ), - ('content', models.TextField(verbose_name='content')), - ], - options={ - 'verbose_name': 'Quib', - 'verbose_name_plural': 'Quibs', - 'ordering': ['-created_at'], - }, - ), migrations.CreateModel( name='Quiblet', fields=[ diff --git a/backend/apps/quiblet/migrations/0002_initial.py b/backend/apps/quiblet/migrations/0002_initial.py index 0677f133..6eec4b49 100644 --- a/backend/apps/quiblet/migrations/0002_initial.py +++ b/backend/apps/quiblet/migrations/0002_initial.py @@ -1,7 +1,7 @@ -# Generated by Django 5.1.3 on 2024-12-06 15:53 +# Generated by Django 5.1.4 on 2024-12-11 02:49 -import django.db.models.deletion import django.db.models.functions.text +from django.conf import settings from django.db import migrations, models @@ -11,47 +11,17 @@ class Migration(migrations.Migration): dependencies = [ ('quiblet', '0001_initial'), - ('user', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ - migrations.AddField( - model_name='quib', - name='dislikes', - field=models.ManyToManyField( - blank=True, - related_name='disliked_quibs', - to='user.profile', - verbose_name='dislikes', - ), - ), - migrations.AddField( - model_name='quib', - name='likes', - field=models.ManyToManyField( - blank=True, - related_name='liked_quibs', - to='user.profile', - verbose_name='likes', - ), - ), - migrations.AddField( - model_name='quib', - name='quibber', - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='quibs', - to='user.profile', - verbose_name='quibber', - ), - ), migrations.AddField( model_name='quiblet', name='members', field=models.ManyToManyField( blank=True, related_name='joined_quiblets', - to='user.profile', + to=settings.AUTH_USER_MODEL, verbose_name='members', ), ), @@ -61,20 +31,10 @@ class Migration(migrations.Migration): field=models.ManyToManyField( blank=True, related_name='ranged_quiblets', - to='user.profile', + to=settings.AUTH_USER_MODEL, verbose_name='rangers', ), ), - migrations.AddField( - model_name='quib', - name='quiblet', - field=models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='quibs', - to='quiblet.quiblet', - verbose_name='quiblet', - ), - ), migrations.AddConstraint( model_name='quiblet', constraint=models.UniqueConstraint( diff --git a/backend/apps/quiblet/migrations/0003_delete_quib.py b/backend/apps/quiblet/migrations/0003_delete_quib.py deleted file mode 100644 index 30751f1d..00000000 --- a/backend/apps/quiblet/migrations/0003_delete_quib.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.1.3 on 2024-12-07 04:11 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('quiblet', '0002_initial'), - ] - - operations = [ - migrations.DeleteModel( - name='Quib', - ), - ] diff --git a/backend/apps/quiblet/models.py b/backend/apps/quiblet/models.py index b11ff5a7..d9bcd821 100644 --- a/backend/apps/quiblet/models.py +++ b/backend/apps/quiblet/models.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _ from dynamic_filenames import FilePattern -from apps.user.models import Profile +from apps.user.models import CustomUser from common.mixins import AvatarMixin, CreatedAtMixin, IsPublicMixin, ShortUUIDIdMixin @@ -18,10 +18,10 @@ class Quiblet(AvatarMixin, CreatedAtMixin, IsPublicMixin, ShortUUIDIdMixin): null=True, ) members = models.ManyToManyField( - Profile, related_name='joined_quiblets', blank=True, verbose_name=_('members') + CustomUser, related_name='joined_quiblets', blank=True, verbose_name=_('members') ) rangers = models.ManyToManyField( - Profile, related_name='ranged_quiblets', blank=True, verbose_name=_('rangers') + CustomUser, related_name='ranged_quiblets', blank=True, verbose_name=_('rangers') ) class Meta: # type: ignore diff --git a/backend/apps/user/admin.py b/backend/apps/user/admin.py index 3c9575c6..339a88e8 100644 --- a/backend/apps/user/admin.py +++ b/backend/apps/user/admin.py @@ -2,11 +2,11 @@ from django.contrib.auth.admin import UserAdmin from django.utils.translation import gettext_lazy as _ -from .forms import CustomUserAdminForm, ProfileAdminForm -from .models import Profile, User +from .forms import CustomUserAdminForm +from .models import CustomUser -@admin.register(User) +@admin.register(CustomUser) class CustomUserAdmin(UserAdmin): # form = CustomUserAdminForm add_form = CustomUserAdminForm @@ -36,21 +36,3 @@ class CustomUserAdmin(UserAdmin): list_display = ('email', 'is_active', 'is_staff', 'is_superuser', 'date_joined') search_fields = ('email',) ordering = ('email',) - - -@admin.register(Profile) -class ProfileAdmin(admin.ModelAdmin): - form = ProfileAdminForm - - fieldsets = ( - ( - None, - {'fields': ('user', 'username', 'color', 'avatar', 'first_name', 'last_name')}, - ), - (_('important dates'), {'fields': ('created_at',)}), - ) - - list_display = ('username', 'user__email', 'created_at') - search_fields = ('username', 'user__email') - ordering = ('-created_at',) - readonly_fields = ('created_at',) diff --git a/backend/apps/user/api/v1/serializers.py b/backend/apps/user/api/v1/serializers.py index feb6a2e3..188c7ab5 100644 --- a/backend/apps/user/api/v1/serializers.py +++ b/backend/apps/user/api/v1/serializers.py @@ -1,33 +1,15 @@ from rest_framework import serializers -from apps.user.models import Profile, User +from apps.user.models import CustomUser class UserSerializer(serializers.ModelSerializer): class Meta: - model = User - fields = ('id', 'email', 'password', 'date_joined') + model = CustomUser + fields = '__all__' read_only_fields = ('date_joined',) extra_kwargs = {'password': {'write_only': True}} def create(self, validated_data): - user = User.objects.create_user(**validated_data) # type: ignore + user = CustomUser.objects.create_user(**validated_data) # type: ignore return user - - -class ProfileSerializer(serializers.ModelSerializer): - user = UserSerializer(read_only=True) - - class Meta: - model = Profile - fields = '__all__' - - -class AuthSerializer(serializers.ModelSerializer): - class Meta: - model = User - fields = ('email', 'password') - - -class AuthTokenResponseSerializer(serializers.Serializer): - token = serializers.CharField() diff --git a/backend/apps/user/api/v1/urls.py b/backend/apps/user/api/v1/urls.py index bae1e891..9eef25c3 100644 --- a/backend/apps/user/api/v1/urls.py +++ b/backend/apps/user/api/v1/urls.py @@ -1,28 +1,19 @@ from django.urls import include, path -from rest_framework.routers import DefaultRouter -from .views import LoginAPIView, LogoutAPIView, MeAPIView, RegisterAPIView -from .viewsets import MyProfilesViewSet, ProfileViewSet - -router = DefaultRouter() -router.register(r'profiles', ProfileViewSet) -router.register(r'me/profiles', MyProfilesViewSet, basename='me-profile') +# from .views import LoginAPIView, LogoutAPIView, MeAPIView, RegisterAPIView urlpatterns = [ # auth endpoints - path( - 'auth/', - include( - [ - path('login/', LoginAPIView.as_view(), name='login'), - path('logout/', LogoutAPIView.as_view(), name='logout'), - path('register/', RegisterAPIView.as_view(), name='register'), - ] - ), - ), - # user view of requested user - path('me/', MeAPIView.as_view(), name='me'), + # path( + # 'auth/', + # include( + # [ + # path('login/', LoginAPIView.as_view(), name='login'), + # path('logout/', LogoutAPIView.as_view(), name='logout'), + # path('register/', RegisterAPIView.as_view(), name='register'), + # ] + # ), + # ), + # # user view of requested user + # path('me/', MeAPIView.as_view(), name='me'), ] - -# router urls should be placed last to prevent overriding -urlpatterns += router.urls diff --git a/backend/apps/user/api/v1/views.py b/backend/apps/user/api/v1/views.py deleted file mode 100644 index f2bdc479..00000000 --- a/backend/apps/user/api/v1/views.py +++ /dev/null @@ -1,78 +0,0 @@ -from django.contrib.auth import authenticate -from drf_spectacular.utils import extend_schema -from rest_framework import exceptions, generics, permissions, views -from rest_framework.authtoken.models import Token -from rest_framework.response import Response - -from common.api.exceptions import ServerError -from common.api.serializers import DetailResponseSerializer - -from .serializers import AuthSerializer, AuthTokenResponseSerializer, ProfileSerializer - - -class LoginAPIView(views.APIView): - """ - Customized drf basic token authentication. - - This view authenticates the user using email and password credentials - and issues a token upon successful login. - """ - - serializer_class = AuthSerializer - - @extend_schema(responses=AuthTokenResponseSerializer) - def post(self, request, format=None): - user = authenticate( - email=request.data.get('email'), password=request.data.get('password') - ) - if user: - token, _ = Token.objects.get_or_create(user=user) - return Response({'token': token.key}) - else: - raise exceptions.AuthenticationFailed() - - -class LogoutAPIView(views.APIView): - """ - View to handle user logout by deleting the authentication token. - """ - - permission_classes = (permissions.IsAuthenticated,) - - @extend_schema(request=None, responses=DetailResponseSerializer) - def post(self, request, format=None): - try: - Token.objects.filter(user=request.user).delete() - return Response({'detail': 'Successfully logged out.'}) - - except Exception as e: - raise ServerError(f"An error occurred while logging out: {str(e)}") - - -class RegisterAPIView(generics.CreateAPIView): - """ - View to handle registering of new users. - """ - - serializer_class = AuthSerializer - - -class MeAPIView(views.APIView): - """ - View to retrieve information for the currently authenticated user. - - - `get`: Returns the details of the authenticated user based on their token. - - Permission: - - Requires user authentication. - """ - - permission_classes = (permissions.IsAuthenticated,) - serializer_class = ProfileSerializer - - def get(self, request): - if request.user_profile: - serializer = self.serializer_class(request.user_profile) - return Response(serializer.data) - else: - raise exceptions.ValidationError('A valid profile must be provided.') diff --git a/backend/apps/user/api/v1/viewsets.py b/backend/apps/user/api/v1/viewsets.py deleted file mode 100644 index 77080756..00000000 --- a/backend/apps/user/api/v1/viewsets.py +++ /dev/null @@ -1,56 +0,0 @@ -from django.conf import settings -from rest_framework import exceptions, filters, permissions, viewsets - -from apps.user.models import Profile - -from .serializers import ProfileSerializer - - -class ProfileViewSet(viewsets.ReadOnlyModelViewSet): - """ - ViewSet for performing read-only operations on the Profile model. - - Filtering: - - Allows searching profiles by username. - """ - - queryset = Profile.objects.all() - serializer_class = ProfileSerializer - filter_backends = (filters.SearchFilter,) - search_fields = ('username',) - - -class MyProfilesViewSet(viewsets.ModelViewSet): - """ - ViewSet to manage profiles associated with the authenticated user. - - Permissions: - - Requires user authentication to access and modify profiles. - """ - - permission_classes = (permissions.IsAuthenticated,) - serializer_class = ProfileSerializer - - def get_queryset(self): # type: ignore - """ - Restrict queryset to profiles owned by the currently authenticated user. - """ - # during schema generation - if getattr(self, 'swagger_fake_view', False): - return Profile.objects.none() - user = self.request.user - return user.profiles.all() # type: ignore - - def perform_create(self, serializer): - """ - Create a new profile for the authenticated user, enforcing a maximum limit. - - Raises: - ValidationError: If the user already has limited profiles. - """ - user = self.request.user - if user.profiles.count() >= settings.PROFILE_LIMIT: # type: ignore - raise exceptions.ValidationError( - f'A user cannot have more than {settings.PROFILE_LIMIT} profiles.' - ) - serializer.save(user=user) diff --git a/backend/apps/user/apps.py b/backend/apps/user/apps.py index 562f008f..ff7e415e 100644 --- a/backend/apps/user/apps.py +++ b/backend/apps/user/apps.py @@ -4,6 +4,3 @@ class UserConfig(AppConfig): default_auto_field = "django.db.models.BigAutoField" name = "apps.user" - - def ready(self) -> None: - from . import signals # noqa: F401 diff --git a/backend/apps/user/auth.py b/backend/apps/user/auth.py deleted file mode 100644 index 025d66f5..00000000 --- a/backend/apps/user/auth.py +++ /dev/null @@ -1,35 +0,0 @@ -from rest_framework import exceptions -from rest_framework.authentication import TokenAuthentication - -from .models import Profile - - -class ExtendedTokenAuthentication(TokenAuthentication): - """ - Extended drf TokenAuthentication - which includes 'user_profile' field on request - """ - - keyword = 'Bearer' - - def authenticate(self, request): - user_auth_token_tuple = super().authenticate(request) - if not user_auth_token_tuple: - return None - - user, auth_token = user_auth_token_tuple - - profile_id = request.headers.get('Profile-Id') - user_profile = None - - if profile_id: - try: - user_profile = Profile.objects.get(id=profile_id, user=user) - except Profile.DoesNotExist: - raise exceptions.PermissionDenied( - 'Profile does not exist or does not belong to the authenticated user.' - ) - - request.user_profile = user_profile - - return (user, auth_token) diff --git a/backend/apps/user/backends.py b/backend/apps/user/backends.py deleted file mode 100644 index 0a7f4c90..00000000 --- a/backend/apps/user/backends.py +++ /dev/null @@ -1,27 +0,0 @@ -from django.contrib.auth import get_user_model -from django.contrib.auth.backends import ModelBackend - -UserModel = get_user_model() - - -class EmailAuthBackend(ModelBackend): - """ - Custom Auth backend with email instead username - """ - - def authenticate( # pyright: ignore [reportIncompatibleMethodOverride] - self, request, email=None, password=None, **kwargs - ): - try: - user = UserModel.objects.get(email=email) - if password and user.check_password(password): - return user - return None - except UserModel.DoesNotExist: - return None - - def get_user(self, user_id: int): - try: - return UserModel.objects.get(pk=user_id) - except UserModel.DoesNotExist: - return None diff --git a/backend/apps/user/forms.py b/backend/apps/user/forms.py index 26a5a94f..266cb087 100644 --- a/backend/apps/user/forms.py +++ b/backend/apps/user/forms.py @@ -1,16 +1,15 @@ from django import forms -from django.conf import settings from django.forms import ModelForm from django.utils.translation import gettext_lazy as _ -from .models import Profile, User +from .models import CustomUser class CustomUserAdminForm(ModelForm): password = forms.CharField(widget=forms.PasswordInput, required=False) class Meta: # pyright: ignore [reportIncompatibleVariableOverride] - model = User + model = CustomUser fields = '__all__' def save(self, commit=True): @@ -21,21 +20,3 @@ def save(self, commit=True): if commit: user.save() return user - - -class ProfileAdminForm(ModelForm): - class Meta: # pyright: ignore [reportIncompatibleVariableOverride] - model = Profile - fields = '__all__' - - def clean(self): # pyright: ignore [reportIncompatibleVariableOverride] - user = self.cleaned_data.get('user') - - if ( - self.instance.pk is None - and user - and user.profiles.count() >= settings.PROFILE_LIMIT - ): - self.add_error(None, _('a user cannot have more than 5 profiles.')) - - return self.cleaned_data diff --git a/backend/apps/user/managers.py b/backend/apps/user/managers.py deleted file mode 100644 index 2a52879f..00000000 --- a/backend/apps/user/managers.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Any - -from django.contrib.auth.models import BaseUserManager - - -class CustomUserManager(BaseUserManager): - """ - Custom user manager where email is the unique identifier - for authentication instead usernamess. - """ - - use_in_migrations = True - - def _create_user(self, email: str, password: str | None, **extra_fields: Any): - """ - base function to save user with email and password (if given) - """ - from .models import User # prevent circular import - - if not email: - raise ValueError("Email is required") - email = self.normalize_email(email) - user: User = self.model(email=email, **extra_fields) - user.set_password(password) - user.save(using=self._db) - return user - - def create_user(self, email: str, password: None = None, **extra_fields: Any): - extra_fields.setdefault('is_staff', False) - extra_fields.setdefault('is_superuser', False) - return self._create_user(email, password, **extra_fields) - - def create_superuser(self, email: str, password: str, **extra_fields: Any): - extra_fields.setdefault('is_staff', True) - extra_fields.setdefault('is_superuser', True) - - # sanity checking - if extra_fields.get('is_staff') is not True: - raise ValueError('Superuser must have staff=True') - if extra_fields.get('is_superuser') is not True: - raise ValueError('Superuser must have superuser=True') - - return self._create_user(email, password, **extra_fields) diff --git a/backend/apps/user/migrations/0001_initial.py b/backend/apps/user/migrations/0001_initial.py index 243e656d..01e99d39 100644 --- a/backend/apps/user/migrations/0001_initial.py +++ b/backend/apps/user/migrations/0001_initial.py @@ -1,13 +1,13 @@ -# Generated by Django 5.1.3 on 2024-12-06 15:53 +# Generated by Django 5.1.4 on 2024-12-11 02:49 import functools -import django.db.models.deletion +import django.contrib.auth.models +import django.contrib.auth.validators +import django.utils.timezone import dynamic_filenames -from django.conf import settings from django.db import migrations, models -import apps.user.managers import common.mixins @@ -21,7 +21,7 @@ class Migration(migrations.Migration): operations = [ migrations.CreateModel( - name='User', + name='CustomUser', fields=[ ( 'id', @@ -46,14 +46,27 @@ class Migration(migrations.Migration): ), ), ( - 'email', - models.EmailField( - max_length=254, unique=True, verbose_name='email address' + 'username', + models.CharField( + error_messages={ + 'unique': 'A user with that username already exists.' + }, + help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', + max_length=150, + unique=True, + validators=[ + django.contrib.auth.validators.UnicodeUsernameValidator() + ], + verbose_name='username', ), ), ( - 'date_joined', - models.DateTimeField(auto_now_add=True, verbose_name='date joined'), + 'first_name', + models.CharField(blank=True, max_length=150, verbose_name='first name'), + ), + ( + 'last_name', + models.CharField(blank=True, max_length=150, verbose_name='last name'), ), ( 'is_staff', @@ -72,53 +85,11 @@ class Migration(migrations.Migration): ), ), ( - 'groups', - models.ManyToManyField( - blank=True, - help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', - related_name='user_set', - related_query_name='user', - to='auth.group', - verbose_name='groups', - ), - ), - ( - 'user_permissions', - models.ManyToManyField( - blank=True, - help_text='Specific permissions for this user.', - related_name='user_set', - related_query_name='user', - to='auth.permission', - verbose_name='user permissions', - ), - ), - ], - options={ - 'verbose_name': 'User', - 'verbose_name_plural': 'Users', - 'ordering': ['-date_joined'], - }, - managers=[ - ('objects', apps.user.managers.CustomUserManager()), - ], - ), - migrations.CreateModel( - name='Profile', - fields=[ - ( - 'id', - models.BigAutoField( - auto_created=True, - primary_key=True, - serialize=False, - verbose_name='ID', + 'date_joined', + models.DateTimeField( + default=django.utils.timezone.now, verbose_name='date joined' ), ), - ( - 'created_at', - models.DateTimeField(auto_now_add=True, verbose_name='create at'), - ), ( 'color', models.CharField( @@ -164,34 +135,43 @@ class Migration(migrations.Migration): ), ), ( - 'username', - models.CharField(max_length=25, unique=True, verbose_name='username'), - ), - ( - 'first_name', - models.CharField( - blank=True, max_length=255, null=True, verbose_name='first name' + 'email', + models.EmailField( + error_messages={'unique': 'A user with that email already exists.'}, + max_length=254, + unique=True, + verbose_name='Email address', ), ), + ('bio', models.TextField(verbose_name='Bio')), ( - 'last_name', - models.CharField( - blank=True, max_length=255, null=True, verbose_name='last name' + 'groups', + models.ManyToManyField( + blank=True, + help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', + related_name='user_set', + related_query_name='user', + to='auth.group', + verbose_name='groups', ), ), ( - 'user', - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - related_name='profiles', - to=settings.AUTH_USER_MODEL, + 'user_permissions', + models.ManyToManyField( + blank=True, + help_text='Specific permissions for this user.', + related_name='user_set', + related_query_name='user', + to='auth.permission', + verbose_name='user permissions', ), ), ], options={ - 'verbose_name': 'Profile', - 'verbose_name_plural': 'Profiles', - 'ordering': ['-created_at'], + 'ordering': ['-date_joined'], }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], ), ] diff --git a/backend/apps/user/models.py b/backend/apps/user/models.py index 24031c11..d3710b1f 100644 --- a/backend/apps/user/models.py +++ b/backend/apps/user/models.py @@ -1,49 +1,20 @@ -from django.contrib.auth.models import AbstractBaseUser, PermissionsMixin +from django.contrib.auth.models import AbstractUser from django.db import models from django.utils.translation import gettext_lazy as _ -from common.mixins import AvatarMixin, ColorMixin, CreatedAtMixin +from common.mixins import AvatarMixin, ColorMixin -from .managers import CustomUserManager - -class User(AbstractBaseUser, PermissionsMixin): - email = models.EmailField(_('email address'), unique=True) - date_joined = models.DateTimeField(_('date joined'), auto_now_add=True) - is_staff = models.BooleanField( - _("staff status"), - default=False, - help_text=_("designates whether the user can log into this admin site."), - ) - is_active = models.BooleanField( - _("active"), - default=True, - help_text=_( - "designates whether this user should be treated as active. unselect this instead of deleting accounts." - ), +class CustomUser(AbstractUser, ColorMixin, AvatarMixin): + email = models.EmailField( + _("Email address"), + unique=True, + error_messages={'unique': _('A user with that email already exists.')}, ) - - objects = CustomUserManager() - - USERNAME_FIELD = 'email' - REQUIRED_FIELDS = [] - - class Meta: # type: ignore - verbose_name = 'User' - verbose_name_plural = 'Users' - ordering = ['-date_joined'] - - -class Profile(CreatedAtMixin, ColorMixin, AvatarMixin): - user = models.ForeignKey(User, related_name='profiles', on_delete=models.CASCADE) - username = models.CharField(_('username'), unique=True, max_length=25) - first_name = models.CharField(_('first name'), max_length=255, blank=True, null=True) - last_name = models.CharField(_('last name'), max_length=255, blank=True, null=True) + bio = models.TextField(_('Bio')) def __str__(self): return f"u/{self.username}" class Meta: # type: ignore - verbose_name = 'Profile' - verbose_name_plural = 'Profiles' - ordering = ['-created_at'] + ordering = ['-date_joined'] diff --git a/backend/apps/user/signals.py b/backend/apps/user/signals.py deleted file mode 100644 index 95214952..00000000 --- a/backend/apps/user/signals.py +++ /dev/null @@ -1,11 +0,0 @@ -from django.db.models.signals import post_save -from django.dispatch import receiver - -from .models import Profile, User - - -@receiver(post_save, sender=User) -def create_profile(sender, instance, created, **kwargs): - if created: - username = instance.email.split('@')[0] - Profile.objects.create(user=instance, username=username) diff --git a/backend/config/settings.py b/backend/config/settings.py index 35c4b467..e15e8332 100644 --- a/backend/config/settings.py +++ b/backend/config/settings.py @@ -1,4 +1,5 @@ import os +from datetime import timedelta from pathlib import Path import dj_database_url @@ -41,7 +42,9 @@ 'django_extensions', # rest framework 'rest_framework', - 'rest_framework.authtoken', + # jwt auth + 'rest_framework_simplejwt', + 'rest_framework_simplejwt.token_blacklist', # django filtering 'django_filters', # middleware (cors) @@ -57,6 +60,7 @@ SELF_APPS = [ 'apps.user', + 'apps.api', 'apps.quiblet', 'apps.quib', ] @@ -72,7 +76,7 @@ REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ - 'apps.user.auth.ExtendedTokenAuthentication', + 'rest_framework_simplejwt.authentication.JWTAuthentication', ], 'DEFAULT_FILTER_BACKENDS': [ 'django_filters.rest_framework.DjangoFilterBackend', @@ -130,6 +134,15 @@ ], } +# https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html + +SIMPLE_JWT = { + 'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), + 'REFRESH_TOKEN_LIFETIME': timedelta(days=30), + 'BLACKLIST_AFTER_ROTATION': True, + 'UPDATE_LAST_LOGIN': True, +} + MIDDLEWARE = [ 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', @@ -223,13 +236,7 @@ DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' # Custom AUTH model and backends -AUTH_USER_MODEL = 'user.User' - -AUTHENTICATION_BACKENDS = [ - 'django.contrib.auth.backends.ModelBackend', - # custom auth backend - 'apps.user.backends.EmailAuthBackend', -] +AUTH_USER_MODEL = 'user.CustomUser' # django-cors-headers settins # https://pypi.org/project/django-cors-headers/ @@ -238,9 +245,6 @@ 'http://localhost:5173', ] -# max no:of profiles a user can create -PROFILE_LIMIT = 3 - # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/4.2/howto/static-files/ diff --git a/backend/config/urls.py b/backend/config/urls.py index eb852455..1253fe67 100644 --- a/backend/config/urls.py +++ b/backend/config/urls.py @@ -16,17 +16,8 @@ urlpatterns = [ # admin path('admin/', admin.site.urls), - # api endpoints - path( - 'api/v1/', - include( - [ - path('users/', include('apps.user.api.v1.urls')), - path('quiblets/', include('apps.quiblet.api.v1.urls')), - path('quibs/', include('apps.quib.api.v1.urls')), - ] - ), - ), + # v1 api + path('api/v1/', include('apps.api.v1.urls')), # openapi path('api/v1/schema/', SpectacularAPIView.as_view(api_version='v1'), name='schema'), path('api/v1/schema.json', SpectacularJSONAPIView.as_view(), name='schema-json'), diff --git a/backend/poetry.lock b/backend/poetry.lock index cd2feabe..544e151d 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -341,6 +341,30 @@ files = [ [package.dependencies] django = ">=4.2" +[[package]] +name = "djangorestframework-simplejwt" +version = "5.3.1" +description = "A minimal JSON Web Token authentication plugin for Django REST Framework" +optional = false +python-versions = ">=3.8" +files = [ + {file = "djangorestframework_simplejwt-5.3.1-py3-none-any.whl", hash = "sha256:381bc966aa46913905629d472cd72ad45faa265509764e20ffd440164c88d220"}, + {file = "djangorestframework_simplejwt-5.3.1.tar.gz", hash = "sha256:6c4bd37537440bc439564ebf7d6085e74c5411485197073f508ebdfa34bc9fae"}, +] + +[package.dependencies] +django = ">=3.2" +djangorestframework = ">=3.12" +pyjwt = ">=1.7.1,<3" + +[package.extras] +crypto = ["cryptography (>=3.3.1)"] +dev = ["Sphinx (>=1.6.5,<2)", "cryptography", "flake8", "freezegun", "ipython", "isort", "pep8", "pytest", "pytest-cov", "pytest-django", "pytest-watch", "pytest-xdist", "python-jose (==3.3.0)", "sphinx_rtd_theme (>=0.1.9)", "tox", "twine", "wheel"] +doc = ["Sphinx (>=1.6.5,<2)", "sphinx_rtd_theme (>=0.1.9)"] +lint = ["flake8", "isort", "pep8"] +python-jose = ["python-jose (==3.3.0)"] +test = ["cryptography", "freezegun", "pytest", "pytest-cov", "pytest-django", "pytest-xdist", "tox"] + [[package]] name = "drf-spectacular" version = "0.28.0" @@ -818,6 +842,23 @@ files = [ {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"}, ] +[[package]] +name = "pyjwt" +version = "2.10.1" +description = "JSON Web Token implementation in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, + {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, +] + +[package.extras] +crypto = ["cryptography (>=3.4.0)"] +dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] +docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] +tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] + [[package]] name = "pyparsing" version = "3.2.0" @@ -1166,4 +1207,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "0c685c41c77a9adc7aae3fc5ebafe15cd79544b4b7bdca7ed37a7d0b50fa8601" +content-hash = "090814607a88f532dd0d8d995354e766e9acbf76a8a173ea7f2ddf02c8efbb36" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index e7a5eea7..55cb363a 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -33,6 +33,7 @@ drf-spectacular = {extras = ["sidecar"], version = "^0.28.0"} shortuuid = "^1.0.13" # 3rd party exception handler drf-standardized-errors = {extras = ["openapi"], version = "^0.14.1"} +djangorestframework-simplejwt = "^5.3.1" [tool.poetry.group.dev.dependencies] # task runner diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 37a99094..813811de 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,13 +1,13 @@ import pytest from rest_framework.authtoken.models import Token -from apps.user.models import Profile, User +from apps.user.models import CustomUser, Profile @pytest.fixture def user(): """Creates and returns a user.""" - return User.objects.create_user(email='test@test.com', password='testpass') # type: ignore + return CustomUser.objects.create_user(email='test@test.com', password='testpass') # type: ignore @pytest.fixture From 2c8587e2f4d68656c5bf8e60fd1088a58631b13c Mon Sep 17 00:00:00 2001 From: moonlitgrace Date: Wed, 11 Dec 2024 08:42:34 +0530 Subject: [PATCH 2/3] chore: update customuseradmin --- backend/apps/user/admin.py | 34 ++++++++++++---------------------- backend/apps/user/forms.py | 22 ---------------------- 2 files changed, 12 insertions(+), 44 deletions(-) delete mode 100644 backend/apps/user/forms.py diff --git a/backend/apps/user/admin.py b/backend/apps/user/admin.py index 339a88e8..0c567695 100644 --- a/backend/apps/user/admin.py +++ b/backend/apps/user/admin.py @@ -2,37 +2,27 @@ from django.contrib.auth.admin import UserAdmin from django.utils.translation import gettext_lazy as _ -from .forms import CustomUserAdminForm from .models import CustomUser @admin.register(CustomUser) class CustomUserAdmin(UserAdmin): - # form = CustomUserAdminForm - add_form = CustomUserAdminForm - fieldsets = ( - (None, {'fields': ('email', 'password')}), - ( - _('permissions'), - { - 'fields': ('is_active', 'is_staff', 'is_superuser'), - }, # , 'groups', 'user_permissions')}, - ), - (_('important dates'), {'fields': ('date_joined',)}), - ) - - add_fieldsets = ( + (None, {'fields': ('username', 'email', 'password')}), + (_('Personal info'), {'fields': ('first_name', 'last_name')}), ( - None, + _('Permissions'), { - 'classes': ('wide',), - 'fields': ('email', 'password', 'is_active', 'is_staff', 'is_superuser'), + 'fields': ( + 'is_active', + 'is_staff', + 'is_superuser', + 'groups', + 'user_permissions', + ), }, ), + (_('Important dates'), {'fields': ('last_login', 'date_joined')}), ) - readonly_fields = ('date_joined',) - list_display = ('email', 'is_active', 'is_staff', 'is_superuser', 'date_joined') - search_fields = ('email',) - ordering = ('email',) + readonly_fields = ('date_joined', 'last_login') diff --git a/backend/apps/user/forms.py b/backend/apps/user/forms.py deleted file mode 100644 index 266cb087..00000000 --- a/backend/apps/user/forms.py +++ /dev/null @@ -1,22 +0,0 @@ -from django import forms -from django.forms import ModelForm -from django.utils.translation import gettext_lazy as _ - -from .models import CustomUser - - -class CustomUserAdminForm(ModelForm): - password = forms.CharField(widget=forms.PasswordInput, required=False) - - class Meta: # pyright: ignore [reportIncompatibleVariableOverride] - model = CustomUser - fields = '__all__' - - def save(self, commit=True): - user = super().save(commit=False) - password = self.cleaned_data.get('password') - if password: - user.set_password(password) - if commit: - user.save() - return user From b6a7a34014345a953cb0723345782c50aa589c02 Mon Sep 17 00:00:00 2001 From: moonlitgrace Date: Wed, 11 Dec 2024 08:49:04 +0530 Subject: [PATCH 3/3] chore: group paths --- backend/apps/api/v1/urls.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/backend/apps/api/v1/urls.py b/backend/apps/api/v1/urls.py index b701bc8f..caccf995 100644 --- a/backend/apps/api/v1/urls.py +++ b/backend/apps/api/v1/urls.py @@ -10,7 +10,14 @@ path('quiblets/', include('apps.quiblet.api.v1.urls')), path('quibs/', include('apps.quib.api.v1.urls')), # jwt auth - path('auth/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), - path('auth/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), - path('auth/token/verify/', TokenVerifyView.as_view(), name='token_verify'), + path( + 'auth/', + include( + [ + path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), + path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), + path('token/verify/', TokenVerifyView.as_view(), name='token_verify'), + ] + ), + ), ]