Part 2: Creating a Multiple User App with Ionic 2, PouchDB & CouchDB

Datetime:2016-08-23 00:52:08          Topic: CouchDB           Share

In thelast tutorial we discussed some strategies for creating a multi user app with PouchDB and CouchDB, specifically in the relation to the todo application created inthis tutorial. If you are unfamiliar with PouchDB and CouchDB I would recommend readingthis post before going any further. In short, PouchDB is used to store data locally and can sync with a remote CouchDB database, which provides two way replication (any changes to either data store will be available instantly everywhere) and support for offline data that syncs when online.

There was a lot to consider in the last tutorial, but in the end we decided that the best structure for turning the single user todo application into a multi user one was to provide each user with their own remote CouchDB database that will sync to their local database. This would look something like this:

In theory it sounds pretty straightforward, but we didn’t get into how to actually implement it. In this tutorial we are going to extend the cloud based todo application that was created previously with Ionic 2, to support multiple users and authentication. We will be using the SuperLogin package to handle authentication for us, as well as setting up secure databases for each user that signs up in the app. In the last tutorial I mentioned security issues that would need to be handled to prevent one user from accessing another users data , and (assuming you’ve disabled the admin party in CouchDB) SuperLogin will handle this for us.

At the end of this tutorial our application should look something like this:

Before We Get Started

Before you go through this tutorial, you should have at least a basic understanding of Ionic 2 concepts. You must also already have Ionic 2 set up on your machine.

If you’re not familiar with Ionic 2 already, I’d recommend reading my Ionic 2 Beginners Guide first to get up and running and understand the basic concepts. If you want a much more detailed guide for learning Ionic 2, then take a look at Building Mobile Apps with Ionic 2 .

It’s also important that you read these tutorials before continuing :

The first tutorial walks through setting up CouchDB and building the original single user todo application, and the second covers the theory behind the solution we will be implementing today. If you’re short on time it is not absolutely necessary to read these tutorials, as I will quickly run through all the code you need to get up to speed in the next section, but it will lack a lot of the instruction and context the original tutorial provides.

1. Generate a New Application

Let’s start off by creating a new Ionic 2 application.

Generate a new application with the following command:

ionic start cloudo-auth blank --v2

We’ll also be using a few pages and a provider in this tutorial, so let’s also generate those now.

Run the following commands to generate the necessary files for the project:

ionic g page Login
ionic g page Signup
ionic g provider Todos

Since we will be adding some styles to this application, let’s make sure we include the .scss files for the new pages we generated.

Modify app.core.scss to include the following imports:

@import "../pages/home/home";
@import "../pages/login/login";
@import "../pages/signup/signup";

and let’s also set up some default colours to use.

Modify the $colors map in app.variables.scss to reflect the following:

$colors: (
  primary:    #95a5a6,
  secondary:  #3498db,
  danger:     #f53d3d,
  light:      #f4f4f4,
  dark:       #222,
  favorite:   #69BB7B
);

This application also has quite a few dependencies, both for the app itself and for the NodeJS server it will be communicating with, so make sure to install all of the following packages before continuing.

Run the following commands to install the dependencies:

npm install pouchdb --save
npm install superlogin --save
npm install morgan --save-dev
npm install express --save-dev
npm install http --save-dev
npm install body-parser --save-dev
npm install cors --save-dev

and finally, we will also need to install some typings for PouchDB.

Run the following command if you do not already have the typings package installed:

npm install -g typings

Run the following command to install the typings for PouchDB

typings install dt~pouchdb/pouch --global --save

2. Create the Pages

Now we're going to start implementing our pages. I'm going to go through this pretty quickly because they are just basic pages with some forms and buttons and so on. If you're after some more details explanations of everything, I would recommend checking out the resources inthis post.

