davidcr01 / WordlePlus

Repository to store all the documentation, files and structure of my Final Degree Project (TFG in Spanish). The main goal is to develop, as full-stack web developer, a remodel of the Wordle game, including more features and functionalities using Ionic, Django REST Api and PostgreSQL.
1 stars 0 forks source link

Customize the User and Players model (S1) #3

Closed davidcr01 closed 1 year ago

davidcr01 commented 1 year ago

Description

Django provides a default User model: https://docs.djangoproject.com/en/4.1/ref/contrib/auth/.

But it is needed to adapt this User model to one that fits with the project. Some of the default fields are not necessary, but it is necessary to add a new field avatar to the User model.

Besides, it is necessary to create the Player model, defined in the Database analysis of the project.

Tasks

Note: the H2-1, H2-2, and H2-3 are implemented in the User model and serializer, as they will be identified by the is_staff field.

Extra tasks

davidcr01 commented 1 year ago

Update Report

Models

Models in Django represents the source of information about your data. It contains the essential fields and behaviors of the data you’re storing. Generally, each model maps to a single database table. They are defined as classes, and their atributes are defined as they were variables, where the type and additional parameters are specified.

The User model looks like this:

class CustomUser(AbstractUser):
    avatar = models.ImageField(upload_to='avatars/', null=True, blank=True)
    last_login = models.DateTimeField(auto_now=True)
    date_joined = models.DateTimeField(auto_now_add=True)

It adds the avatar to the Django default user model and specifies that the last_login and date_joined will be automatically stored.

class Player(models.Model):
    user = models.OneToOneField(CustomUser, on_delete=models.CASCADE, related_name='player')
    wins = models.PositiveIntegerField(default=0)
    wins_pvp = models.PositiveIntegerField(default=0)
    wins_tournament = models.PositiveIntegerField(default=0)
    xp = models.PositiveIntegerField(default=0)

Model of the player. Every Player is related to its CustomUser information, and adds to it some new information.

Every time the models are created or edited, it is necessary to make the migrations in Django. To make this, execute the following command with the containers launched:

docker-compose exec dj python manage.py makemigrations
docker-compose exec dj python manage.py migrate

Serializers

The serializers allow complex data such as querysets and model instances to be converted to native Python datatypes that can then be easily rendered into JSON, XML or other content types. They specify how the information is inserted and returned.

The related serializers of the previous models are quite simple:

class CustomUserSerializer(serializers.ModelSerializer):
    class Meta:
        model = CustomUser
        fields = '__all__'

class PlayerSerializer(serializers.ModelSerializer):
    user = CustomUserSerializer()

    class Meta:
        model = Player
        fields = ('user', 'wins', 'wins_pvp', 'wins_tournament', 'xp')

Notice that every serializer needs its model, and in the case of the Player serializer, as it is related to the CustomUser model, it also needs the serializer of the CustomUser.

Views

A view is function that takes a web request and returns a web response. This response can be the HTML contents of a web page, or a redirect, or a 404 error, or an XML document, or an image. In our context, we could treat it as an API endpoint. Django REST framework allows you to combine the logic for a set of related views in a single class, called a ViewSet. ViewSets can be defined in multiple ways, in this case, the CustomUser and Player viewsets are defined using the ModelViewSet DRF class, including the CRUD operations over these objects and allowing to include more actions to it.

In views.py

class CustomUserViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows users to be viewed or edited.
    """
    queryset = CustomUser.objects.all().order_by('-date_joined')
    serializer_class = CustomUserSerializer
    #permission_classes = [permissions.IsAuthenticated]

class PlayerViewSet(viewsets.ModelViewSet):
    """
    API endpoint that allows players to be viewed or edited.
    """
    queryset = Player.objects.all().order_by('wins')
    serializer_class = PlayerSerializer
    #permission_classes = [permissions.IsAuthenticated]

The viewsets use a serializer defined previously to know how to treat the information.

URLS

Summarizing, we defined a model to structure the information, serializers to convert them, and views to manage them. Now, it is necessary to define the API URLs that will use these views to manipulate the information:

In urls.py:

router = routers.DefaultRouter()
router.register(r'users', views.CustomUserViewSet)
router.register(r'players', views.PlayerViewSet)

router.register(r'users', views.CustomUserViewSet) and router.register(r'users', views.PlayerViewSet) registers the CustomUserViewSet and PlayerViewSet viewsets with the router. This creates the necessary routes for CRUD operations (create, retrieve, update, delete) under the base URL "users/" and "players/". For example, with this registration, you would have routes like "users/" to get a list of users and create a new one, "users/{id}/" to view, update, and delete a specific user, among others, and the same for the players.

Testing the API

To test the API, we can use an HTTP client such as Postman or, in my case, the VSCode extension called Thunder Client.

A POST request could be http://localhost:80/users/ with the following JSON content: 1

The information below is the returned information of the newly created user. The same information is returned if we use a GET request to http://localhost:80/users/6.

We can update the related information with a PUT request, and specifying the username and password of the user. For example: 2

And finally, we can delete the related information with a DELETE request to the same URL: 3

The API works as expected, but this is very insecure as every person that knows the URLs can easily manipulate this information. This problem will be solved in https://github.com/davidcr01/TFG/issues/4

Admin

The admin site of Django is a built-in feature that provides a user interface for managing the data of the application. It automatically generates forms and views for creating, editing, and deleting data, as well as for managing users and groups. The admin site can be customized by modifying the admin.py file of the application, allowing you to define custom views, forms, and actions. The admin site is often used during development to quickly create and manage data, and can also be used in production as a convenient way to manage content.

This can be useful to the administrators of the application to manage all the information. In this case, the admin site is customized as follows:

class CustomUserAdmin(UserAdmin):
    model = CustomUser
    list_display = ('username', 'email', 'first_name', 'last_name', 'is_staff', 'is_active',)
    list_filter = ('username', 'email', 'is_staff', 'is_active',)
    fieldsets = (
        (None, {'fields': ('username', 'password')}),
        (('Personal info'), {'fields': ('first_name', 'last_name', 'email', 'avatar')}),
        (('Permissions'), {'fields': ('is_active', 'is_staff', 'is_superuser', 'groups', 'user_permissions')}),
        (('Important dates'), {'fields': ('last_login', 'date_joined')}),
    )
    add_fieldsets = (
        (None, {
            'classes': ('wide',),
            'fields': ('username', 'email', 'password1', 'password2'),
        }),
    )

    readonly_fields = ('last_login', 'date_joined', 'username', 'is_superuser')

class PlayerAdmin(admin.ModelAdmin):
    list_display = ('user', 'wins', 'wins_pvp', 'wins_tournament', 'xp',)
    list_filter = ('user',)
    search_fields = ('user__username', 'user__email',)

admin.site.register(CustomUser, CustomUserAdmin)
admin.site.register(Player, PlayerAdmin)

The CustomUserAdmin class is an admin class for the CustomUser model:

The PlayerAdmin class is an admin class for the Player model. It customizes the admin interface for the Player model and specifies how the player data is displayed and managed on the admin site. The attributes used in this class include list_display, list_filter, and search_fields.

To enable the Admin site, is necessary to add this information in urls.py:

from django.contrib import admin

urlpatterns = [
    ...
    path('admin/', admin.site.urls),
]

To access the Admin site, it is necessary to create a superuser account. To make this, execute docker-compose exec dj python manage.py createsuperuser and fill in the required information.

If we navigate to http://localhost/admin/ we can se something like this: 4

If we click on "Users": 5

If we click on "Players" and click on the "Plus" icon: 6

⚠️ A good test is to check what happens if we try to create a player who has already an account related to a CustomUser row. This is the result: 7

davidcr01 commented 1 year ago

Update Report

Editing the username

A new logic has been created to avoid changing the username of the CustomUser information or the Player information. This may have security consequences and it is not a good idea to edit the primary key of a model, although an error would be generated in case that primary key already exists.

To manage this, a new function has been added to the CustomUserAdmin and PlayerAdmin classes.

def get_readonly_fields(self, request, obj=None):
        if obj:  # Edition of an existing user
            return self.readonly_fields + ('username',)
        return self.readonly_fields  # Creation of a new user

The method checks if the object objis present, which indicates that an existing user is being edited. In that case, the tuple of read-only fields, including username, is returned. If obj is not present, indicating that a new user is being created, the tuple of read-only fields is returned excluding username.

Securizing the password

Testing the POST request, it seems that the password was being stored in plain text. However, if the user was created by the Admin site, it was encrypted successfully.

To solve this, a new logic has been added to the CustomUser serializer:

def create(self, validated_data):
        password = validated_data.pop('password')
        validated_data['password'] = make_password(password)  # Encrypt the password
        return super().create(validated_data)

This overrides the create function created by default by the ModelSerializer class. In this snippet, the password is extracted from the validated dictionary (validated_data), and the make_password() function is used to encrypt it before assigning it back to the password field in validated_data. We then call the create() method of the serializer's base class to perform user creation with the encrypted password.

Now, if we make a GET request to a user, the password field is not returned. This is very important to the security of the app. The password is encrypted and hidden.

8

9