From fde4c78483d59252a8c9be3e82c946b26ba8cd62 Mon Sep 17 00:00:00 2001 From: Trevor James Nangosha Date: Fri, 8 Nov 2024 23:09:33 +0300 Subject: [PATCH] Use a proxy model for the Subject model This is easier compared to the previous approach since it means we don't have to manually manage our tokens(at least for now). Less code to write, less code to maintain! - disable the subject auth backend for now. --- api/admin.py | 54 +++++++++++++++++++++---------------------- api/models.py | 57 ++++++++-------------------------------------- api/serializers.py | 18 +++++++++++---- api/views.py | 55 +++++++++----------------------------------- thea/settings.py | 2 +- thea/urls.py | 2 +- 6 files changed, 64 insertions(+), 124 deletions(-) diff --git a/api/admin.py b/api/admin.py index 4e7764c..5ebd104 100644 --- a/api/admin.py +++ b/api/admin.py @@ -5,37 +5,37 @@ Disease, Result, Location) -class SubjectAdmin(admin.ModelAdmin): - # control which columns appear in the list view - list_display = ('email', 'name', 'name') +# class SubjectAdmin(admin.ModelAdmin): +# # control which columns appear in the list view +# list_display = ('email', 'name', 'name') - # Fields for when adding a user - add_fieldsets = ( - (None, { - 'classes': ('wide',), - 'fields': ( - 'name', - 'email', - 'password', - )} - ), - ) +# # Fields for when adding a user +# add_fieldsets = ( +# (None, { +# 'classes': ('wide',), +# 'fields': ( +# 'name', +# 'email', +# 'password', +# )} +# ), +# ) - # Fields for editing existing users, grouped in fieldsets - # fieldsets = ( - # (None, {'fields': ('email', 'password')}), - # ('Personal info', {'fields': ('name',)}), - # ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}), - # ('Important dates', {'fields': ('last_login', 'date_joined')}), - # ) +# # Fields for editing existing users, grouped in fieldsets +# # fieldsets = ( +# # (None, {'fields': ('email', 'password')}), +# # ('Personal info', {'fields': ('name',)}), +# # ('Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}), +# # ('Important dates', {'fields': ('last_login', 'date_joined')}), +# # ) - # Fields you can search by - search_fields = ('email', 'name') +# # Fields you can search by +# search_fields = ('email', 'name') - # Fields you can use to order the user list - ordering = ('email', 'name') +# # Fields you can use to order the user list +# ordering = ('email', 'name') - readonly_fields = ('date_archived', 'date_deleted', 'created_at', 'modified_at') +# readonly_fields = ('date_archived', 'date_deleted', 'created_at', 'modified_at') class TestAdmin(admin.ModelAdmin): list_display = ('disease_id', 'subject') @@ -100,7 +100,7 @@ class LocationAdmin(admin.ModelAdmin): readonly_fields = ('created_at', 'modified_at') -admin.site.register(Subject, SubjectAdmin) +# admin.site.register(Subject, SubjectAdmin) admin.site.register(Test, TestAdmin) admin.site.register(Disease, DiseaseAdmin) admin.site.register(Result, ResultAdmin) diff --git a/api/models.py b/api/models.py index 3cba6c5..6b277a2 100644 --- a/api/models.py +++ b/api/models.py @@ -7,11 +7,16 @@ from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin class CustomUserManager(BaseUserManager): - def create_user(self, email, name, password=None): + def create_user(self, email, name, password=None, user_role=None): if not email: raise ValueError('Users must have an email address') + user = self.model(email=self.normalize_email(email), name=name) user.set_password(password) + + if user_role: + user.user_role = user_role + user.save(using=self._db) return user @@ -26,6 +31,7 @@ class UserRole(models.TextChoices): LAB_TECHINICIAN = 'lab_tech', 'Lab_tech' USER = 'user', 'User' ADMIN = 'admin', 'Admin' + SUBJECT = 'subject', 'Subject' class User(AbstractBaseUser, PermissionsMixin): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) @@ -57,52 +63,9 @@ def delete(self, *args, **kwargs): def __str__(self): return self.name -class Subject(AbstractBaseUser, PermissionsMixin): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - name = models.CharField(max_length=255) - email = models.EmailField(unique=True) - password = models.CharField(max_length=128) - date_archived = models.DateTimeField(null=True, blank=True) - date_deleted = models.DateTimeField(null=True, blank=True) - created_at = models.DateTimeField(auto_now_add=True) - modified_at = models.DateTimeField(auto_now=True) - - objects = CustomUserManager() - - USERNAME_FIELD = 'email' - REQUIRED_FIELDS = ['name'] - - groups = models.ManyToManyField( - 'auth.Group', - related_name='subject_set', - blank=True, - verbose_name=('groups'), - help_text=( - 'The groups this subject belongs to. A subject will get all permissions ' - 'granted to each of their groups.' - ), - ) - - user_permissions = models.ManyToManyField( - 'auth.Permission', - related_name='subject_set', - blank=True, - verbose_name=('user permissions'), - help_text=('Specific permissions for this subject.'), - ) - - def set_password(self, raw_password): - self.password = make_password(raw_password) - - def check_password(self, raw_password): - return check_password(raw_password, self.password) - - def delete(self, *args, **kwargs): - self.date_deleted = timezone.now() - self.save() - - def __str__(self): - return self.name +class Subject(User): + class Meta: + proxy = True class Location(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) diff --git a/api/serializers.py b/api/serializers.py index 132524f..c306f3f 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,10 +1,19 @@ from rest_framework import serializers -from .models import Subject, User, Location, Test, Disease, Result, Hotspot, InfectionRate - +from .models import ( + Subject, + User, + Location, + Test, + Disease, + Result, + Hotspot, + InfectionRate, + UserRole +) class UserSerializer(serializers.ModelSerializer): class Meta: model = User - fields = ['id', 'name', 'email', 'password', 'user_role'] + fields = ['id', 'name', 'email', 'password'] extra_kwargs = { 'password': {'write_only': True}, } @@ -35,7 +44,8 @@ def create(self, validated_data): subject = Subject.objects.create_user( email=validated_data['email'], name=validated_data['name'], - password=validated_data['password'] + password=validated_data['password'], + user_role=UserRole.SUBJECT ) return subject diff --git a/api/views.py b/api/views.py index 0ae2776..687d86b 100644 --- a/api/views.py +++ b/api/views.py @@ -4,8 +4,6 @@ from django.db.models import F, Subquery, OuterRef, Count from django.core.paginator import Paginator from django.db.models.functions import TruncDate -from django.contrib.auth import authenticate -from django.core.exceptions import PermissionDenied from rest_framework import viewsets, status, serializers from rest_framework.response import Response @@ -19,9 +17,7 @@ from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.serializers import TokenObtainPairSerializer -from api.auth.backends import SubjectAuthBackend - -from .models import Subject, User, Location, Test, Disease, Result, Hotspot, InfectionRate +from .models import Subject, User, Location, Test, Disease, Result, Hotspot, InfectionRate, UserRole from .serializers import ( SubjectSerializer, UserSerializer, @@ -36,45 +32,18 @@ class TheaUserTokenObtainPairSerializer(TokenObtainPairSerializer): def validate(self, attrs): data = super().validate(attrs) - - # Add custom claims... + data['user'] = UserSerializer(self.user).data return data class TheaUserTokenObtainPairView(TokenObtainPairView): serializer_class = TheaUserTokenObtainPairSerializer - -class SubjectRefreshToken(RefreshToken): - @classmethod - def for_user(cls, subject): - token = cls() - return token class TheaSubjectTokenObtainPairSerializer(TokenObtainPairSerializer): - @classmethod - def get_token(cls, subject: Subject): - return SubjectRefreshToken.for_user(subject) - - def validate(self, attrs): - authenticate_kwargs = { - self.username_field: attrs[self.username_field], - "password": attrs["password"], - } - - subject_auth_backend = SubjectAuthBackend() - self.subject = subject_auth_backend.authenticate(**authenticate_kwargs) - - if self.subject: - refresh = self.get_token(self.subject) - data = { - 'refresh': str(refresh), - 'access': str(refresh.access_token), - } - data['subject'] = SubjectSerializer(self.subject).data - - return data - else: - raise PermissionDenied("Invalid credentials") + def validate(self, attrs): + data = super().validate(attrs) + data['subject'] = SubjectSerializer(self.user).data + return data class TheaSubjectTokenObtainPairView(TokenObtainPairView): serializer_class = TheaSubjectTokenObtainPairSerializer @@ -110,8 +79,6 @@ def post(self, request): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class LogoutView(APIView): - # i think this view could work for both users and subjects! - def post(self, request): try: refresh_token = request.data["refresh_token"] @@ -124,7 +91,7 @@ def post(self, request): class SubjectViewSet(viewsets.ModelViewSet): serializer_class = SubjectSerializer - queryset = Subject.objects.filter(date_deleted__isnull=True) + queryset = Subject.objects.filter(date_deleted__isnull=True, user_role=UserRole.SUBJECT) def retrieve(self, request, *args, **kwargs): instance = self.get_object() @@ -202,11 +169,11 @@ class LocationViewSet(viewsets.ModelViewSet): def list(self, request, *args, **kwargs): # get locations that a particular user has been to - user = request.query_params.get('user') - if user: - queryset = Location.objects.filter(date_deleted__isnull=True, user_id=user) + subject = request.query_params.get('subject') + if subject: + queryset = Location.objects.filter(date_deleted__isnull=True, subject=subject) else: - return Response({'error': 'user_id is required'}, status=status.HTTP_400_BAD_REQUEST) + return Response({'error': 'subject is required'}, status=status.HTTP_400_BAD_REQUEST) serializer = LocationSerializer(queryset, many=True) return Response(serializer.data) diff --git a/thea/settings.py b/thea/settings.py index 9422563..3768075 100644 --- a/thea/settings.py +++ b/thea/settings.py @@ -154,6 +154,6 @@ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" AUTHENTICATION_BACKENDS = [ - 'api.auth.backends.SubjectAuthBackend', + # 'api.auth.backends.SubjectAuthBackend', # disable this for now - may come in handy when we start enabling different auth/register methods for subject roles "django.contrib.auth.backends.ModelBackend" ] diff --git a/thea/urls.py b/thea/urls.py index 37437cd..5731a08 100644 --- a/thea/urls.py +++ b/thea/urls.py @@ -18,7 +18,7 @@ path('register/user/', UserRegisterView.as_view(), name='user_register'), path('login/user/', TheaUserTokenObtainPairView.as_view(), name='user_token_obtain_pair'), - path('login/user/refresh/', TokenRefreshView.as_view(), name='user_token_refresh'), # client must pass the refresh token! + path('login/user/refresh/', TokenRefreshView.as_view(), name='user_token_refresh'), path('logout/user/', LogoutView.as_view(), name='user_logout'), path('register/subject/', SubjectRegisterView.as_view(), name='subject_register'),