Skip to content
mauritslamers edited this page Sep 13, 2010 · 13 revisions

This is a small tutorial on how to build a login client based on OrionDB. Before we look at the code, let’s look at how it actually works.

Normally, a login view is needed when the contents of a certain web application is only intended for authenticated users. However, a SC application without content still does tell a lot about the app itself, so it can be very useful to force a user out of a specific client and to the login view when that particular user is not authenticated.

Because the functionality forcing an unauthenticated user out needs to be omnipresent, it needs to be in a framework. This framework can also contain other shared parts of course, but in this case it also needs to keep an eye on the state of the system: is this user authenticated and is he allowed to view this client.

So there are three main parts in this setup:

  • a Login client, providing the user with fields to enter username and password information and a authentication method if necessary.
  • a Framework, providing the watchful eye
  • a different client, that only authenticated users are allowed to use, which we will call the myApp client

How it works:

  • The user will request a client, either the Login or the myApp client. Both clients will request the system state using the framework.
  • when a user is authenticated (which works by session cookies) the user is allowed access to both clients
  • if a user is not authenticated, it is allowed access to the Login client, but not to the myApp client. If the unauthenticated user tries to access the myApp client, the framework should replace the current page with the Login client
  • When the user has given in his username, password and authentication method and pushes the Login button, the Login clients sends an authentication request to OrionDB. The server will answer with a System State, showing the user has successfully logged in, or not.
  • if the user has successfully logged in, he is forwarded to a preferred client, which can be stored in the database. (hardcoded for now)

The login client

The login client consists of a window with a few fields for username and password, and, if needed, a selection of the different authentication possibilities.

(For some unknown reason, the wiki inserts href tags in the following code. Please ignore those…)

Body.rhtml


<% content_for('body') do %>
      <%= label_view :login_username_label, :tag => 'h4', :value => loc('_label_username') %>
    	<%= text_field_view :login_username_fv,
    		:bind => {
    			:value => 'Login.loginFormController.userName'	
    		} %>

    	<%= label_view :login_usertype_label, :tag => 'h4', :value => loc('_label_login_method') %>
    	<% scroll_view :login_entry_page_user_type_sv do %>
    		<%= list_view :login_entry_page_user_type_cv, 
    			:example_view => 'SC.ListItemView',
    			:is_editable => false,
    			:content_value_key => 'name',
    			:bind => {
    				:content => 'Login.authenticationServerCollectionController.arrangedObjects',
    				:selection => ' Login.authenticationServerCollectionController.selection '	
    			}
    		%>
    	<% end %>

      <%= label_view :login_password_label, :tag => 'h4', :value => loc('_label_password') %>
    	<%= password_field_view :login_passwd_fv,
    		:bind => {
    			:value => 'Login.loginFormController.passwd'	
    		} 
    	%>

      <%= button_view :login_cancel_button, :title => loc('_caption_cancel'), 
          :cancel => true %>
      <%= button_view :login_login_button, :title => loc('_caption_login'), 
          :default => true, 
          :action => 'Login.loginFormController.commitLogin',
          :bind => {
             :enabled => 'Login.authenticationServerCollectionController.hasSelection' 
          } %>
<% end %>

Body.css


#login_username_label {
  position: absolute;
  left: 100px;
  top: 80px; 
  margin-top: 0px; 
}

#login_username_fv {
  position: absolute;
  left: 200px;
  top: 80px;   
}

#login_usertype_label {
  position: absolute;
  left: 400px;
  top: 50px; 
  margin-top: 0px;  
}

#login_entry_page_user_type_sv {
  position: absolute;
  left: 400px;
  top: 75px;
  height: 100px;
  width: 200px;    
}

#login_password_label {
  position: absolute;
  left: 100px;
  top: 130px; 
  margin-top: 0px;  
}

#login_passwd_fv {
  position: absolute;
  left: 200px;
  top: 130px;    
}

#login_cancel_button {
  position: absolute;
  left: 300px;
  top: 220px;    
}

#login_login_button {
  position: absolute;
  left: 400px;
  top: 220px;   
}

