Sproutcore: Login Logout Sample

Now it is time to extend the login sample further using what I’ve learnt so far.

Once again, the code can be found here.

See it in action here.

This walk through assumes that you are familiar with the Login Sample walk through.

Use Case

If the user is not logged in, present the user with the login screen

  1. After the user logs in, store the login details in a cookie
  2. After login, the user is able to navigate between pages.
  3. If the cookie expires during the session, the user is taken back to the login screen. After logging in, the user is taken back to the screen they were on.
  4. If the user clicks the Logout button, the user is taken to the login screen.

Step 1. Create Login Controller

We crate a login controller like we did in the Login sample.

LoginLogoutSample.loginPageController = SC.ObjectController.create(
  /** @scope LoginLogoutSample.loginPageController.prototype */ {

  // View-Model
  // I have used page level properties for the control to bind to  because I generally see the "model" as data
  // in a datastore.
  // As a personal preference, I like to separate data displayed on pages (View-Model) from data in the datastore
  // (Model) because I've found on complex pages, the data on pages may
  username: '',
  password: '',
  rememberMe: '',
  errorMessage: '',
  isLoggingIn: NO,
  returnRoute: '',

  /**
   Start async login process

   @returns YES if async call successfully started, NO if it failed. If error, the error message
   will be placed in the 'errorMessage' property.
   */
  beginLogin: function() {
    try {
      // Get our data from the properties using the SC 'get' methods
      // Need to do this because these properties have been bound/observed.
      var username = this.get('username');
      if (username == null || username == '') {
        throw SC.Error.desc('Username is required');
      }

      var password = this.get('password');
      if (password == null || password == '') {
        throw SC.Error.desc('Password is required');
      }

      // Start login
      this.set('isLoggingIn', YES);

      // Simulate a HTTP call to check our data.
      // If the credentials not admin/admin, then get a bad url so we get 404 error
      var url = '/login_logout_sample/en/current/source/resources/login_page.js';
      if (username != 'admin' || password != 'admin') {
        url = '/login_logout_sample/en/current/source/resources/bad_url.js';
      }

      SC.Request.getUrl(url)
        .notify(this, 'endLogin')
        .send();

      return YES;
    }
    catch (err) {
      // Set Error
      this.set('errorMessage', err.message);

      // Finish login processing
      this.set('isLoggingIn', NO);

      return NO;
    }
  },

  /**
   Callback from beginLogin() after we get a response from the server to process
   the returned login info.

   @param {SC.Response} response The HTTP response
   @param {function} callback A function taking SC.Error as an input parameter. null is passed if no error.
   */
  endLogin: function(response) {
    try {
      // Flag finish login processing to unlock screen
      this.set('isLoggingIn', NO);

      // Check status
      SC.Logger.info('HTTP status code: ' + response.status);
      if (!SC.ok(response)) {
        // Error
        throw SC.Error.desc('Invalid username or password. Try admin/admin ;-) ');
      }

      // Set cookie
      var rememberMe = this.get('rememberMe');
      var authCookie = SC.Cookie.create();
      authCookie.name = 'LoginLogoutSampleCookie';
      authCookie.value = 'sometoken passed back in the response';
      if (rememberMe == '3seconds') {
        // Cookie is saved for 3 seconds
        var d = new Date();
        d.setTime(d.getTime() + 3000);
        authCookie.expires = d;
      } else if (rememberMe == 'closeBrowser') {
        // Cookie removed when browser closed
        authCookie.expires = null;
      } else {
        // Cookie is saved for 1 year
        var d = new Date();
        d.setFullYear(d.getYear() + 1);
        authCookie.expires = d;
      }
      authCookie.write();

      // clear data
      this.set('errorMessage', '');

      // Go to next page
      var returnRoute = LoginLogoutSample.loginPageController.get('returnRoute');
      if (returnRoute != undefined && returnRoute != null && returnRoute != '') {
        SC.routes.set('location', returnRoute);
      }
    }
    catch (err) {
      this.set('errorMessage', err.message);
      SC.Logger.info('Error in endLogin: ' + err.message);
    }
  }

});

However, this time, we are going to set a cookie if we successfully login.

We have 3 types of cookie that we can set:

  1. The 3seconds cookie will expire after 3 seconds. This allows us to simulate in a testing environment a 20 minute timeout by just waiting 3 seconds.  Navigating between pages 3 seconds after login will force you back to the login screen.
  2. The closeBrowser cookie will expire when the browser is closed.  Next time the user visits our sproutcore app, they will have to login.
  3. The 1 year cookie will expire after 1 year.  This allows the user to close the browser and when they come back another day, they will still be logged in.

By setting the expiry date, we can control when the cookie is invalidated.  Then on navigating and/or performing other actions, we can check to see if the cookie exists.  If the cookie exists then the user is logged in.  If the cookie does not exist, the user is not logged in.

Step 2. Routes

