Closed davidcr01 closed 1 year ago
A mockup has been done to represent the idea of the pages. The mockup is done in Figma:
The staff code will be an optional input. If the user fills it, it means that the user wants to be an administrator.
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:
staff_code
field as write_only (not editable) and not required.validate_staff_code
that checks if the code passed in the body exists and it's not used.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.
⚠️ 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.
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
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:
page-name.module.ts: This file is a module file that declares and exports the page/component. It is responsible for managing the page's dependencies, providers, and routing.
page-name.page.ts: This is the TypeScript file that contains the logic and functionality for the page/component. You can define properties, methods, and lifecycle hooks in this file.
page-name.page.html: This file is an HTML template that represents the structure and layout of the page/component. You can define the user interface elements, bindings, and event handlers in this file.
page-name.page.scss: This file contains the page/component's specific styles written in SCSS (Sass) syntax. You can customize the appearance of the page/component by adding CSS rules and styles in this file.
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
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 *ngIf
parameter.
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>
ng-container
and ng-template
tags. ng-container
and ng-template
are Angular structural directives that allow you to control the rendering and reuse of elements in the DOM without adding additional HTML elements.register.page.ts
file tooThis 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]
})
...
If we navigate to the inputs of the form, we notice that:
Validators
tooldiv *ngIf
defined in every ion-item
✔️ If we do not fill the form correctly, a message below the form is generated:
✔️ 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.
✔️ 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.
✔️ If the username is already taken, the form will generate an error message. Same thing to the staff_code:
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: