Integrating Parse with Lavaca — Building Signup and Login Views

The goal of this tutorial is to build a Lavaca app that contains an authentication flow for user signup, login, logout, and password reset with Parse. The end result should provide a seed for future projects or tutorials exploring Parse and Lavaca.

Project Setup

If you haven't already, pull down a copy of lavaca-starter and follow the Getting Started instructions to setup your dev environment. If you would rather just follow along the completed source of this tutorial, you can grab it from my github.

You will also need to setup a new app at http://Parse.com. When creating your app, make note of the Application ID and Javascript Key, we will use these in our app to initialize the Parse JavaScript SDK.

Install Dependencies

All client-side dependencies in Lavaca should be managed by Bower where possible. For this tutorial we'll need to add the Parse JavaScript SDK and bootstrap as dependencies in ./bower.json.

"dependencies": {
  "lavaca": "https://github.com/mutualmobile/lavaca.git#dev",
  "parse": "https://parse.com/downloads/javascript/parse-1.2.12.js",
  "bootstrap": "~3.0.0"
}

Once the dependencies are added, open the terminal and cd into your project's root directory. Run $ bower install to update ./src/www/components with the new dependecies.

In order to use these dependencies with RequireJS we need to update ./src/www/js/app/boot.js to shim them in.

paths: {
  ...
  'parse': '../components/parse/index',
  'bootstrap': '../components/bootstrap/dist/js/bootstrap'
},
shim: {
  ...
  parse: {
    exports: 'Parse'
  },
  bootstrap: {
    deps: ['$']
  }
}

Lastly, we need to import our bootstrap less into ./src/www/css/app/app.less so app doesn't look so shitty.

@import '../../components/bootstrap/less/bootstrap.less';
@import '../../components/bootstrap/less/theme.less';

Initialize Parse and Scaffold Routes

Our app kicks off from ./src/www/js/app/app.js. Edit the file to look like this.

define(function(require) {
  var History = require('lavaca/net/History');
  var HomeController = require('./net/HomeController');
  var Connectivity = require('lavaca/net/Connectivity');
  var Application = require('lavaca/mvc/Application');
  var Translation = require('lavaca/util/Translation');
  var headerView = require('app/ui/views/controls/HeaderView');
  var AuthenticationController = require('app/net/AuthenticationController');
  var Parse = require('parse');
  require('lavaca/ui/DustTemplate');
  require('hammer');
  require('bootstrap');

  Parse.initialize({app_id}, {js_key});


  // Uncomment this section to use hash-based browser history instead of HTML5 history.
  // You should use hash-based history if there's no server-side component supporting your app's routes.
  History.overrideStandardsMode();

  /**
   * Global application-specific object
   * @class app
   * @extends Lavaca.mvc.Application
   */
  var app = new Application(function() {
    // Add routes
    this.router.add({
      '/': [HomeController, 'index'],
      '/logout': [AuthenticationController, 'logout', {bypassAuthentication: true}],
      '/login': [AuthenticationController, 'login', {bypassAuthentication: true}],
      '/signup': [AuthenticationController, 'signup', {bypassAuthentication: true}],
      '/forgot-password': [AuthenticationController, 'forgotPassword', {bypassAuthentication: true}]
    });
    // Initialize messages
    Translation.init('en_US');
    //render header
    headerView.render();
  });

  // Setup offline AJAX handler
  Connectivity.registerOfflineAjaxHandler(function() {
    var hasLoaded = Translation.hasLoaded;
    alert(hasLoaded ? Translation.get('error_offline') : 'No internet connection available. Please check your settings and connection and try again.');
  });

  return app;

});

You'll notice a few changes from the original file. First we require in a few dependencies needed by our application.

var AuthenticationController = require('app/net/AuthenticationController');
var Parse = require('parse');
...
require('bootstrap')

Parse.initialize({app_id}, {js_key});

The AuthenticationController will contain the actions for handeling the auth flow, and bootstrap needs to be required once by our app so our responsive header is functional.

In the last line we initialize Parse JavaScript SDK, which we will use as a model and service layer for our application. Be sure to replace the {app_id} and {js_key} with your Parse app keys.

We also added some new routes to our application's router to stub out the complete authentication flow.

this.router.add({
  '/': [HomeController, 'index'],
  '/logout': [AuthenticationController, 'logout', {bypassAuthentication: true}],
  '/login': [AuthenticationController, 'login', {bypassAuthentication: true}],
  '/signup': [AuthenticationController, 'signup', {bypassAuthentication: true}],
  '/forgot-password': [AuthenticationController, 'forgotPassword', {bypassAuthentication: true}]
});