The login client also needs a authentication server list controller:


// ==========================================================================
// Login.AuthenticationServerCollectionController
// ==========================================================================

require('core');
require('framework'); // < Don't forget to include your framework here!
/** @class

  (Document Your View Here)

  @extends SC.Object
  @author AuthorName
  @version 0.1
  @static
*/
Login.authenticationServerCollectionController = SC.CollectionController.create(
/** @scope Login.authenticationServerCollectioncontrollerController */ {

  // TODO: Add your own code here.

}) ;

and an object controller handling the login form:


// ==========================================================================
// Login.LoginFormController
// ==========================================================================

require(‘core’);
require(‘framework’);

/** @class

(Document Your View Here) @extends SC.Object @author AuthorName @version 0.1 @static

/
Login.loginFormController = SC.ObjectController.create(
/
* @scope Login.loginFormController */ {

// TODO: Add your own code here. userName: ’’, passwd: ’’, errorMessage: ’’, userTypeSelectionBinding : ‘Login.authenticationServerCollectionController.selection’, commitLogin: function(){ var un = this.get(‘userName’); var pw = this.get(‘passwd’); var selectedUserType = this.get(‘userTypeSelection’); if(selectedUserType instanceof Array){ var ut = selectedUserType.first().get(‘guid’); // now create the record to post to the server to login Framework.systemStateController.set(‘userName’, un); Framework.systemStateController.set(‘passwd’,pw); Framework.systemStateController.set(‘authServerId’,ut); Framework.systemStateController.commitChanges(); Framework.server.commitRecords(Framework.systemStateController.get(‘content’)); // make sure the system will forward to the preferredClient Framework.checkSystemState(); } else { // alert? } }

}) ;

You’ll notice immediately that all data of the login form is set on an object controller in the framework.
This is because it is much easier to keep track of an existing record with a fixed guid for a system state, as a SC.RestServer request must have a response of the same recordType. So, the authentication data is put onto a record of type Framework, so when the user authentication request is sent off, the response by the server can contain information whether the authentication attempt has succeeded or not.

The framework

The framework can contain many things, but for our login to work, it absolutely needs to contain a model and controller for the system state. First the model:


// ==========================================================================
// SystemState
// ==========================================================================

require('core');
require('framework');

/** @class

  (Document your class here)

  @extends SC.Record
  @author    AuthorName  
  @version 0.1
*/  
Framework.SystemState = SC.Record.extend(
/** @scope Lesson.prototype */ {

  // TODO: Add your own code here.

  //dataSource: SC.Server.create({ prefix: [""], urlFormat: "?%@&%@" }),
  //dataStore: SC.Server.create({ prefix: [""], urlFormat: "?%@&%@" }),
  dataStore: Framework.server, // maybe Contacts.server?
  dataSource: Framework.server,
  /*
  define the URL for this Record type. 
     - updates will be POSTed to '/ajaxcom/contact/update' 
     - new records will be POSTed to '/ajaxcom/contact/create' 
     - and existing records will be fetched (GET) from
       '/ajacom/contact/show/23' (if the record has guid=23 and
        only one record is fetched)
  */
  resourceURL: 'SystemState', 

  // this list of properties will be used when talking to the server 
  // backend. If you don't define this only 'guid' will be used. 
  properties: ['id','userName','passwd','authServerId','loginStatus','preferredClient']
});

and the controller:


// ==========================================================================
// Framework.systemStateController
// ==========================================================================

require('core');
require('models/systemstate');

/** @class

  (Document Your View Here)

  @extends SC.Object
  @author AuthorName
  @version 0.1
  @static
*/
Framework.systemStateController = SC.ObjectController.create(
/** @scope Framework.systemStateController */ {

  // TODO: Add your own code here.

  contentObserver: function(){
    // get the content
    var curContent = this.get('content');
    if((curContent) && (curContent instanceof Array) && (curContent.length == 1)){
      var curState = curContent.first();
      if(curState){
         var curLoginStatus = curState.get('loginStatus');
         if(curLoginStatus){
            if(window.Login){
               // if we are at the login screen and the login is still valid,
               // replace the login with the preferredClient if it exists
               var prefClient = curState.get('preferredClient');
               if(prefClient){
                 var curURL = this.getBareURL();
                 var lastSlashPos = curURL.lastIndexOf('/');
                 var newPos = [location.pathname.substr(0,lastSlashPos+1), prefClient].join('');
                 var newURL = [location.protocol, '//',location.host,newPos].join('');
                 location.replace(newURL); 
                 console.log(newURL);
                 Framework.systemStateTimer.invalidate();
               }
            }
         }          
         else {
           //if the current client is not login, replace the current URL with
           // the login client
           if(!window.Login){
             //console.log('Mmm, not logged in and at a different client? Not good');
             // get the current url
             var curURL = this.getBareURL();
             var lastSlashPos = curURL.lastIndexOf('/');
             // remove last part of string from lastSlashPos
             var newPos = [curURL.substr(0,lastSlashPos+1), 'login'].join('');
             var newURL = [location.protocol,'//',location.host,newPos].join('');
             console.log(newURL);
             Framework.systemStateTimer.invalidate();
             location.replace(newURL);
             //console.log(newURL);
           }
           else {
             //console.log('We are already trying to login :) ');
             Framework.systemStateTimer.invalidate();
           }
         }
      }      
    }     
  }.observes('content'),

  getBareURL: function(){
    // function to get correct url, whether in dev mode or in prod mode
    var curURL = location.pathname;
    // now find out whether we are in production mode (/path/to/client/en/index.html)
    // or in dev mode (ending in /)
    if(curURL.substr(curURL.length - 14) == "/en/index.html"){
      // prod mode
      return curURL.substr(0,curURL.length -14); 
    }
    else {
      // dev mode 
      return curURL;
    }
  },

  process: function(){
    // set the content
    var result = SC.Store.findRecords({ 'guid' : 1}, Framework.SystemState);
    //console.log(result);
    if((result) && (result instanceof Array)){
      //console.log('result of correct type');
      if(result.length == 1){
         //console.log('setting content');
         this.set('content',result);  
      }
    }
  }

}) ;

Framework.retrieveSystemState = function(){
  var sysStates = Framework.SystemState.collection();
  Framework.server.listFor(sysStates);
  Framework.systemStateTimer = SC.Timer.schedule({
    target: 'Framework.systemStateController',
    action: 'process',
    interval: 2000,
    repeats: YES
  });
};

Framework.checkSystemState =  function(){
  Framework.systemStateTimer = SC.Timer.schedule({
    target: 'Framework.systemStateController',
    action: 'process',
    interval: 2000,
    repeats: YES
  });
};

What happens here is that a few functions are provided:

  • Framework.retrieveSystemState :: A function to get the system state from the server. Because it is unclear when the record will be returned exactly, this function starts a SC.Timer which will call the Framework.systemStateController.process function every two seconds. This process will continue until a record is found and processed successfully.
  • Framework.checkSystemState :: A function to check for the existing record in the Store whether anything should happen.
  • Framework.systemStateController.process :: A function that retrieves the current record with system state information from SC.Store and set it to the content of Framework.systemStateController.
  • Framework.systemStateController.contentObserver :: A function called the moment the content of this object controller is changed. It will try to find out what kind of response the server has sent back and whether anything should be done. It expects the preferredView to be empty if a redirect is not in order. When a redirect is in order, it will find out whether it is run in production mode or development mode (which causes a different URL scheme use), and adjust the view to the preferredView or, when the user is not logged in, a redirect to the Login client.

Warning: The code above does not yet work for multiple language-clients. If you know how to do that, please feel free to change the code.

myApp

The only thing myApp needs to have is two-fold: a call to Framework.retrieveSystemState in its main.js and every time for communication with the db backend.

Conclusion

You’ll have noticed by now that this login functionality is still a bit crude and only works properly when you have one client. It can not yet determine if the client is correct or whether the user actually is allowed to work with that client. However, the basic structure of redirecting users to the Login client is present, so with a few tweaks it should be quite easy to add this check.

Comments

Clone this wiki locally