Lesson 14

Login and Registration

Enabling user registration and authentication in the application

PRO

Lesson Outline

Login and Registration

In the last lesson, we set up a simple Node/Express server that uses SuperLogin to facilitate user registration and authentication. In the same way that we can interact directly with CouchDB using an HTTP API (but we use PouchDB API instead), we can also interact with SuperLogin's functionality through HTTP requests.

We are going to walk through implementing this in our application in this lesson, but to give you the basic idea, if we wanted to create a new user we would make a POST request like this:

this.http.post(SERVER_ADDRESS + "auth/register", details);

or if we wanted to validate that a particular username was valid, we would make a GET request like this:

return this.http.get(SERVER_ADDRESS + "auth/validate-username/" + username);

In this lesson, we will be creating two new services to help us deal with the integration with SuperLogin, and then we will also be adding two new pages to handle logging in and account creation.

User Service

We are going to create a User service that we will be able to use throughout the application to access the currently logged in user's details. This is going to be a very simple service, all it will do is keep a reference to the currently logged in users data, and save and fetch that data from storage.

As we did for our Chat and Notice types, we will also create an interface for a User.

Create a file at src/app/interfaces/user.ts and add the following:

export interface User {
  issued: number;
  expires: number;
  provider: string;
  ip: string;
  token: string;
  password: string;
  user_id: string;
  roles: string[];
  userDBs: {
    ["hangz-app"]: string;
  };
}

NOTE: We are using ["hangz-app"] here because our database name is hyphenated, otherwise we could have just done something like hangz: string

If you are wondering how I came up with these properties for the User object, you can see the structure of the data that is returned by SuperLogin in the documentation. A response from SuperLogin might look like this:

{
  "issued": 1440232999594,
  "expires": 1440319399594,
  "provider": "local",
  "ip": "127.0.0.1",
  "token": "aViSVnaDRFKFfdepdXtiEg",
  "password": "p7l9VCNbTbOVeuvEBhYW_A",
  "user_id": "joesmith",
  "roles": ["user"],
  "userDBs": {
    "supertest": "http://aViSVnaDRFKFfdepdXtiEg:[email protected]:5984/supertest$joesmith"
  }
}

Create the service with the following command:

ionic g service services/User

Modify src/app/services/user.service.ts to reflect the following:

import { Injectable } from '@angular/core';
import { Storage } from '@ionic/storage-angular';

import { User } from '../interfaces/user';

@Injectable({
  providedIn: 'root',
})
export class UserService {
  public currentUser: User;
  private _storage: Storage;
  private storageInitialised: boolean = false;

  constructor(private storage: Storage) {}

  async init() {
    const storage = await this.storage.create();
    this._storage = storage;
    this.storageInitialised = true;
  }

  async saveUserData(data: User): Promise<void> {
    if (!this.storageInitialised) {
      await this.init();
    }
    this.currentUser = data;
    this._storage.set('hangzUserData', data);
  }

  async getUserData(): Promise<User> {
    if (!this.storageInitialised) {
      await this.init();
    }

    return this._storage.get('hangzUserData');
  }
}

We have a class member called currentUser that we will use to store the currently logged in user's data, and we will be able to access that from anywhere in the application directly through this provider.

We also have a saveUserData method that we will pass the user's data into, this will set the currentUser member and also save the data to local storage. The getUserData method will retrieve any existing user data that has been saved to storage.

Auth Service

Now we are going to create the Auth service that will handle the integration with SuperLogin, this one is a little bit more complex but it's still reasonably straightforward - SuperLogin already does most of the heavy lifting for us.

Create the provider with the following command:

ionic g service services/Auth

We are going to create a couple of new interfaces now as well. We will create an interface to represent the data object containing a user's credentials when they are attempting to log in, and an interface to represent the data required to create a new user account.

Create a file at src/app/interfaces/credentials.ts and add the following:

export interface Credentials {
  username: string;
  password: string;
}

Create a file at src/app/interfaces/registration-details.ts and add the following:

export interface RegistrationDetails {
  username: string;
  email: string;
  password: string;
  confirmPassword: string;
}

Modify src/app/services/auth.service.ts to reflect the following:

import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { NavController } from '@ionic/angular';
import { UserService } from './user.service';
import { DataService } from './data.service';
import { SERVER_ADDRESS } from '../../environments/environment';
import { Observable } from 'rxjs';