By using Sproutcore routes when we move between pages, we can check to see if the user is logged in everytime there is a navigation event.

LoginLogoutSample.routes = SC.Object.create({

  /**
   Property to store the main pane of the page that is currently shown to the user
   */
  currentPagePane: null,

  /**
   Navigate to the specified route

   @param {Object} routeParams route parameters are set as properties of this
   object. The parameters are specified when registering the route using
   SC.routes.add() in main.js.
   */
  gotoRoute: function(routeParams) {
    // Default to page 1
    var pageName = routeParams.pageName;
    if (pageName == undefined || pageName == '') {
      pageName = 'onePage';
    }
    var paneName = routeParams.paneName;
    if (paneName == undefined || paneName == '') {
      paneName = 'onePane';
    }

    // If authentication cookie not found or expired, then go to login page
    if (pageName != 'loginPage' && paneName != 'loginPane') {
      var authCookie = SC.Cookie.find('LoginLogoutSampleCookie');
      if (authCookie == null) {
        LoginLogoutSample.loginPageController.set('username', '');
        LoginLogoutSample.loginPageController.set('password', '');
        LoginLogoutSample.loginPageController.set('returnRoute', pageName + '/' + paneName);
        SC.routes.set('location', 'loginPage/loginPane');
        return;
      }
    }

    // If this is the special logout out, then log out
    if (pageName == 'logoutPage' && paneName == 'logoutPane') {
      var authCookie = SC.Cookie.find('LoginLogoutSampleCookie');
      if (authCookie != null) {
        authCookie.destroy();
      }
      LoginLogoutSample.loginPageController.set('username', '');
      LoginLogoutSample.loginPageController.set('password', '');
      LoginLogoutSample.loginPageController.set('returnRoute', 'onePage/onePane');
      SC.routes.set('location', 'loginPage/loginPane');
      return;
    }

    // If there is a current pane, remove it from the screen
    if (this.currentPagePane != null) {
      this.currentPagePane.remove();
    }

    // Show the specified pane
    var pagePanePath = pageName + '.' + paneName;
    var pagePane = LoginLogoutSample.getPath(pagePanePath);
    pagePane.append();

    // Save the current pane so we can remove it when process the next route
    this.currentPagePane = pagePane;
  }

});

We use SC.Cookie.find(‘LoginLogoutSampleCookie’) to locate the cookie. If the cookie exists, the user is logged in and we continue. If the cookie is not there, we redirect to the login page.

We also set the returnRoute property of our loginPageController so that it knows where to navigate after a successful login.

We also check for a special route called “logoutPage/logoutPane”. If this is passed in, then we logout and go to the login page.

Step 3. Login Screen

The login screen is essentially the same as for the login sample with the addition of a drop down list for the user to select how long before they have to login again. In a real world app, this would be like a “remember me” checkbox.

Note the use of acceptsFirstResponder in the rememberMe SelectFieldView.  This is needed to allow the SelectFieldView (drop down list) to accept focus when the user presses the TAB key. [Edit: now fixed in Sproutcore 1.4.2]

