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

Register page (S1/S2) #5

Closed davidcr01 closed 1 year ago

davidcr01 commented 1 year ago

Description

The register page will display all the necessary fields to create an user. The form should be validate before inserting the user in the backend. To register the administrators (event managers), they will have to insert a staff code given by the superuser. This is a straightforward security method to register authorized staff members. To perform this, is necessary to create a new model that stores some codes.

Tasks

⚠️ The registration of administrators (EM) is done in this issue which is related to Sprint 2, but due to design reasons it has been done in this Sprint (1)

Some of the tasks in the description were done in previous issues:

davidcr01 commented 1 year ago

Update Report

Mockups

A mockup has been done to represent the idea of the pages. The mockup is done in Figma:

2

The staff code will be an optional input. If the user fills it, it means that the user wants to be an administrator.

Staff codes

The staff codes will be used only when registering administrators (EM, event managers). These codes will be delivered by the superuser of the platform.

To represent this information, a new model has been created:

class StaffCode(models.Model):
    code = models.CharField(max_length=20)
    used = models.BooleanField(default=False)

    def __str__(self):
        return self.code

Just added a field used to control if the code has been used or not. This does not mean that a code can be used more than one time. But is a good idea to deny the use of the same code in a short period of time due to security reasons.

To apply these changes, create the migrations and migrate with:

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

To apply this logic, it is necessary to modify the serializer of the CustomUser, specifically the create method.

The following code:

class CustomUserSerializer(serializers.ModelSerializer):
    password = serializers.CharField(write_only=True)
    ...
    def validate_staff_code(self, value):
        if value and not StaffCode.objects.filter(code=value, used=False).exists():
            raise serializers.ValidationError('Invalid staff code.')
        return value

    def create(self, validated_data):
        password = validated_data.pop('password')
        staff_code = validated_data.pop('staff_code', None)

        validated_data['password'] = make_password(password)  # Encrypt the password

        # These fields are ignored of the body
        validated_data.pop('is_staff', None)
        validated_data.pop('is_superuser', None)

        is_staff = False
        if staff_code:
            staff_code_obj = StaffCode.objects.get(code=staff_code, used=False)
            staff_code_obj.used = True
            staff_code_obj.save()
            is_staff = True

        user = CustomUser.objects.create(is_staff=is_staff, **validated_data)

        return user

Let's test the behavior of this new logic:

✔️ If the code exists, the user is created:

{
    "username": "admin",
    "password": "test123test",
    "email": "admin@gmail.com",
    "staff_code": "819493"
}

Result:

{
  "id": 29,
  "is_superuser": false,
  "username": "admin",
  ....
  "is_staff": true,
  "groups": [
    3
  ],
}

Notice that the group is automatically assigned due to #9

✔️ If we try to create another user with the same code, we got this error

{
  "staff_code": [
    "Invalid staff code."
  ]
}

✔️ If we try to create another user without a staff code and specify the is_staff field, the user will be created but NOT as a staff member.

{
    "username": "admin4",
    "password": "test123test",
    "email": "admin@gmail.com",
    "is_staff": "True"
}
  "id": 31,
  "is_superuser": false,
  "username": "admin4",
  "is_staff": false,
  "groups": [],

With this, the staff members are created with a staff code mandatorily.

davidcr01 commented 1 year ago

Update Report

Register page

⚠️ Please notice that the dockerized frontend, in this case, Ionic Framework, is not updated with the code changes, so these changes will not be reflected immediately. To see the changes is necessary to rebuild the image. To solve this and avoid rebuilding the image, the development will be done locally by serving the Ionic service with the ionic serve command. After the development, it will be necessary to rebuild the Docker image to apply the changes.

Connection with the backend

From Ionic, if we try to make a request to the backend, we will get this error:

Access to XMLHttpRequest at 'http://localhost/api/users/' from origin 'http://localhost:8100' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

CORS is a security mechanism implemented in web browsers to prevent requests made from an origin (domain, protocol or port) different from the requested resource.

To fix this, we need to configure our project in DRF to accept CORS requests from Ionic.

In the settings.py file, we add the following configuration:

INSTALLED_APPS = [
     ...
    'corsheaders',
]
MIDDLEWARE = [
  ...
 'corsheaders.middleware.CorsMiddleware',
 'django.middleware.common.CommonMiddleware',
]
## Conection with the FrontEnd

CORS_ALLOWED_ORIGINS = [
    'http://localhost:8100', # Ionic in local (dev)
    'http://localhost:8080', # Ionic in Docker
]

And before applying the changes, make sure that the environment has the corsheaders package. In our case, execute the following command to install it:

docker-compose exec dj python -m pip install django-cors-headers

davidcr01 commented 1 year ago

Update Report

Generating the register page

Ionic provides some utilities to create a page and link it to the project. To easily create the logic of the register page, we can use the command ionic g page pages/register. It defines that we are going to generate (g) a page inside the folder pages called register. When this command is executed, some files are created:

In addition to these files, the command may also update other files, such as the app module (app.module.ts) to import and declare the newly generated page/component.

Just with this, a new page is available in our project by navigating to http://localhost:<port>/register

HTML body

First, is needed to create the HTML body of this page. Every Ionic template should use the tags ion-header and ion-content. The first one represents the header of the page, and the second one, the body. In this case, the header is not necessary:

<ion-content>
  <div class="flex-center">
    <ion-card style="border-radius: 10px;">
      <ion-card-header>
        <img src="assets/logo_no_background.png" alt="App Logo" class="logo">
      </ion-card-header>