Each route is mapped to a controller type with a specific action. Optionally you can pass data to the controller action. Note the {bypassAuthentication: true} flag, we will use this to unprotect these routes in ./src/www/js/app/net/BaseController.

Authentication Controller and Logic

Before we stub out the AuthenticationController, lets add some logic to our BaseController to handle cases where the user is not logged in. Give this code a quick skim and then replace ./src/www/js/app/net/BaseController.

define(function(require) {

  var Controller = require('lavaca/mvc/Controller');
  var merge = require('mout/object/merge');
  var stateModel = require('app/models/StateModel');
  var Parse = require('parse');

  /**
   * Base controller
   * @class app.net.BaseController
   * @extends Lavaca.mvc.Controller
   */
  var BaseController = Controller.extend(function(){
      Controller.apply(this, arguments);
    }, {
    updateState: function(historyState, title, url, stateProps){
      var defaultStateProps = {pageTitle: title};
      this.history(historyState, title, url)();

      stateProps = merge(stateProps || {}, defaultStateProps);
      stateModel.apply(stateProps, true);
      stateModel.trigger('change');
    },
    exec: function(action, params) {
      var redirect = _shouldRedirect.call(this, action, params);

      if (redirect) {
        return this.redirect(redirect);
      } else {
        return Controller.prototype.exec.apply(this, arguments);
      }
    },
    isAuthenticated: function() {
      return !!Parse.User.current();
    }
  });

  function _shouldRedirect(action, params) {
    var redirect;
    if (!params.bypassAuthentication && !this.isAuthenticated()) {
      redirect = '/login';
    }
    return redirect;
  }

  return BaseController;

});

You'll notice we overwrote exec. This method calls a controller action with its parameters and history state. It is also the logical place to test if a user has sufficient permissions to visit the specified controller action and redirect them to the login route.

We can check if there is currently an authenticated user by casting a boolean from the return value of Parse.User.current().

isAuthenticated: function() {
  return !!Parse.User.current();
}

Since we don't want to redirect all routes to the login screen, we added a flag to bypass authentication.

if (!params.bypassAuthentication && !this.isAuthenticated()) {
  redirect = '/login';
}

Next we need to create a new controller to realize the routes we scaffolded in app.js. Create a new file ./src/www/js/app/net/AuthenticationController with the code below.

define(function(require) {

  var BaseController = require('app/net/BaseController'),
      stateModel = require('app/models/StateModel'),
      LoginView = require('app/ui/views/LoginView'),
      SignupView = require('app/ui/views/SignupView'),
      ForgotPasswordView = require('app/ui/views/ForgotPasswordView'),
      Parse = require('parse');

  /**
   * Authentication controller
   * @class app.net.AuthenticationController
   * @extends app.net.BaseController
   */
  var AuthenticationController = BaseController.extend({

    logout: function(params, history) {
      Parse.User.logOut();
      stateModel.set('user', null);
      return this.redirect('/login');
    },

    login: function(params, history) {
      Parse.User.logOut();
      stateModel.set('user', null);
      return this
        .view(null, LoginView, {})
        .then(this.updateState(history, 'Login', params.url));
    },

    signup: function(params, history) {
      return this
        .view(null, SignupView, {})
        .then(this.updateState(history, 'Signup', params.url));
    },

    forgotPassword: function(params, history) {
      return this
        .view(null, ForgotPasswordView, {})
        .then(this.updateState(history, 'Forgot Password', params.url));
    }
  });

  return AuthenticationController;

});

In Lavaca controllers have three main responsibilities:

  • Control the user's flow through the application
  • Control when data is loaded from, saved to, or modified on the server
  • Supply the data (model) to the visual presentation (view)

The first thing we do is require the views used by the controller. We'll be adding these views to the project later.

...
LoginView = require('app/ui/views/LoginView'),
SignupView = require('app/ui/views/SignupView'),
ForgotPasswordView = require('app/ui/views/ForgotPasswordView'),
...

The first action is to handle logout. Parse makes this easy for us with a simple method call. After the synchronous call to logout, we unset the user from the state model and redirect the user to the login screen. The return statement is important here. Every controller action should return a promise allowing the router to properly dispose of the controller.

logout: function(params, history) {
  Parse.User.logOut();
  stateModel.set('user', null);
  return this.redirect('/login');
},

