Sign in or sign out users with Ionic 4 and Django

DAY 5 - August 2019

This tutorial is quite old, you should read the new better one: How to secure Ionic application and Django API with JWT authentication ?

In previous tutorials, we have seen how to implement an API and secure it. So now everything is setup, we are ready to register or login the users of our application. We will also see how to implement the forgotten password email. As a reminder, i'm not a designer so our Ionic pages that we will implement will be very simple. But you can purchase ready to use template on website such as Hybapps.com.

Create a login page with Ionic 4

First, we will add our page to our Ionic project.

ionic g page LoginPage

Then as we have seen in previous tutorial, i will move the new created directory login-page inside the subdirectory pages and will reflect the change in the app-routing.mobule.ts file.

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';
const routes: Routes = [
  { path: '', loadChildren: './pages/tabs/tabs.module#TabsPageModule' },
  { path: 'login-page', loadChildren: './pages/login-page/login-page.module#LoginPagePageModule' }
];
@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })
  ],
  exports: [RouterModule]
})
export class AppRoutingModule {}

In the previous tutorial, we have implemented a method accessAuthorizedWithUrl in our app.component.ts file. This should be a good start to redirect our user to the login page.

import { Component } from '@angular/core';
import { Storage } from '@ionic/storage';
import { Platform } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { ApiDjangoService } from '../app/services/api-django.service'
import { Router } from '@angular/router';
@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html'
})
export class AppComponent {
  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    public apiService: ApiDjangoService,
    public storage : Storage,
    public router: Router
  ) {
    this.initializeApp();
  }
  accessAuthorizedWithUrl() {
    this.router.navigateByUrl("/login-page")
  }

To redirect the user, we will use the Router component provided by Ionic. So we import it :

import { Router } from '@angular/router';

And then inside our accessAuthorizedWithUrl() method, we tell the router to go to our login page like this:

 this.router.navigateByUrl("/login-page")

If you launch the Ionic project with ionic serve, you should see a blank page with a title : LoginPage, which mean that the redirection has been made successfully.
Let's edit the code of our login-page-page.html to add two input fields, one for the email and one for the password. We will also add a link to click on, if the user wants to receive his password by email. And finally we add two buttons : one for login and the other one to go on the register page. Here is the code:

<ion-content>
  <div class="auth-container">
     <div class="auth-body">
       <div class="logo">
         <b>Bike Application</b>
       </div>
       <ion-list lines="full" mode="ios" floating-form>
       <div class="buttons">
        <form (ngSubmit)="connect()" #registerForm="ngForm">
         <ion-item>
           <ion-label position="floating">E-mail</ion-label>
           <ion-input type="email" name="email"  [(ngModel)]="registerCredentials.email" required></ion-input>
         </ion-item> 
         <ion-item>
           <ion-label position="floating">Password</ion-label>
           <ion-input type="password" name="password" [(ngModel)]="registerCredentials.password" required></ion-input>
         </ion-item>
       <ion-button mode="ios" size="small" fill="outline" color="dark" expand="block" type="submit">Login</ion-button>
      </form>
       <a href="#" (click)="forgotPassword()">Password forgotten</a>
       <ion-button mode="ios" size="small" fill="outline" color="dark" expand="block" (click)="subscribe()">Subscribe</ion-button>
       </div>
       </ion-list>
     </div>
   </div>
 </ion-content>

Now we will write the code of the login-page.page.ts but before that we will need an external library (crypto-js) to encrypt/decrypt the password. So let's install it by typing:

npm install --save crypto-js

We will also need some useful methods such as showing a loading message while doing a request to our backend, our showing that there is no network, or other messages. We could implement each time this methods in our pages, but it will be a code duplication. Instead of this, i'm going to modify our ApiDjangoService and add those methods:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Storage } from '@ionic/storage';
import { Observable } from 'rxjs';
import { catchError, retry } from 'rxjs/operators';
import { AlertController ,LoadingController} from '@ionic/angular';
@Injectable({
  providedIn: 'root'
})
export class ApiDjangoService {
  loader:any;
  tokenSSO: String = "";
  expireDate: any;
  networkConnected: boolean = true;
  virtualHostName: string = "https://app-b86ed9a3-cfa5-4d79-a572-890b5a4e545f.cleverapps.io/"
  oAuth2ClientId: string = 'VbXH6Jwp7FSMqgL9fuXn3Qo6dI57BEYH1FGEMQPF';
  oAuth2ClientSecret: string = 'Vur9PaOf39LaWEWxIAitTUrvbQOCDegCUpwCPCmKqcbZDz5FhbTKfYeI0IUBFvY4g3hxf4CctEWzw2KJrOfEsAVTDQ2z4LPImoRoGxyzZMmulswIwNLJHRIK8JIfTD93';
  oAuth2Username: string = 'csurbier@idevotion.fr'; // Set your oAuth2 username
  oAuth2Password: string = 'eclipse'; // Set your oAuth2 password
  appName: string = 'bikeapplication';
  apiPrefix = "/api"
  getOauthUrl = this.virtualHostName + "/o/token/";
  getUserUrl = this.virtualHostName + this.apiPrefix + "/users/"
  constructor(public http: HttpClient,
    public loadingController: LoadingController,
    public alertCtrl: AlertController,
    public storage: Storage) {
  } 

We import all classes mandatories (AlerController, LoadingControllers) and we initialize them in our constructor method. Then we can write the methods that we will use everywhere in our application.