If you haven't already created the Cloudo application from the previous tutorial, I am going to run through the code you will need to set up, so it's not a requirement that you complete the other tutorial first (again, I'd recommend it though). Even if you have completed the previous tutorial, make sure you look through these snippets as well because there are a few minor changes I've made.

For now, we will only be implementing the templates – we will handle all the logic later.

Modify login.html to reflect the following:

<ion-content padding class="login">

    <ion-row class="login-logo">
        <ion-col><img src="http://placehold.it/100x100" /></ion-col>
    </ion-row>

    <ion-row class="login-form">
        <ion-col>
            <ion-list inset>

              <ion-item>
                <ion-label><ion-icon name="person"></ion-icon></ion-label>
                <ion-input [(ngModel)]="username" placeholder="username" type="text"></ion-input>
              </ion-item>

              <ion-item>
                <ion-label><ion-icon name="lock"></ion-icon></ion-label>
                <ion-input [(ngModel)]="password" placeholder="password" type="password"></ion-input>
              </ion-item>

            </ion-list>

            <button (click)="login()" primary class="login-button">Login</button>

        </ion-col>
    </ion-row>

    <ion-row>
        <ion-col>
            <button (click)="launchSignup()" class="create-account">Create an Account</button>
        </ion-col>
    </ion-row>

</ion-content>

Modify login.scss to reflect the following:

.login {

    background-color: map-get($colors, secondary);

    scroll-content {
        display: flex;
        flex-direction: column;
    }

    ion-row {
        align-items: center;
        text-align: center;
    }

    ion-item {
        border-radius: 30px !important;
        padding-left: 10px !important;
        margin-bottom: 10px;
        background-color: #f6f6f6;
        opacity: 0.7;
        font-size: 0.9em;
    }

    ion-list {
        margin: 0;
    }

    .login-logo {
        flex: 2;
    }

    .login-form {
        flex: 1;
    }

    .create-account {
        color: #fff;
        text-decoration: underline;
        background: none;
    }

    .login-button {
        width: 100%;
        border-radius: 30px;
        font-size: 0.9em;
        background-color: transparent;
        border: 1px solid #fff;
    }

}

With the above changes we have created a simple login form which will serve as the initial root page for our application. It just contains a username and password field that the user will use to log in, as well as a Create Account button that will eventually take them to the signup page.

We’ve added some styling, and since it isn’t really the goal of this tutorial I’m not going to talk through those in detail, however I wanted to point out a couple of things that might seem peculiar to some people. We have wrapped all the styles with the .login selector so that this styling only effects the login page (since our <ion-content> has a class of login ). Even though we have added the styles to login.scss specifically, the styles still apply globally. We have also used this syntax:

background-color: map-get($colors, secondary);

which allows us to grab the secondary colour that is defined in the app.variables.scss file, rather than defining it manually. This makes the app much more easily customisable. Now let’s move on to the signup page.

Modify signup.html to reflect the following:

<ion-header>

  <ion-navbar secondary>
    <ion-title>Create Account</ion-title>
  </ion-navbar>

</ion-header>


<ion-content padding class="signup">

    <ion-row class="account-form">
        <ion-col>
            <ion-list inset>

                <ion-item>
                    <ion-label><ion-icon name="person"></ion-icon></ion-label>
                    <ion-input [(ngModel)]="name" placeholder="Name" type="text"></ion-input>
                </ion-item>

                <ion-item>
                    <ion-label><ion-icon name="person"></ion-icon></ion-label>
                    <ion-input [(ngModel)]="username" placeholder="Username" type="text"></ion-input>
                </ion-item>

                <ion-item>
                    <ion-label><ion-icon name="mail"></ion-icon></ion-label>
                    <ion-input [(ngModel)]="email" placeholder="Email" type="email"></ion-input>
                </ion-item>

                <ion-item>
                    <ion-label><ion-icon name="lock"></ion-icon></ion-label>
                    <ion-input [(ngModel)]="password" placeholder="Password" type="password"></ion-input>
                </ion-item>

                <ion-item>
                    <ion-label><ion-icon name="lock"></ion-icon></ion-label>
                    <ion-input [(ngModel)]="confirmPassword" placeholder="Confirm password" type="password"></ion-input>
                </ion-item>

            </ion-list>

            <button (click)="register()" class="continue-button">Register</button>

        </ion-col>
    </ion-row>

</ion-content>

Modify signup.scss to reflect the following:

.signup {

    background-color: map-get($colors, secondary);

    scroll-content {
        display: flex;
        flex-direction: column;
    }

    ion-item {
        border-radius: 30px !important;
        padding-left: 10px !important;
        margin-bottom: 10px;
        background-color: #f6f6f6;
        opacity: 0.7;
        font-size: 0.9em;
    }

    ion-list {
        margin: 0;
    }

    .heading-text {
        flex: 1;
        align-items: center;
        text-align: center;
    }

    .heading-text h3 {
        color: #fff;
    }


    .account-form {
        flex: 1;
    }

    .continue-button {
        width: 100%;
        border-radius: 30px;
        margin-top: 30px;
        font-size: 0.9em;
        background-color: transparent;
        border: 1px solid #fff;
    }

}

This is pretty much the same as the login page, except we have a few more fields since this will be handling the signup process.

Modify home.html to reflect the following:

<ion-header>
    <ion-navbar secondary no-border-bottom>
      <ion-title>
        ClouDO
      </ion-title>
      <ion-buttons start>
        <button (click)="logout()"><ion-icon name="power"></ion-icon></button>
      </ion-buttons>
      <ion-buttons end>
        <button (click)="createTodo()"><ion-icon name="cloud-upload"></ion-icon></button>
      </ion-buttons>
    </ion-navbar>
</ion-header>

<ion-content class="home">

    <ion-list no-lines>

      <ion-item-sliding *ngFor="let todo of todos">

        <ion-item>

            {{todo.title}}

        </ion-item>

        <ion-item-options>
          <button light (click)="updateTodo(todo)">
            <ion-icon name="create"></ion-icon>
          </button>
          <button primary (click)="deleteTodo(todo)">
            <ion-icon name="checkmark"></ion-icon>
          </button>
        </ion-item-options>
      </ion-item-sliding>

    </ion-list>   

</ion-content>

Modify home.scss to reflect the following:

.home {

    background-color: map-get($colors, secondary);

    scroll-content {
        display: flex !important;
        justify-content: center;
    }

    ion-list {
        width: 90%;
    }

    ion-item-sliding {
        margin-top: 20px;
        border-radius: 20px;
    }

    ion-item {
        border: none !important;
        font-weight: bold !important;
    }

}

This is the page for the main part of our application, that will display all of the todos for a user, as well as allow them to create new todos, modify them, and delete them. If you’d like more information on how this all works, I recommend taking a look at theoriginal tutorial.

3. Create the Server

In the last tutorial we just had our PouchDB syncing to the remote CouchDB database with nothing in between. This is still going to be the case, but we are going to need to add a server in now so that we can make calls to it for our signup and authentication. The SuperLogin package is going to do all of the heavy lifting for us here – it handles registration, authentication, setting up private databases for users and a whole lot more we won’t be making use of. We just need to set up a basic NodeJS server and include it.

I’m going to assume you already know what a NodeJS server is, so I’m not going to explain a lot of what is in the file we are about to create. However, if you would like some more background I’d recommend taking a look at a previous tutorial where we built a REST API with NodeJS that made use of MongoDB.

Create a new folder at app/server and add a file called server.js to it with the following:

var express = require('express');
var http = require('http');
var bodyParser = require('body-parser');
var logger = require('morgan');
var cors = require('cors');
var SuperLogin = require('superlogin');

var app = express();
app.set('port', process.env.PORT || 3000);
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cors());

