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



·

In the last tutorial we discussed some strategies for creating a multi-user app with PouchDB and CouchDB, specifically in the relation to the todo application created in this tutorial. If you are unfamiliar with PouchDB and CouchDB I would recommend reading this 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:

multiuser-multicouch

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 user’s 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:

Ionic 2, PouchDB, CouchDB App

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

Run the following command to switch to your project once it has generated:

cd cloudo-auth

We’ll 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

We’re not going to get too much into the styling with this application, but we are going to just set up some basic colours now.

Modify the $colors map in src/theme/variables.scss to reflect the following:

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

Since we will be making use of PouchDB, we will need to install that in the project.

Run the following commands to install PouchDB:

npm install pouchdb --save

and to make sure that the TypeScript compiler doesn’t complain about not knowing what PouchDB is, we will need to install the types for PouchDB:

Run the following command to install the types for PouchDB

npm install @types/pouchdb --save --save-exact

Finally, we are going to have to set up the pages and the provider we generated in the app.module.ts file.

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

import { NgModule, ErrorHandler } from '@angular/core';
import { IonicApp, IonicModule, IonicErrorHandler } from 'ionic-angular';
import { MyApp } from './app.component';
import { HomePage } from '../pages/home/home';
import { LoginPage } from '../pages/login/login';
import { SignupPage } from '../pages/signup/signup';
import { Todos } from '../providers/todos';

@NgModule({
  declarations: [
    MyApp,
    HomePage,
    LoginPage,
    SignupPage
  ],
  imports: [
    IonicModule.forRoot(MyApp)
  ],
  bootstrap: [IonicApp],
  entryComponents: [
    MyApp,
    HomePage,
    LoginPage,
    SignupPage
  ],
  providers: [{provide: ErrorHandler, useClass: IonicErrorHandler}, Todos]
})
export class AppModule {}

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 in this 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 src/pages/login/login.html to reflect the following:

<ion-content padding>

  <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 ion-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:

.ios, .md {

  page-login {

      .scroll-content {
        background-color: map-get($colors, secondary);
        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 page-login selector so that this styling only affects the login page. 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 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 color="secondary">
    <ion-title>Create Account</ion-title>
  </ion-navbar>

</ion-header>


<ion-content padding>

  <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 ion-button (click)="register()" class="continue-button">Register</button>

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

</ion-content>

Modify signup.scss to reflect the following:

.ios, .md {

  page-signup {

      .scroll-content {
        background-color: map-get($colors, secondary);
        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 no-border>
  <ion-navbar color="secondary">
    <ion-title>
      ClouDO
    </ion-title>
    <ion-buttons start>
      <button ion-button icon-only (click)="logout()"><ion-icon name="power"></ion-icon></button>
    </ion-buttons>
    <ion-buttons end>
      <button ion-button icon-only (click)="createTodo()"><ion-icon name="cloud-upload"></ion-icon></button>
    </ion-buttons>
  </ion-navbar>
</ion-header>

<ion-content>

  <ion-list no-lines>

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

      <ion-item>

        {{todo.title}}

      </ion-item>

      <ion-item-options>
        <button ion-button icon-only color="light" (click)="updateTodo(todo)">
          <ion-icon name="create"></ion-icon>
        </button>
        <button ion-button icon-only color="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:

page-home {

  .scroll-content {
    background-color: map-get($colors, secondary);
    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 the original 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.

In order to set up the server, you will need to create a new folder outside of your Ionic 2 project to contain it. Typically, people will create a project with new folders: a client folder to contain the front end (the Ionic 2 project) and a server folder to contain the server. You may use that structure if you like.

Create a server folder outside of your Ionic 2 project

Create a file called package.json inside of the server folder, and add the following:

{
  "name": "cloudo-auth",
  "version": "0.1.0",
  "description": "A sample Node.js app using Express 4",
  "engines": {
    "node": "5.9.1"
  },
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {
    "body-parser": "^1.15.2",
    "cors": "^2.8.0",
    "del": "2.2.0",
    "express": "^4.14.0",
    "http": "0.0.0",
    "method-override": "^2.3.6",
    "morgan": "^1.7.0",
    "superlogin": "^0.6.1"
  }
}

This defines all of the dependencies that our server will require, and it also defines the file that should be used to start the server (which we will create in a moment).

Once you have that file created, you will need to run another command to install all of the dependences.

Switch to the **server* folder, and then run the following command:

npm install

Now that we have everything set up, we can start implementing the server logic.

Create a file called server.js inside of the **server* folder and add the following 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 the original 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:

Screenshot 2016-07-21 20.27.13

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:

Screenshot 2016-07-21 20.29.49

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 src/providers/todos.ts to reflect the following:

import { Injectable } from '@angular/core';
import PouchDB from 'pouchdb';

@Injectable()
export class Todos {

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

  constructor() {

  }

  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.

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';

@Component({
  selector: 'page-login',
  templateUrl: 'login.html'
})
export class LoginPage {

  username: string;
  password: string;

  constructor(public nav: NavController, public http: Http, public 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';

@Component({
  selector: 'page-signup',
  templateUrl: 'signup.html'
})
export class SignupPage {

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

  constructor(public nav: NavController, public http: Http, public 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, AlertController } from 'ionic-angular';
import { Todos } from '../../providers/todos';
import { LoginPage } from '../login/login';

@Component({
  selector: 'page-home',
  templateUrl: 'home.html'
})
export class HomePage {

  todos: any;

  constructor(public nav: NavController, public todoService: Todos, public alertCtrl: AlertController) {

  }

  ionViewDidLoad(){

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

  }

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

  createTodo(){

    let prompt = this.alertCtrl.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});
          }
        }
      ]
    });

    prompt.present();

  }

  updateTodo(todo){

    let prompt = this.alertCtrl.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
            });
          }
        }
      ]
    });

    prompt.present();
  }

  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.