LoginLogoutSample.loginPage = SC.Page.design({

  loginPane:  SC.MainPane.design({
    layout: { width: 360, height: 160, centerX: 0, centerY: 0 },
    classNames: ['login-pane'],
    childViews: 'boxView'.w(),

    boxView: SC.View.design({
      childViews: 'username password rememberMe loginButton loadingImage errorMessage'.w(),

      username: SC.View.design({
        layout: { left: 17, right: 14, top: 17, height: 26 },
        childViews: 'label field'.w(),

        label: SC.LabelView.design({
          layout: { left: 0, width: 107, height: 18, centerY: 0 },

          value: '_Username',
          localize: YES,
          textAlign: SC.ALIGN_RIGHT
        }),

        field: SC.TextFieldView.design({
          layout: { width: 200, height: 22, right: 3, centerY: 0 },

          isEnabledBinding: SC.Binding.from("LoginLogoutSample.loginPageController.isLoggingIn")
            .bool()
            .transform(function(value, isForward) {
            return !value;
          }),
          valueBinding: 'LoginLogoutSample.loginPageController.username'
        })
      }),

      password: SC.View.design({
        layout: { left: 17, right: 14, top: 45, height: 26 },
        childViews: 'label field'.w(),

        label: SC.LabelView.design({
          layout: { left: 0, width: 107, height: 18, centerY: 0 },

          value: '_Password',
          localize: YES,
          textAlign: SC.ALIGN_RIGHT
        }),

        field: SC.TextFieldView.design({
          layout: { width: 200, height: 22, right: 3, centerY: 0 },

          isPassword: YES,
          isEnabledBinding: SC.Binding.from("LoginLogoutSample.loginPageController.isLoggingIn")
            .bool()
            .transform(function(value, isForward) {
            return !value;
          }),
          valueBinding: 'LoginLogoutSample.loginPageController.password'
        })
      }),

      rememberMe: SC.View.design({
        layout: { left: 17, right: 14, top: 72, height: 26 },
        childViews: 'label field'.w(),

        label: SC.LabelView.design({
          layout: { left: 0, width: 107, height: 18, centerY: 0 },

          value: '_RememberMe',
          localize: YES,
          textAlign: SC.ALIGN_RIGHT
        }),

        field: SC.SelectFieldView.design({
          layout: { width: 200, height: 22, right: 3, centerY: 0 },

          objects: [
            {name:'For 3 Seconds', value:'3seconds'},
            {name:'Until the browser is closed', value:'closeBrowser'},
            {name:'For 1 year', value:'1year'}
            ],
          nameKey: 'name',
          valueKey: 'value',

          isEnabledBinding: SC.Binding.from("LoginLogoutSample.loginPageController.isLoggingIn")
            .bool()
            .transform(function(value, isForward) {
            return !value;
          }),
          valueBinding: 'LoginLogoutSample.loginPageController.rememberMe'
        })
      }),

      loginButton: SC.ButtonView.design({
        layout: { height: 24, width: 80, bottom: 17, right: 17 },
        title: '_Login',
        localize: YES,
        isDefault: YES,
        isEnabledBinding: SC.Binding.from("LoginLogoutSample.loginPageController.isLoggingIn")
          .bool()
          .transform(function(value, isForward) {
          return !value;
        }),

        target: 'LoginLogoutSample.loginPageController',
        action: 'beginLogin'
      }),

      loadingImage: SC.ImageView.design({
        layout: { width: 16, height: 16, bottom: 20, right: 110 },
        value: sc_static('images/loading'),
        useImageCache: NO,
        isVisibleBinding: 'LoginLogoutSample.loginPageController.isLoggingIn'
      }),

      errorMessage: SC.LabelView.design({
        layout: { height: 40, width: 230, right: 120, bottom: 7 },
        classNames: ['error-message'],

        valueBinding: 'LoginLogoutSample.loginPageController.errorMessage'
      })

    })  //contentView

  })  //loginPane

}); //loginPage

Step 4. Other Pages

one_page and two_page are essentially the same as the one from the route sample. The have 2 buttons.

  • The first button lets the user navigate to the other page using the route.
  • The second button lets the user log out by navigating to the special ‘logoutPage/logoutPane’ route.
LoginLogoutSample.onePage = SC.Page.design({

  onePane: SC.MainPane.design({
    childViews: 'labelView buttonView logoutButtonView'.w(),

    labelView: SC.LabelView.design({
      layout: { top: 20, left: 27, width: 400, height: 20 },
      value: 'You are on page #1.',
      classNames: ['title1']
    }),

    buttonView: SC.ButtonView.design({
      layout: { top: 70, left: 27, width: 400 },
      title: 'Click to go to page #2.',
      action: 'go'
    }),

    logoutButtonView: SC.ButtonView.design({
      layout: { top: 110, left: 27, width: 400 },
      title: 'Logout',
      action: 'goLogout'
    }),

    go: function() {
      SC.routes.set('location', 'twoPage/twoPane');
    },

    goLogout: function() {
      SC.routes.set('location', 'logoutPage/logoutPane');
    }

  })

});

Final Words

In a proper real life implementation, you will most likely need your REST server to return a token. The token can be stored in the cookie and then passed back to the server with each call.

Checking if the user has logged in or not should also occur on every controller action. e.g. before saving a record. Currently, we only check when navigating between pages.

Leave a comment

5 Comments.

  1. Great posts. Very helpful regarding how to handle login on the client side, however I am still a bit mystified about how to handle the actual authentication (the part where you “simulate” the http request). I am pretty new to web development in general, so perhaps I’m a bit over my head, but thats always how I’ve learned best. Do you know of any resources that could help me implement a secure login in a Sproutcore app?

    Thanks

    • Hi Andrew

      You can replace the simulated login with a call to a REST service on the server. To implement a rest service and integrate it with Sproutcore, see the To Do tutorial in the wiki.

      For example, you can pass the username and password to the REST service which returns a token. The token can be saved in the cookie. The next time you call a REST service, the cookie will be sent back to the server for your service to check. If the token has expired, your REST service can return an error.

      Hope this help.
      Veebs

  2. If you are going through these steps and you are fresh from the earlier two examples, you probably figured this out, but if you are jumping directly to this one, the following may help:

    – Remember to copy over the string.js file from the login example. It needs to be modified with the new _RememberMe string.

    – Also, the login.css file needs to be copied over.

    – The routes need to be registered, so copy in the corresponding code from the main.js in the route example.

    Jeff

  3. Hi,

    Thanks for a great tutorial! Very handy as I am just starting out with SproutCore :)

    I was wondering, how would you go about localisation on such an example? To enable the page links to still work correctly etc

    I see you have the strings.js file created already :)

    John

Leave a Reply


[ Ctrl + Enter ]