The login action ensures the user is logged out then presents the LoginView to ViewManager to load. Lastly, the action updates the history state with the state, title, and url. This common pattern shared by all of the other controller actions.

login: function(params, history) {
  Parse.User.logOut();
  return this
    .view(null, LoginView, {})
    .then(this.updateState(history, 'Login', params.url));
},

Login View

Every view requires a template. Our login view template needs a form to collect the username and password. We also need links to the forgot password flow and account creation. Create a file called ./src/www/js/templates/login.html with this markup.

<form class="form-signin">
  <h2 class="form-heading">Login</h2>
  <input type="email" name="email" placeholder="Email" class="form-control">
  <input type="password" name="password" placeholder="Password" class="form-control">
  <button class="btn btn-lg btn-primary btn-block form-control">Login</button>
  <p class="login-note">
    <a href="/signup">Don't have an account?</a>
  </p>
  <p class="login-note">
    <a href="/forgot-password">Forgot password?</a>
  </p>
</form>

Let's also add some css to make this template a little less ugly. Create a new file ./src/www/css/app/LoginView.less with these styles.

.login.view {
  form {
    max-width: 330px;
    padding: 15px;
    margin: 0 auto;
    .form-heading, {
      margin-bottom: 10px;
    }
    .checkbox {
      font-weight: normal;
    }
    .form-control {
      position: relative;
      font-size: 16px;
      height: auto;
      padding: 10px;
      -webkit-box-sizing: border-box;
         -moz-box-sizing: border-box;
              box-sizing: border-box;
    }
    .form-control:focus {
      z-index: 2;
    }
    input[type="text"], input[type="email"] {
      margin-bottom: -1px;
      border-bottom-left-radius: 0;
      border-bottom-right-radius: 0;
    }
    input[type="password"] {
      margin-bottom: 10px;
      border-top-left-radius: 0;
      border-top-right-radius: 0;
    }
    button {
      margin: 10px 0;
    }
  }
  .login-note {
    text-align: right;
  }
}

Make sure to import the new less file into ./src/www/css/app/app.less so it gets included.

@import 'LoginView.less';

Now that we have a template and some styles, lets build a view to render our template and intercept the form's submit. Create a file at ./src/www/js/app/ui/views/LoginView.js.

define(function(require) {

  var BaseView = require('./BaseView');
  var router = require('lavaca/mvc/Router');
  var stateModel = require('app/models/StateModel');
  var Parse = require('parse');
  require('rdust!templates/login');

  /**
   * Example view type
   * @class app.ui.views.LoginView
   * @extends app.ui.views.BaseView
   */
  var LoginView = BaseView.extend(function() {
    BaseView.apply(this, arguments);
    this.mapEvent({
      form: {
        submit: this.onFormSubmit.bind(this)
      }
    });
  }, {
    /**
     * The name of the template used by the view
     * @property {String} template
     * @default 'login'
     */
    template: 'templates/login',
    /**
     * A class name added to the view container
     * @property {String} className
     * @default 'login'
     */
    className: 'login',
    onFormSubmit: function(e) {
      e.preventDefault();
      var form = this.el.find('form'),
          email = form.find('[name="email"]').val(),
          password = form.find('[name="password"]').val();
      Parse.User.logIn(email, password).then(function() {
        stateModel.set('user', Parse.User.current());
        router.exec('/');
      }, function(err) {
        console.log(err);
      });
    }

  });

  return LoginView;

});

In the constructor of the view we add an event listener to intercept the form submit event.

this.mapEvent({
  form: {
    submit: this.onFormSubmit.bind(this)
  }
});

When the form is submitted, we grab the email and password values and login the user. If the login is successful, we set the current user on the state model broadcasting to the rest of the application that a user has logged in. Later in this tutorial we will use the state model to conditionally show a logout link when a user is logged in. Finally, we execute the index route to take the user home.

onFormSubmit: function(e) {
  e.preventDefault();
  var form = this.el.find('form'),
      email = form.find('[name="email"]').val(),
      password = form.find('[name="password"]').val();
  Parse.User.logIn(email, password).then(function() {
    stateModel.set('user', Parse.User.current());
    router.exec('/');
  }, function(err) {
    console.log(err);
  });
}

If we were to try to load our application as it stands right now, RequireJS would error out trying to find our views. But, I am kind of interested what the app is looking like, so lets comment out the unused actions and views in our authentication controller.