This snippet starts with the ion-content tag, and it encapsulates everything in a div which is defined in the register.page.scss, that centers everything using flex.

Besides, it uses an ion card to show the rest of the information. This is a pre-build Ionic component. Please see https://ionicframework.com/docs/components

In the header of the card, we insert the logo of the project.

<form [formGroup]="registerForm" (ngSubmit)="onSubmit()">
      <ion-card-content>
      <ion-item>
        <ion-label position="floating">Username</ion-label>
        <ion-input type="text" formControlName="username" required></ion-input>
      </ion-item>
      <div *ngIf="registerForm.get('username')?.invalid && registerForm.get('username')?.touched"  >
        Username is required. Min 4 characters.
      </div>

The rest of the HTML continues with the register form. The content of the form is encapsulated in the ion-card-content, the pair of the ion-card-header tag.

The form will be called registerForm, and it must have the same name in the register.page.ts file. For each field, we define an ion-label and ion-input tags. These tags define the name of the labels and personalize the received input.

Besides, it is defined a div below the ion-item tag that is shown then the field is invalid and it is touched by the user. This condition is represented inside the *ngIfparameter.

At the end of the form, we have this snippet:

<div class="button-container">
        <ion-button class="purple-button" expand="block" type="submit" [disabled]="isLoading">
          <ng-container *ngIf="!isLoading; else loadingIcon">
            Register
          </ng-container>
          <ng-template #loadingIcon>
            <ion-spinner name="dots"></ion-spinner>
          </ng-template>
        </ion-button> 
      </div>
      <div *ngIf="successMessage" class="success-message">{{ successMessage }} <ion-icon name="checkmark-outline"></ion-icon> </div> 
      <div *ngIf="errorMessage" class="error-message" >{{ errorMessage }} <ion-icon name="close-outline"></ion-icon></div>

TS logic

This file uses a ReactiveForm, a way to manage forms in Ionic.

export class RegisterPage implements OnInit{
  registerForm: FormGroup;
  successMessage: string = '';
  errorMessage: string = '';
  isLoading: boolean = false;
  isStaff: boolean = false;

First, we define the variables this component is going to use. The registerForm variable will be a FormGroup type.

constructor(public formBuilder: FormBuilder, private http: HttpClient) {}

Second, we define that the component will use a public formBuilder and a private HTTP object.

ngOnInit() {
    // Define the fields of the form
    this.registerForm = this.formBuilder.group({
      username: ['', [Validators.required, Validators.minLength(4)]],
      email: ['', [Validators.required, Validators.email]],
      first_name: ['', Validators.required],
      last_name: ['', Validators.required],
      password: ['', [Validators.required, Validators.minLength(6)]],
      staff_code: ['', [Validators.pattern('^[0-9]+$')]]
    });
  }

The ngOnInit method is executed the first time the component is loaded. In this case, it just defines the fields of the HTML form, validating the necessary fields.

onSubmit() {
    if (this.registerForm.invalid) {
      this.successMessage = '';
      this.errorMessage = 'Please fill the form correctly.';
      return;
    }

    const userData = {
      username: this.registerForm.get('username').value,
      email: this.registerForm.get('email').value,
      first_name: this.registerForm.get('first_name').value,
      last_name: this.registerForm.get('last_name').value,
      password: this.registerForm.get('password').value,
    };

    const staffCode = this.registerForm.get('staff_code').value;

    // Case of registering a player. The 'staff_code' field is not added
    if (staffCode === '') {
      this.createUser('http://localhost/api/players/', { user: userData });
    } 
    else { // Case of registering an admin. 'staff_code' field is added
      userData['staff_code'] = staffCode;
      this.createUser('http://localhost/api/users/', userData);
    }
  }

When the form is submitted, we filter the received fields and call the API depending on the staff_code field. If it exists, we will attempt to create an admin, if not, a player.

To control this condition, we use the following createUser method, which receives the URL and the body (the fields of the form)

createUser(url: string, body: any) {
    this.isLoading = true;

    this.http.post(url, body)
      .subscribe(
        (response) => {
          console.log('User created successfully', response);
          this.successMessage = 'User created successfully';
          this.errorMessage = '';
          this.registerForm.reset();
          this.isLoading = false;
        },
        (error) => {
          const errorContent = error.error;

          // Thrown errors could be of the staff code or the username
          if (errorContent.staff_code) {
            this.errorMessage = errorContent.staff_code;
          } else if (errorContent.username) { // Case of creating an admin
            this.errorMessage = errorContent.username;
          } else if (errorContent.user && errorContent.user.username) { // Case of creating a player
            this.errorMessage = errorContent.user.username[0];
          } else {
            this.errorMessage = 'Error creating user';
          }
          this.isLoading = false;
        }
      );
  }

It calls the specified URL and shows a successful or error message depending on the API response. These messages will be used in the HTML file.

⚠️ To use the ReactiveFormsModule and the HttpClientModule, is necessary to import them into the register.module.ts:

import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';

...

@NgModule({
  imports: [
    FormsModule,
    ReactiveFormsModule,
    HttpClientModule,
    ...
  ],
  declarations: [RegisterPage]
})
...

Testing

If we navigate to the inputs of the form, we notice that:

3

✔️ If we do not fill the form correctly, a message below the form is generated:

4

✔️ If we fill the form correctly and do not specify the staff code, a player will be created and a successful message will be generated. The form will be cleaned too.

5

6

7

✔️ If we fill the form correctly and specify the staff code, an admin will be created and a successful message will be generated. The form will be cleaned too.

8

✔️ If the username is already taken, the form will generate an error message. Same thing to the staff_code:

9 10