app.use(function(req, res, next) {
   res.header("Access-Control-Allow-Origin", "*");
   res.header('Access-Control-Allow-Methods', 'DELETE, PUT');
   res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
   next();
});

var config = {
  dbServer: {
    protocol: 'http://',
    host: 'localhost:5984',
    user: '',
    password: '',
    userDB: 'sl-users',
    couchAuthDB: '_users'
  },
  mailer: {
    fromEmail: '[email protected]
', options: { service: 'Gmail', auth: { user: ' [email protected]

', pass: 'userpass' } } }, security: { maxFailedLogins: 3, lockoutTime: 600, tokenLife: 86400, loginOnRegistration: true, }, userDBs: { defaultDBs: { private: ['supertest'] } }, providers: { local: true } } // Initialize SuperLogin var superlogin = new SuperLogin(config); // Mount SuperLogin's routes to our app app.use('/auth', superlogin.router); app.listen(app.get('port')); console.log("App listening on " + app.get('port'));

This is mostly the same as the server we created for the MongoDB REST API, with the main difference being we are including SuperLogin . First, we set up the following configuration for SuperLogin:

var config = {
  dbServer: {
    protocol: 'http://',
    host: 'localhost:5984',
    user: '',
    password: '',
    userDB: 'sl-users',
    couchAuthDB: '_users'
  },
  mailer: {
    fromEmail: '[email protected]
', options: { service: 'Gmail', auth: { user: ' [email protected]

', pass: 'userpass' } } }, security: { maxFailedLogins: 3, lockoutTime: 600, tokenLife: 86400, loginOnRegistration: true, }, userDBs: { defaultDBs: { private: ['supertest'] } }, providers: { local: true } }

This config object is used to intialise SuperLogin, and contains various settings that we can change. The most important one here is this:

host: 'localhost:5984',

you need to make sure this is the address of the CouchDB instance that is running on your machine (or remotely). If you have not already set up CouchDB, go back to theoriginal tutorial for instructions on how to do that.

We have the configuration for the mailer set up here which is what will be used when sending confirmation emails, forgot password emails and so on (yes, SuperLogin even handles that!). You will need to configure this for whatever service you are using though, this tutorial does not actually make use of this feature.

Next we have the security settings, which are pretty self explanatory, but we have also enabled loginOnRegistration . This will cause a user to automatically be authenticated when they signup so that we can direct them straight to the main application, rather than having them login after they sign up.

The userDBs section defines the databases that will be created. We simply have a single private database called supertest , which means that each user that signs up will be given their own private database, which will be created in the following format:

although we haven’t made user of it here, you can also tell SuperLogin to create a shared database that will allow access to more than one user.

There’s a ton more configurations you can supply, including things like Facebook integration, so I’d recommend taking a look at the documentation . We will be leaving it there for now though.

The other important part here is these lines:

// Initialize SuperLogin 
var superlogin = new SuperLogin(config);

// Mount SuperLogin's routes to our app 
app.use('/auth', superlogin.router);

This uses our configuration settings and sets up all of the SuperLogin routes on /auth , meaning we don’t need to set them all up manually like we did in the MongoDB application. Now we will be able to make HTTP requests to URLs like /auth/login and /auth/register from within our application to make use if the SuperLogin functionality.

Now that our server is created, all you have to do is navigate to it in your terminal and run the following command to start the server:

node server.js

and you should see something like this:

4. Create the Todos Provider

Now let’s set up our Todos provider, which handles saving and loading our todos. We mostly covered everything that is happening in this provider in the original tutorial, but there are some minor changes.

Modify todos.ts to reflect the following:

import {Injectable} from '@angular/core';
import {Http} from '@angular/http';
import 'rxjs/add/operator/map';
import * as PouchDB from 'pouchdb';

@Injectable()
export class Todos {

  data: any;
  db: any;
  remote: any;

  constructor(private http: Http) {

  }

  init(details){

    this.db = new PouchDB('cloudo');

    this.remote = details.userDBs.supertest;

    let options = {
      live: true,
      retry: true,
      continuous: true
    };

    this.db.sync(this.remote, options);

    console.log(this.db);

  }

  logout(){

    this.data = null;

    this.db.destroy().then(() => {
      console.log("database removed");
    });
  }

  getTodos() {

    if (this.data) {
      return Promise.resolve(this.data);
    }

    return new Promise(resolve => {

      this.db.allDocs({

        include_docs: true

      }).then((result) => {

        this.data = [];

        let docs = result.rows.map((row) => {
          this.data.push(row.doc);
        });

        resolve(this.data);

        this.db.changes({live: true, since: 'now', include_docs: true}).on('change', (change) => {
          this.handleChange(change);
        });

      }).catch((error) => {

        console.log(error);

      }); 

    });

  }

  createTodo(todo){
    this.db.post(todo);
  }

  updateTodo(todo){
    this.db.put(todo).catch((err) => {
      console.log(err);
    });
  }

  deleteTodo(todo){
    this.db.remove(todo).catch((err) => {
      console.log(err);
    });
  }

  handleChange(change){

    let changedDoc = null;
    let changedIndex = null;

    this.data.forEach((doc, index) => {

      if(doc._id === change.id){
        changedDoc = doc;
        changedIndex = index;
      }

    });

    //A document was deleted
    if(change.deleted){
      this.data.splice(changedIndex, 1);
    } 
    else {

      //A document was updated
      if(changedDoc){
        this.data[changedIndex] = change.doc;
      } 

      //A document was added
      else {
        this.data.push(change.doc); 
      }

    }

  }

}

If you’ve got a keen eye then you might notice that this provider has been modified slightly when compared to the original provider from the last tutorial. Instead of immediately initialising PouchDB, we have moved it into its own init function. This function takes in the users authentication details, and then syncs the local instance of PouchDB to the appropriate remote CouchDB database (which is their own private database).

We also have a logout function that removes all of the data from memory and destroys the local database. Now that we have that created, we need to make sure we add it to the application:

Modify app.ts to reflect the following:

import {Component} from '@angular/core';
import {Platform, ionicBootstrap} from 'ionic-angular';
import {Todos} from './providers/todos/todos';
import {StatusBar} from 'ionic-native';
import {LoginPage} from './pages/login/login';

@Component({
  template: '<ion-nav [root]="rootPage"></ion-nav>'
})
export class MyApp {
  rootPage: any = LoginPage;

  constructor(platform: Platform) {
    platform.ready().then(() => {
      // Okay, so the platform is ready and our plugins are available.
      // Here you can do any higher level native things you might need.
      StatusBar.styleDefault();
    });
  }
}

ionicBootstrap(MyApp, [Todos]);

We’ve imported the provider here and added it the bootstrap function (for more information on exactly how providers work in Ionic 2, take a look atthis tutorial). As well as that, we have also imported our initial LoginPage and set it up as the root page so that it is the first page users see.

5. Implement Login and Signup

We’ve got most of the app set up, now we just need to implement the logic for the integration with SuperLogin. We will need to make some modifications to all three of the pages in the application, and I will talk through those as we go.

Modify login.ts to reflect the following:

import { Component } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { NavController } from 'ionic-angular';
import { SignupPage } from '../signup/signup';
import { HomePage } from '../home/home';
import { Todos } from '../../providers/todos/todos';

@Component({
  templateUrl: 'build/pages/login/login.html',
})
export class LoginPage {

    username: string;
    password: string;

    constructor(private nav: NavController, private http: Http, private todoService: Todos) {

    }

    login(){

        let headers = new Headers();
        headers.append('Content-Type', 'application/json');

        let credentials = {
            username: this.username,
            password: this.password
        };

        this.http.post('http://localhost:3000/auth/login', JSON.stringify(credentials), {headers: headers})
          .subscribe(res => {
            this.todoService.init(res.json());
            this.nav.setRoot(HomePage);
          }, (err) => {
            console.log(err);
          });

    }

    launchSignup(){
        this.nav.push(SignupPage);
    }

}

For our login page we need to implement the login() function that is called when the user clicks the “Login” button. This takes the credentials they entered and then POSTs to the auth/login route on our serve. SuperLogin will do its magic, and if the users credentials are correct we will get a success response back which will contain information about the user, including the URL for their own private CouchDB database. We make a call to the Todos provider to initialise it with that data, and then change the root page to the Home page, which is the main page of the application.

We have also implemented the launchSignup function here which simply pushes the Signup page if the user clicks the ‘Create Account’ button.

Modify signup.ts to reflect the following:

import { Component } from '@angular/core';
import { Http, Headers } from '@angular/http';
import { NavController } from 'ionic-angular';
import { HomePage } from '../home/home';
import { Todos } from '../../providers/todos/todos';

@Component({
  templateUrl: 'build/pages/signup/signup.html',
})
export class SignupPage {

    name: string;
    username: string;
    email: string;
    password: string;
    confirmPassword: string;

    constructor(private nav: NavController, private http: Http, private todoService: Todos) {

    }

    register(){

        let headers = new Headers();
        headers.append('Content-Type', 'application/json');

        let user = {
            name: this.name,
            username: this.username,
            email: this.email,
            password: this.password,
            confirmPassword: this.confirmPassword
        };

        this.http.post('http://localhost:3000/auth/register', JSON.stringify(user), {headers: headers})
          .subscribe(res => {
            this.todoService.init(res.json());
            this.nav.setRoot(HomePage);
          }, (err) => {
            console.log(err);
          });   

    }

}

This is almost exactly the same as the login page, except this time we post to the /auth/register URL instead. We still initialise the Todos service with the returned data, and again we change the root page to the home page. Remember how we set the loginOnRegistration configuration to true in our server? This is important because if we didn’t do that, the user wouldn’t be logged in when we send them to the Home page here.

Modify home.ts to reflect the following:

import {Component} from "@angular/core";
import {NavController, Alert} from 'ionic-angular';
import {Todos} from '../../providers/todos/todos';
import {LoginPage} from '../login/login';

@Component({
  templateUrl: 'build/pages/home/home.html'
})
export class HomePage {

  todos: any;

  constructor(private nav: NavController, private todoService: Todos) {

  }

  ionViewLoaded(){
    this.todoService.getTodos().then((data) => {
        this.todos = data;
    });
  }

  logout(){
    this.todoService.logout();
    this.todos = null;
    this.nav.setRoot(LoginPage);
  }

  createTodo(){

    let prompt = Alert.create({
      title: 'Add',
      message: 'What do you need to do?',
      inputs: [
        {
          name: 'title'
        }
      ],
      buttons: [
        {
          text: 'Cancel'
        },
        {
          text: 'Save',
          handler: data => {
            this.todoService.createTodo({title: data.title});
          }
        }
      ]
    });

    this.nav.present(prompt);

  }

  updateTodo(todo){

    let prompt = Alert.create({
      title: 'Edit',
      message: 'Change your mind?',
      inputs: [
        {
          name: 'title'
        }
      ],
      buttons: [
        {
          text: 'Cancel'
        },
        {
          text: 'Save',
          handler: data => {
            this.todoService.updateTodo({
                _id: todo._id,
                _rev: todo._rev,
                title: data.title
            });
          }
        }
      ]
    });

    this.nav.present(prompt);
  }

  deleteTodo(todo){
    this.todoService.deleteTodo(todo);
  }

}

There isn’t really much difference here when compared to the original version of this tutorial, except that we have implemented the logout function that will call the logout method in the Todos service, clear the data, and send the user back to the login page.

6. Run the Server and Test

That’s it, we’re done! All you have to do now is make sure your server is running by navigating to the server folder and running:

node server.js

and then serving your application with:

ionic serve

Go ahead and create a new account, post some todos, logout, login, create a new account and do it all again. Hopefully you should find you can use many different accounts independently of each other.

BONUS CONTENT:Download the source code for this tutorial by entering your email address below:

Summary

Creating a login system is no small technical challenge, we have to consider many requirements including:

  • Login
  • Registration
  • Forgot passwords
  • Security
  • Emails
  • Social registration

among many more. SuperLogin simplifies all of this greatly (to the point where you really don’t even need to do much at all). I hope these tutorials have highlighted just how useful this package can be, how powerful (but also complex) PouchDB and CouchDB are when used together, and how to build a practical, real world scenario application reasonably simply. For the amount we are doing with this application, the code base is surprisingly small and simple.





About List