Finally, we are going to need to modify the root component so that the Login page is the first page to display, rather than the home page.

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

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

@Component({
  templateUrl: 'app.html'
})
export class MyApp {
  rootPage = 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();
      Splashscreen.hide();
    });
  }
}

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.

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.

What to watch next...

  • Hugh Hou

    I try to use the same logic but avoid having my own NodeJS database. I use Auth0 to SignUp/Login, and it will return token and unique user_id. What I am not sure about is the “this.remote = details.userDBs.supertest;” part. What is the URL of details.userDBs.supertest will looks like so I can mock the same unique URL in my app. Also. in Firebase, a different “databases” just mean a different node – like firebase.io/mymotherdatabase/userdatabase-[userId]/… Is CouchDB (or CloudDB which I am using) using the same logic of creating new database? Or there is hidden magic I am not aware of with superLogin? My question is how I use Angular2 to create new database for login users in CloudDB (what superLogin does for me…)

    • I don’t think you can get away with not having your own NodeJS server here (at least with the way this tutorial is set up). What you’re referring to with Firebase is just creating a new child on the same database, i.e mymotherdatabase/user1, mymotherdatabase/user2 – those are just children of the same database. What SuperLogin is doing is creating an entirely new database for each user (which is what is contained in details.userDBs.supertest).

      You can just have all of the users data in the one database in seperate nodes as you’ve suggested, the difficult with CouchDB though is in restricting who has access to what data in that database. This may be achievable in Firebase though, I’m not super familiar with how their ACL rules work, but if you go with the structure you mention and can create some security rules that only allow access to the users own data it should be fine.

      • Hugh Hou

        I guess I am still think in Firebase. And yes, Firebase has security rule to node access and can be achieve easily on client side with Angular. Well, if the concern – the different database structure for each user – is only security, I guess I will let go security. If I figure out a way to make ONLY login user can edit the database (with Auth0), the chance of signed up user “intentionally” hack other signed up users would be really low…

  • Konrad

    thanks for this great tutorial, would you have a this up on git somewhere so I could clone and test it out?

    • Hey Konrad, you can pop your email in the box at the end of the post and the source code will be sent to you.

  • Bob

    I could be wrong, but this seems like it is very light on a security front. What would you adjust for this to be in a real world app in production? I am assuming at minimum the node server would be hosted as HTTPS, but it doesn’t seem like you are passing the token from superlogin into couchdb. Does this mean that the security is just as far as getting the database downloaded locally in pouchdb. The assumption from there is that if the pouchdb is populated, then the user has rights to get to that couchdb version of that data? I am just curious what steps you would take to make this an actual real world scenario.

    Thanks!

    • I’m going to look more into security – SuperLogin does enable you to prevent access to other users (say if someone were to manually edit the remote database link on the client side), but I’m not 100% sure on the steps to make sure this is set up properly. Once I know I’ll either update this post or perhaps create a new one on security if it is warranted.

      • Bob

        Awesome, many thanks for your tutorials you post. I will also be looking into taking this further. I will be sure to provide any information I determine. Thanks again.

      • I looked into this, and if you take a closer look at the value for the URL that is returned that gives access to the users database it looks like this: “supertest”: “http://aViSVnaDRFKFfdepdXtiEg:[email protected]:5984/supertest$joesmith”

        Both the authentication token and password are returned here, which is used to access the remote database – without that another user couldn’t sync to someone elses database.

      • Kévin

        Hiya, you can definitely use superlogin-client too, to really get the best out of this tutorial, this superlogin-client is able to keep a session open even after page reload. It’s really neat. Check this out.
        https://github.com/micky2be/superlogin-client

        And yes you can give access to many shared databases too from a user login.
        It works perfectly with cloudant even if they don’t manage the couchdb predefined users_db, the only thing is that is generating a api key every time you logout/login, I’m trying to understand this concept now.

        As a bonus, I’ve been able to make superlogin working with postmarkapp.com, email confirmation and password reset works with superlogin customisable templates like a charm. Use this plugin on top of superlogin :
        https://github.com/killmenot/nodemailer-postmark-transport

        and in your superlogin config file :

        options: {
        auth: {
        api_user: ‘[YOUR API ACCOUNT]’,
        //comment the api_key param, for postmarkapp is apiKey
        //api_key: ‘3aaa8431-acea-4dd3-a03b-f7e34b05efff’,
        apiKey: ‘[YOUR GENERATED API KEY]’
        }
        }

        Thank you so much Josh for all your tutos, before you I was lost with Ionic2…. and now client is crazy happy !
        :))))

      • Wonderful tutorial Josh! Do you know if it is possible to use SuperLogin to authenticate two different databases? One that contains read only data that can be shared between all users of the App, and one that is specific to each user (as is the case in your tutorial). I am wanting to create an App for a brewery that has a database of beers that can be seen by all users, and a database of bookmarks that is specific to each user.

  • Cyril Ade

    Hi Y’all,
    May i suggest that if you run into an error thus:
    typeScript error: /……./cloudo-auth/app/providers/todos/todos.ts(4,26): Error TS2307: Cannot find module ‘pouchdb’.
    that you run the following command within your ‘clouds-auth’ folder thus:

    cd cloudo-auth
    typings install dt~require –global –save

    and change todos.ts file where theres is “import * as PouchDB from ‘pouchdb’;” to “let PouchDB = require(‘pouchdb’);”

    • Kévin

      Yes and it’ll allow the use of pouchdb plugins very easily like this for example :

      PouchDB.plugin(require(‘pouchdb-find’));

  • dhananjay

    when the register button is click …..it give this error http://localhost:3000/auth/register net::ERR_CONNECTION_REFUSED…plz help

  • Nathaniel Stokoe

    Thanks for these Ionic 2 posts! They are excellent.

    After getting the initial project configured I transitioned to a couchdb instance on Cloudant. While playing around with this setup I noticed some strangeness when deleting a todo where it would think more than one todo was deleted and the data in the app looked different than the data in the user’s database. Anyone else had a similar problem? My next step is to revert back to a local couchdb and see if it’s the Cloudant service that’s the cause of the issue.

    • Kévin

      yes Nathaniel me too, as I’ve deploy this super powerful login stack few days ago in my very own end, I’ve played with cloudant and superlogin too. I can tell you that to use remove is not recommended at all, instead use :

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

      I recommend as well to use the superlogin-client library that helps a lot to interact with the superlogin server. I had problem integrating the script in the tutorial but it went perfect like this :

      var superlogin = require(‘superlogin-client’).default;

      If you need more infos let me know

      • Nathaniel Stokoe

        Thanks for the tip. I made the change but it didn’t fix the problem. After stepping through the code I found the problem was with the handleChange function. I think because the database is destroyed each time all records come back as changes, thus pass through the handleChange method. Anything that was deleted would try to splice the data array but the changedIndex variable was null, blowing away what had already been pushed there. Anyway, it’s not pretty but this change got it working for me:


        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) {
        if(changedIndex != null) {
        this.data.splice(changedIndex, 1);
        }
        } else {
        //A document was updated
        if(changedDoc){
        if(changedIndex != null) {
        this.data[changedIndex] = change.doc;
        }
        }
        //A document was added
        else {
        this.data.push(change.doc);
        }
        }

        }

      • Kévin

        Very strange, I don’t have this problem. I’ve changed those two things though:

        – in the init function I name my local PouchDB with the user id instead of cloudo it prevents to destroy the db on logout function. After all we want a persisting offline db so I we don’t need to delete it after logout. (I’m now trying to check how to integrate encryption.)

        this.db = new PouchDB(‘cloudo’);
        BECOMES :
        this.db = new PouchDB(details.user_id);

        AND

        logout(){

        this.data = null;

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

      • chrisgaona

        Thank you @kvinperre:disqus. Your changes fixed the issue for me brought up by @nathanielstokoe:disqus.

  • Amr Ayoub

    Great tutorial Josh , can you please make a tutorial about superlogin alone , and how can we integrate the other social login that it provide like ( facebook and twitter ).
    and is superlogin alternative to firebase ?

    Thanks

  • Varshil Shah

    hey josh many many thank your for this post…
    i have doubt .. i want image right side of text box . how can i do that??

  • Onno

    Great tutorial, thanks a lot!
    I got four TypeScript errors, because the Alert component has been changed in the last beta. If somebody else gets these errors, it is easy to fix: In file home.ts change “Alert” to “AlertController” in line 2, “Alter.create(” to “this.alertController.create(” in lines 31 and 58 and “this.nav.present(prompt)” to “promt.present()” in lines 52 and 83.

  • Luís Cunha

    Hi Josh, great tutorial once again, everything working as expected.
    I have a question though, how would be a good follow up to this tutorial to automatically login a user if the session/auth token/user info exists or is still valid considering?
    Thanks

  • femi oni

    I have a silly issue nobody else seem to have; after config and running server, i tried adding new user and it errored with “Name or password is incorrect.”,”reason”:”You are not a server admin.”. what am i not doing right?

    • Been a while since I looked at this, so not sure what might be causing that, I am hoping to update this soon though

    • Felix Wittmann

      This happens if you previously added an admin user to your couchdb. Then you need to subsequently login in with your admin credentials. The easiest to get this running is to delete the existing admin user in your coudb (eg via futon/fauxton)

  • Atbara

    Hi Josh, first I would like to thank you very much for of your efforts to educate us, I’ve been following your posts with pleasure, absolute thanks.

    I followed the steps in this tutorial and it works beautifully, I used it with the latest version of [email protected] v2 (as of today Nov. 14. 2016), I had to modify couple of things though. I will be happy to write here about the modifications if anyone is interested.

    The issue:

    – I don’t see the todos posted in the ‘home.html’, I’m getting an error in the shell saying the following:

    ———————————————————————————————————————

    tslint: src/providers/todos.ts, line: 61
    Unused variable: ‘docs’

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

    ———————————————————————————————————————

    I changed ‘docs’ to ‘doc’ but apparently that is not the solution

    Any ideas why this is happening?

    Thank you again Josh and much appreciated your thoughts

    • Atbara

      For those who are interested in the ionic 2 version I pushed it to git, here is the link:

      https://github.com/atbraoy/ionic2-apache-couchDB/tree/master/clouddb

      • Atbara

        I placed the server is in “/src/app/todos-server”, and you run it as
        $ node todos-server.js

    • Amanda

      I got the same error. Atbara, did you manage to fix it? Thanks for the updated code – I used it for my project.

  • Atbara

    I will also appreciate if someone can show me how to display the username at the home page!! Hit me with a function 🙂

    I’m new to this field and enjoying it very much but feel like my learning curve seems to be pretty much steady with couple of plateaus, every now and then 🙂
    Any recommended materials will definitely help, I have no clue how these .ts files/function works!

    What I did so far is using my python/django skills to figure out what that code/function is about, I do look here and there for java too. So far I can create ionic apps, but I want to build something more complicated, like multiuser with messaging and shared blogs, etc. Thanks to ‘Josh’ he taught me, through this tutorial, how to use couchdb.

    I will so much appreciate any help from anyone

    peace & love.

    • ThonyFD

      Hi Atbara, a profesional way to do that is create a user model or service but, I dit it with a little change in todos.ts, home.ts and home.html

      todo.ts:

      Declare global var
      user: any;

      In init function set details as user var value
      this.user = details;

      home.ts

      Declare global var
      user: any;

      In ionViewDidLoad function set value for this.user var;
      this.user = this.todoService.user;

      home.html

      ClouDO | {{user.user_id}}

      PD: Sorry for my panamanian english

  • mikeydiamonds

    Hi Josh, thanks for these tutorials and your book (which is a huge value, everyone here should purchase). I’ve used this tutorial and several from your book to create a basic CRUD app and now I’m testing on device. My question is how do I get the app to connect to a node server in the “cloud”? I have it setup and working properly I’m just not sure how to tell the app to look at http://node.myserver.com.

  • Fred McDavid

    Hi Josh, thanks and nice work on this tutorial. Just wanted to mention an issue I hit: when I run ‘ionic serve’, I’m hitting an error: Error: Cannot find module ‘../lib/cli’. I punted with ‘npm run ionic:serve’ and it seems to work. Cheers!

  • Wormling

    Hi Josh,
    login(){
    let headers = new Headers();
    headers.append(‘Content-Type’, ‘application/json’);

    this.http.post(‘http://localhost:3000/auth/logout’, {}, {headers: headers})
    .subscribe(res => {
    console.log(res);
    }, (err) => {
    console.log(err);
    });
    }

    Do you have a working example of what to pass to make logout succeed? Right now that gives me a 401. It looks like the token isn’t being passed when using Ionic 2.

    • Wormling

      UPDATE: The following works but I was expecting it to happen with an HTTP interceptor.


      headers.append('Authorization', 'Bearer '+this.token+':'+this.key);