...
// SignupView = require('app/ui/views/SignupView'),
// ForgotPasswordView = require('app/ui/views/ForgotPasswordView'),

...

// signup: function(params, history) {
//   return this
//     .view(null, SignupView, {})
//     .then(this.updateState(history, 'Signup', params.url));
// },

// forgotPassword: function(params, history) {
//   return this
//     .view(null, ForgotPasswordView, {})
//     .then(this.updateState(history, 'Forgot Password', params.url));
// }

...

Then open a terminal and start the dev server, $ grunt server. The application should be running on http://localhost:8080.

Once your satisfied the app is coming along, go back to the authentication controller and uncomment the signup action and SignupView require.

Account Creation

The SignupView will require a very similar template to the LoginView. Create a new file ./src/www/js/templates/signup.html with this markup.

<form>
  <h2 class="form-heading">Sign Up</h2>
  <input type="email" name="email" placeholder="Email" class="form-control">
  <input type="password" name="password" placeholder="Password" class="form-control">
  <button class="btn btn-lg btn-primary btn-block form-control">Signup</button>
  <p class="login-note"><a href="/login">Already have an account?</a></p>
</form>

The SignupView should also function very similarly to the LoginView, but instead of logging in the user on form submit we create a new account. Create a new file for the signup view at ./src/www/js/app/ui/views/SignupView.js.

define(function(require) {

  var BaseView = require('./BaseView');
  var router = require('lavaca/mvc/Router');
  var stateModel = require('app/models/StateModel');
  var Parse = require('parse');
  require('rdust!templates/signup');

  /**
   * Example view type
   * @class app.ui.views.SignupView
   * @extends app.ui.views.BaseView
   */
  var SignupView = BaseView.extend(function() {
    BaseView.apply(this, arguments);
    this.mapEvent({
      form: {
        submit: this.onFormSubmit.bind(this)
      }
    });
  }, {
    /**
     * The name of the template used by the view
     * @property {String} template
     * @default 'signup'
     */
    template: 'templates/signup',
    /**
     * A class name added to the view container
     * @property {String} className
     * @default 'signup'
     */
    className: 'signup login',
    onFormSubmit: function(e) {
      e.preventDefault();
      var form = this.el.find('form'),
          email = form.find('[name="email"]').val(),
          password = form.find('[name="password"]').val(),
          userModel = new Parse.User();
      userModel.set({
        username: email,
        password: password,
        email: email
      });
      userModel.signUp().then(function() {
        stateModel.set('user', Parse.User.current());
        router.exec('/');
      }, function(err) {
        console.log(err);
      });
    }

  });

  return SignupView;

});

Looks familiar huh. The interesting part is onFormSubmit.

onFormSubmit: function(e) {
  e.preventDefault();
  var form = this.el.find('form'),
      email = form.find('[name="email"]').val(),
      password = form.find('[name="password"]').val(),
      userModel = new Parse.User();
  userModel.set({
    username: email,
    password: password,
    email: email
  });
  userModel.signUp().then(function() {
    stateModel.set('user', Parse.User.current());
    router.exec('/');
  }, function(err) {
    console.log(err);
  });
}

Here we create a new instance of a Parse.User and set some properties on that user. We then call the signUp() method, which posts our user data to Parse for account creation. On success, we update our state model with the current user. The newly created user is logged in automatically by Parse, so when we execute the index route the user should pass authentication without a redirect.

Once you have completed this step, the app should allow you to create a new account. Make sure your dev server is running a visit http://localhost:8080/signup and create an account.

Logout and Header

Im sure you have noticed that once your logged in, you stay logged in even if you refresh the page. Parse stores the current user in LocalStorage under the key Parse/{app_id}/currentUser. To logout open the webinspector console and run.

require('lavaca/mvc/Router').exec('/logout')

We can't expect our users to logout this way, so lets update the header to add a conditional logout link and look more bootstrappy. Replace ./src/www/js/templates/header.js with:

<div class="navbar navbar-inverse navbar-fixed-top">
  <div class="container">
    <div class="navbar-header">
      <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target=".navbar-collapse">
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
        <span class="icon-bar"></span>
      </button>
      <a class="navbar-brand" href="/">Auth Flow</a>
    </div>
    <div class="navbar-collapse collapse" style="height: 1px;">
      <ul class="nav navbar-nav">
        {?user}
        <li><a href="/logout">Logout</a></li>
        {/user}
      </ul>
    </div>
  </div>