import { Credentials } from '../interfaces/credentials';
import { RegistrationDetails } from '../interfaces/registration-details';
import { User } from '../interfaces/user';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  constructor(
    private http: HttpClient,
    private userService: UserService,
    private dataService: DataService,
    private navCtrl: NavController
  ) {}

  authenticate(credentials: Credentials): Observable<User> {
    return this.http.post<User>(`${SERVER_ADDRESS}/auth/login`, credentials);
  }

  async logout(): Promise<void> {
    const headers = new HttpHeaders();

    headers.append(
      'Authorization',
      `Bearer ${this.userService.currentUser.token}:${this.userService.currentUser.password}`
    );

    this.http
      .post(`${SERVER_ADDRESS}/auth/logout`, {}, { headers: headers })
      .subscribe(() => {});

    try {
      await this.dataService.db.destroy();
      this.dataService.db = null;
      this.userService.saveUserData(null);
      this.navCtrl.navigateRoot('/login');
    } catch (err) {
      console.log(err);
      console.log('could not destroy db');
    }
  }

  register(details: RegistrationDetails): Observable<User> {
    return this.http.post<User>(`${SERVER_ADDRESS}/auth/register`, details);
  }

  validateUsername(username: string): Observable<Object> {
    return this.http.get(
      `${SERVER_ADDRESS}/auth/validate-username/${username}`
    );
  }

  validateEmail(email: string): Observable<Object> {
    const encodedEmail = encodeURIComponent(email);

    return this.http.get(
      `${SERVER_ADDRESS}/auth/validate-email/${encodedEmail}`
    );
  }
}

Although this is not the limit of what SuperLogin can do, this service facilitates all of the integrations we need which are:

  • Logging In
  • Logging out
  • Account Creation
  • Validating usernames
  • Validating emails

The authenticate function accepts a credentials object that will contain the user supplied username and password. It then sends a POST request with this data to the auth/login route.

The logout function makes a request to the auth/logout route, but it in order to access protected routes that SuperLogin provides (ones that require a user to be logged in) we must send a Bearer token. To do this, we create a header with the current user's token and password.

IMPORTANT: I want to make it extremely clear that the password field here is NOT the user's actual password, it is a temporary password/token that SuperLogin provides for accessing the database. This is the only reason that we are storing this password in local storage, you should never store the user's actual password in local storage. Generally speaking, you should only ever be storing the user's password as a hash of the original password on the server side. Fortunately, we have no need to do anything with the user's password at all as SuperLogin handles it all for us.

Making this POST request will destroy the user's session with SuperLogin, then we also destroy the local PouchDB database, delete the current user data, and then force them back to the Login page.

The register function is more or less the same as the authenticate function, except that we post the data to the auth/register route and we include a few more details. Instead of just a username and a password, we will supply a username, email, password, and confirmPassword (more on this later).

The final validateUsername and validateEmail functions just make a GET request to their respective routes, and supply the username or email in the URL. SuperLogin will send a response back indicating whether or not the username or email is valid and not already in use.

You may have noticed that we are using SERVER_ADDRESS in place of an actual address, and we import this at the top of the file. Since we are going to be switching between our local server and a remote server, we will be creating a SERVER_ADDRESS constant so that we only ever need to update it in one place.

Update src/environments/environment.ts to reflect the following:

export const environment = {
  production: false,
};

export const SERVER_ADDRESS = "http://localhost:8080";

You can also update the environment.prod.ts file to provide an alternate SERVER_ADDRESS for production builds.

This is the address of our local server - as you may recall we have this line in the index.js file for our server:

app.listen(process.env.PORT || 8080);

Which will listen on port 8080 if the PORT environment variable is not defined (this will become relevant later). As long as we have the server running locally, we will be able to interact with it through: http://localhost:8080.

Login Page

We've got our services to handle all of the backend stuff for us, now we just need to implement them in our interface. As of right now, the application just launches straight into the notices page and doesn't require any kind of authentication. This is obviously going to need to change, so we are going to create a login page that users will be taken to initially. From here they will be able to either log in or create a new account and then log in. In the next lesson, we will also be implementing a method to automatically re-authenticate users who have already logged in previously.

Run the following command to create the login page:

ionic g page Login

Currently, we have our application set up so that our home page is the default page. We are going to modify this so that the Login page is the default page, and we are going to handle database initialisation as part of the login process now (instead of in our root component).

Modify src/app/app.component.ts to reflect the following:

import { Component } from '@angular/core';
import { SplashScreen } from '@capacitor/splash-screen';
import { StatusBar } from '@capacitor/status-bar';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.scss'],
})
export class AppComponent {
  constructor() {
    this.initializeApp();
  }

  initializeApp() {
    SplashScreen.hide().catch((err) => {
      console.warn(err);
    });

    StatusBar.hide().catch((err) => {
      console.warn(err);
    });
  }
}

Modify src/app/app-routing.module.ts to reflect the following:

import { NgModule } from '@angular/core';
import { PreloadAllModules, RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: '',
    redirectTo: 'login',
    pathMatch: 'full',
  },
  {
    path: 'home',
    loadChildren: () =>
      import('./home/home.module').then((m) => m.HomePageModule),
  },
  {
    path: 'login',
    loadChildren: () =>
      import('./login/login.module').then((m) => m.LoginPageModule),
  },
];

@NgModule({
  imports: [
    RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }),
  ],
  exports: [RouterModule],
})
export class AppRoutingModule {}

Now let's implement the login page itself.

Modify src/app/login/login.page.ts to reflect the following:

PRO

Thanks for checking out the preview of this lesson!

The full version of this lesson is only available to pro members. If you would like full access to this module and all of the other pro modules on Elite Ionic you can become a pro member (or log in if you are already a member).