  async showLoading() { 
    this.loader = await this.loadingController.create({
      message:  'Please wait',
      duration: 4000
    });
    return await this.loader.present(); 
  }
   public async showLoadingMessage(message) {
    this.loader = await this.loadingController.create({
      message: message,
    });
    this.loader.present();
  }
  async stopLoading() { 
    if (this.loader){
      this.loader.dismiss()
    } 
  }
  async showNoNetwork() {
    let alert = await this.alertCtrl.create({
      header: 'Sorry',
      message: 'No network detected. Please check your internet connexion',
      buttons: ['OK']
    }); 
    return await alert.present(); 
  }
  async showError(text) {
    let alert = await this.alertCtrl.create({
      header: 'Error',
      message: text,
      buttons: ['OK']
    });
    return await alert.present();
  }
  async showMessage(title, message) {
    let alert = await this.alertCtrl.create({
      header: title,
      message: message,
      buttons: ['OK']
    });
    return await alert.present();
  }

Ok now let's write our login-page.page.ts

import { Component, OnInit } from '@angular/core';
import { Platform } from '@ionic/angular';
import { ApiDjangoService } from '../../services/api-django.service';
import { Router } from '@angular/router';
import CryptoJS from 'crypto-js';
@Component({
  selector: 'app-login-page',
  templateUrl: './login-page.page.html',
  styleUrls: ['./login-page.page.scss'],
})
export class LoginPagePage implements OnInit {
  registerCredentials = { email: '', password: '' };
  constructor(
    public apiService: ApiDjangoService,
    public router: Router) {
  }
  ngOnInit() {
  }
  public connect(): void {
    if (this.apiService.networkConnected) {
      this.apiService.showLoading();
      let password = CryptoJS.SHA256(this.registerCredentials.password).toString(CryptoJS.enc.Hex);
      let queryPath = "?email=" + this.registerCredentials.email + "&password=" + password;
      this.apiService.findUser(queryPath).subscribe((listUsers) => {
        if (listUsers) {
          console.log("Find " + JSON.stringify(listUsers));
        }
        else {
          this.apiService.stopLoading();
          this.apiService.showError("Wrong login or password.");
        }
      })
    }
    else {
      this.apiService.showNoNetwork();
    }
  }
  public forgotPassword(): void {
    this.router.navigateByUrl("/forgot-password")
  }
  public subscribe() {
    this.router.navigateByUrl("/register")
  }
}

Ok let's dive in. On our html page, we define a form which will store our email and password in a dictonary called registerCredentials

 <form (ngSubmit)="connect()" #registerForm="ngForm">
         <ion-item>
           <ion-label position="floating">E-mail</ion-label>
           <ion-input type="email" name="email"  [(ngModel)]="registerCredentials.email" required></ion-input>
         </ion-item> 
         <ion-item>
           <ion-label position="floating">Password</ion-label>
           <ion-input type="password" name="password" [(ngModel)]="registerCredentials.password" required></ion-input>
         </ion-item>

This dictionnary is declared in our typescript page

registerCredentials = { email: '', password: '' };

Once submited the form will call a method connect(). To submit the form we added a button

 <ion-button mode="ios" size="small" fill="outline" color="dark" expand="block" type="submit">Login</ion-button>
      </form>

In our connect() method, first thing to do is to check if we have the network (mobile can lost network while moving).

  if (this.apiService.networkConnected) {

If no network is available, we use one of the method we declare before, to warn the user:

 else {
      this.apiService.showNoNetwork();
    }

Ok cool, but how the ApiDjangoService can know if there is network or not. We need to implement that. To do that, we need to use and install a plugin named Network. Just write on your terminal console:

ionic cordova plugin add cordova-plugin-network-information
npm install @ionic-native/network

Then we can declare this plugin in our app.module.ts file:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { IonicModule, IonicRouteStrategy } from '@ionic/angular';
import { SplashScreen } from '@ionic-native/splash-screen/ngx';
import { StatusBar } from '@ionic-native/status-bar/ngx';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import {ApiDjangoService} from './services/api-django.service'
import { HttpClientModule } from '@angular/common/http';
import { Network } from '@ionic-native/network/ngx';
import { IonicStorageModule } from '@ionic/storage';
@NgModule({
  declarations: [AppComponent],
  entryComponents: [],
  imports: [BrowserModule,
     IonicModule.forRoot(), 
     IonicStorageModule.forRoot(),
     HttpClientModule,
     AppRoutingModule],
  providers: [
    StatusBar,
    SplashScreen,
    Network,
    { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
    ApiDjangoService,
  ],
  bootstrap: [AppComponent]
})
export class AppModule {}

And import it in our app.component.ts file and inject it in the constructor method:

import { Network } from '@ionic-native/network/ngx';
@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html'
})
export class AppComponent {
  constructor(
    private platform: Platform,
    private splashScreen: SplashScreen,
    private statusBar: StatusBar,
    public apiService: ApiDjangoService,
    public storage : Storage,
    public network : Network,
    public router: Router
  ) {
    this.initializeApp();
  } 

Now we will use this Network library to check the network status while user is using the application. To do this, we write the method:

 watchNetwork() {
    console.log("================= WATCH NETWORK APP COMPONENT "+this.apiService.networkConnected+" ? ============")
    // watch network for a connection
    this.network.onConnect().subscribe(() => {
      console.log('=====>APP COMPONENT network connect :-(');
      this.apiService.networkConnected = true 
      setTimeout(() => {
        if (this.network.type === 'wifi') {
          console.log('we got a wifi connection, woohoo!');
          this.apiService.networkConnected = true 
        }
      }, 3000);
    });
    // watch network for a disconnect
    this.network.onDisconnect().subscribe(() => {
      console.log('=====>APP COMPONENT network was disconnected :-(');
      this.apiService.networkConnected = false; 
    });
  }

It's pretty simple, we subscribe to onConnect() and onDisconnect() events of this library, and we set our networkConnected boolean value in our ApiDjangoService. And of course we need to call this watchNetwork() method while initializing our app:


  initializeApp() {
    this.platform.ready().then(() => {
      this.statusBar.styleDefault();
      this.splashScreen.hide();
      console.log("========== INIT APP ==============")
      // check if we have a token to use API
      this.apiService.checkOauthToken().then((result) => {
        if (result) { 
          this.watchNetwork()
          this.accessAuthorizedWithUrl()
        }
        else { 
          this.getToken()
        }
      });
    });
  }

Ok now back to our connect() method of our login page. If we have network, we need to check in our backend if we have a user already existing with the email/password provided in the form.

 this.apiService.showLoading();
      let password = CryptoJS.SHA256(this.registerCredentials.password).toString(CryptoJS.enc.Hex);
      let queryPath = "?email=" + this.registerCredentials.email + "&password=" + password;
      this.apiService.findUser(queryPath).subscribe((listUsers) => {
      this.apiService.stopLoading();
        if (listUsers) {
          console.log("Find " + JSON.stringify(listUsers));
        }
        else {
          this.apiService.stopLoading();
          this.apiService.showError("Wrong login or password.");
        }
      })

First we show a loader to the user (since the request could take some time), then i encrypt the entered password in SHA256 (we will discuss about this later), and then i'm building the query path with two parameters (email and password) before calling a new API findUser. This API will send back the user (if found) otherwise it means we have no user with such an email/password, and i display an error message.
Nothing really complicated. But now we need to implement this new API findUser in our django backend, before going back to the Ionic method.

If you remember, in this tutorial, we learn how to create an API and filter data. In our api/views.py of our Django project, we declare a Class to get users and/or filter with by the attributes email or username. As a reminder:


class UserListCreateView(generics.ListCreateAPIView):
    """
            create:
                add users
            get:
                Search or get users
                You can search using:
                    :param email
                    :param username
    """
    permission_classes = [permissions.IsAuthenticated]
    queryset = User.objects.all()
    serializer_class = UserSerializer
    filter_backends = (DjangoFilterBackend,)
    filterset_fields = ('email', 'username')

So we just need to modify the filterset_fields to tell that we want to be able to filter by password too :

filterset_fields = ('email', 'username','password')

Now if you redeploy the django project (on Clevercloud or whatever), you should be able to call the API and specifying the two fields you want to filter on, like this:

https://app-b86ed9a3-cfa5-4d79-a572-890b5a4e545f.cleverapps.io/api/users/?email=csurbier@idevotion.fr&password=toto

Don't forget to pass the oAuth2 authentification as we learn here.

So if you go back to our Ionic code, in our method connect(), we just need to construct the end of the queryPath with our two parameters (email and password). And since, the password is encrypted in SHA256 in our backend database, that's why i use the crypto-js library to encrypt the password entered by our user, in it's SHA256 version, like this:

let password = CryptoJS.SHA256(this.registerCredentials.password).toString(CryptoJS.enc.Hex); 

As a final step for login, we need to write the findUser method in our ApiDjangoService:

findUser(path){
    const options = {
      headers: new HttpHeaders({
        'Authorization': 'Bearer ' + this.tokenSSO,
        'Content-Type': 'application/json'
      })
    }; 
    let url = this.getUserUrl+path;
    return Observable.create(observer => {
      // At this point make a request to your backend to make a real check!
      console.log("on appelle BACKEND encoded url " + url);
      this.http.get(url, options)
        .pipe(retry(1))
        .subscribe(res => {
          observer.next(res);
          observer.complete();
        }, error => {
          observer.next();
          observer.complete();
          console.log(error);// Error getting the data
        });
    });
  }

This method is very similar to the getUsers() method we already made before. Some optimization could be done to avoid duplicate code, but i let you do this if you want too.
So if a user is found with that email and password the listUsers will be fill with one result.

Ok now on next part, we will see how to create our register page for a user.

If you want to kickoff your Ionic and Django project in few minutes, don't hesitate to visit my new website Ionic And Django Kickoff