</div>

Note we add a logout link when a user is logged in.

{?user}
<li><a href="/logout">Logout</a></li>
{/user}

In order for our HeaderView to be redrawn properly when a user logs in or out. We need to update the scope of the redraw method in ./src/www/js/app/ui/views/controls/HeaderView.js to redraw the .navbar-nav when the model changes.

onModelChange: function() {
  this.redraw('.navbar-nav');
}

In order to beautify the bootstrap header replace ./src/www/css/app/HeaderView.less with some new styles.

#nav-header {
  .navbar {
    min-height: 45px;
  }
  .navbar-collapse {
    overflow: hidden;
  }
  .navbar-toggle {
    margin-top: 5px;
    margin-bottom: 5px;
    margin-right: 10px;
  }
  .navbar-header {
    a {
      padding: 12px;
    }
  }
  .navbar-nav {
    margin-top: 0;
    margin-bottom: 0;
    li {
      a {
        padding: 12px;
      }
    }
  }
}

Lastly in ./src/www/css/app/main.less lets fix the view roots height to work with the bootstrap header.

top: 68px;
top: 6.8rem;

with

top: 44px;

The app should now be 90% complete! Run the dev server and checkout your handywork.

Don't forget about password reset

Password reset is a piece of functionality you don't miss until you need it. Luckily Parse makes it easy on us. Open up the AuthenticationController and uncomment the forgot password view and action.

Next we need to create a new template containing a password reset form; name it ./src/www/js/templates/password-reset.html.

<form>
  <h2 class="form-heading">Forgot Password</h2>
  <input type="email" name="email" placeholder="Email" class="form-control">
  <button class="btn btn-lg btn-primary btn-block form-control">Submit</button>
  <p class="login-note"><a href="/login">I remembered, back to login &raquo;</a></p>
  <p class="login-note {noteType}">{note}</p>
</form>

The last <p> in the template will be used to message the user on a successful or failed reset attempt.

The view interaction for the ForgotPasswordView is setup just like the signup and login views. Create a new file ./src/www/js/app/ui/views/ForgotPasswordView.js.

define(function(require) {

  var BaseView = require('./BaseView');
  var Parse = require('parse');
  require('rdust!templates/forgot-password');

  /**
   * Example view type
   * @class app.ui.views.ForgotPasswordView
   * @extends app.ui.views.BaseView
   */
  var ForgotPasswordView = BaseView.extend(function() {
    BaseView.apply(this, arguments);
    this.mapEvent({
      form: {
        submit: this.onFormSubmit.bind(this)
      }
    });
  }, {
    /**
     * The name of the template used by the view
     * @property {String} template
     * @default 'login'
     */
    template: 'templates/forgot-password',
    /**
     * A class name added to the view container
     * @property {String} className
     * @default 'login'
     */
    className: 'forgot-password login',

    onFormSubmit: function(e) {
      e.preventDefault();
      var form = this.el.find('form'),
          email = form.find('[name="email"]').val();
      Parse.User.requestPasswordReset(email).then(function() {
        this.redraw('.login-note', {
          noteType: 'success',
          note: 'An email has been sent with instructions on how to reset your password.'
        });
      }.bind(this), function(error) {
        this.redraw('.login-note', {
          noteType: 'error',
          note: error.message
        });
      }.bind(this));
    }

  });

  return ForgotPasswordView;

});

Again, onFormSubmit(), is where the action lies. Parse provides the method requestPasswordReset(). When called with a valid email, an email is sent to the user to verify the request. The email contains a link to a password reset form hosted by Parse.

onFormSubmit: function(e) {
  e.preventDefault();
  var form = this.el.find('form'),
      email = form.find('[name="email"]').val();
  Parse.User.requestPasswordReset(email).then(function() {
    this.redraw('.login-note', {
      noteType: 'success',
      note: 'An email has been sent with instructions on how to reset your password.'
    });
  }.bind(this), function(error) {
    this.redraw('.login-note', {
      noteType: 'error',
      note: error.message
    });
  }.bind(this));
}

On success or failure of the requestPasswordReset() method, we redraw the note with a message indicating the result.

Conclusion

By the end of this tutorial you should have a working Lavaca application integrated with Parse that contains, account creation, login, logout, and password reset. Although this application relied on Parse as a service and model layer, the same patterns can be used to create a authentication flow with any backend system. Hopefully you learned something, if not sorry this was so long.

Get the source on my github

comments powered by Disqus