Extending Apstrata Widgets

Extending Apstrata Widgets

What makes a good third party software is how extendible it is

When deciding what third party tools to use for a project, one of the key factors that goes into the decision is how much you can customize this tool to fit your own needs. For example, using the Facebook registration plugin might not be a good idea if you want to skin your page in a particular way.

In a previous post we showed how to extend the registration widget to make your own using a token connection instead of the regular connection. But as any web application these days, you might want to add some social aspect to your app using Facebook or Twitter. One way to add social networking to your site is by using “Sign up/in with Facebook”. But if you’re already using Apstrata, this might get tricky since now you are integrating 2 third party tools into your website doing the same functionality. Managing the workflow to handle this needs to be well thought out.

First let’s look at the HTML file that will display the registration widget and the Sign up with Facebook button:


<div id="fb-root"></div>
<script src="http://connect.facebook.net/en_US/all.js"></script>

<br />
<br />
<h2>Register</h2>

<div id="flash-error"></div>

<div class="twist-register-widget">
    
    <div class="sign-with-fb" id='fb-login'><a id="connect"></a></div>
    <div class="or"><p class="or-text">or</p></div>
    <div id="register" class="registrationWidget embed spinner" 
         data-apstrata-embedType="apstrata.ui.widgets.RegistrationWidget" 
         loginurl="/?pageId=login">
    </div>
</div>


<script type="text/javascript">
  dojo.ready(function() {
    dojo.requireLocalization("apstrata.ui.widgets", "registration-widget");
    dojo.require("apstrata.sdk.Client");
    dojo.require("dojo.parser");
    dojo.require("doh.runner");
    dojo.require("tourtwist.utils.users.accountsUtils");
    dojo.require("tourtwist.ui.widgets.TwistRegistrationWidget");

    var connection;

    var registrationWidget = new tourtwist.ui.widgets.TwistRegistrationWidget({loginurl:"/?pageId=login"});

  });
</script>

<script type="text/javascript" src="src/home/spin.min.js"></script>
<script type="text/javascript" src="src/home/FBLogin.js"></script>

in this file we require the appropriate modules like the extended registration widget (shown in the previous post) and a utils file that has some helper functions for user related stuff. We also include a javascript file that will handle the Facebook related login and registration workflow call FBLogin.js.

The utils file (shown below) contains helper methods that make use of what Apstrata does in terms of user management. When a user logs in using the login widget, a cookie is created with the corresponding credentials (username) and auth token. This cookie is used to sign API calls to Apstrata on the user’s behalf, using the TokenConnection object. The login widget requires a username and password to send to the VerifyCredentials API that will check the credentials and create the cookie. But when sign in with Facebook, we will not have access to the user’s password. What we can do is retrieve the user that is sign in using the facebook_uid returned by the Facebook authentication call along (specifically the username and the hashed password). These credentials are then signed and a call is made to VerifyCredentials mimicking the one made by the login widget. This process is then responsible for creating the cookie required. The script that handles all this intermediary stuff is on the server side and not show here.


dojo.provide("tourtwist.utils.users.accountsUtils");

dojo.require('apstrata.sdk.TokenConnection');
dojo.require('apstrata.sdk.Client');

dojo.declare("tourtwist.utils.users.accountsUtils", null,
{
  constructor: function() {
    this.connectionCookie = dojo.fromJson(dojo.cookie("apstrata.token.connection"));
  },

  isLoggedIn: function() {
    return this.connectionCookie !== undefined && this.connectionCookie.credentials !== undefined;
  },

  loggedInCredentials: function() {
    if(this.isLoggedIn()) {
      return this.connectionCookie.credentials
    }

    return null;
  },

  isUseParameterToken: function() {
    if(this.isLoggedIn()) {
      return this.connectionCookie.token.authToken !== undefined;
    }

    return false;
  },

  logout: function() {
    if(this.isLoggedIn()) {
      var client = new apstrata.sdk.Client(new apstrata.sdk.Connection({
        loginType: apstrata.sdk.Connection.prototype._LOGIN_TYPE_MASTER
      }));


      var params = {"userList": this.loggedInCredentials().user};
      client.call("DeleteToken", params);
    }

    dojo.cookie("apstrata.token.connection", null, {expires: -1});
    window.location = '/';
  },

  _setupConnection: function(user) {
    var credentials;
    var serviceUrl;
    var timeout;

    // Retrieve the authKey and service URL from the ApConfig 
    if (apstrata.registry.get("apstrata.sdk", "Connection")) {
      credentials = apstrata.registry.get("apstrata.sdk", "Connection").credentials;
      serviceURL = apstrata.registry.get("apstrata.sdk", "Connection").serviceURL;
      timeout = apstrata.registry.get("apstrata.sdk", "Connection").timeout;
    }

    credentials.user = user;

    // Create an instance of TokenConnection
    var connection = new apstrata.sdk.TokenConnection({
      credentials: credentials,
      serviceURL: serviceURL,
      timeout: timeout,
      loginType: "user",
      isUseParameterToken: this.isUseParameterToken()}
    );

    // setup the client and Apstrata elements
    var client = new apstrata.sdk.Client(connection);

    return client;
  },

  getLoggedInUserInfo: function() {
    if(this.isLoggedIn()) {
      var user = this.loggedInCredentials().user;

      // to avoid making a call to apstrata each time, check if the user is already present
      if(this.user !== undefined && this.user !== null && this.user.login == user) return this.user

      var client = this._setupConnection(user);

      // get logged in user
      client.call("GetUser", {"login": user}, null, {method: "post"}).then(
        function(response) {
          this.user = response.result.user;
          console.info("logged in user: " + this.user.name);
        },
        function(response) {
          console.dir(response);
        }
      );
    } else {
      alert("User not logged in!");
    }
  },

  loginWithFB: function(accessToken, uid, spinner) {
    var client = new apstrata.sdk.Client(new apstrata.sdk.Connection({
      loginType: apstrata.sdk.Connection.prototype._LOGIN_TYPE_MASTER
    }));
    
    var request = {
      "apsdb.scriptName": "user.loginWithFB",
      "accessToken": accessToken,
      "uid": uid
    };
    
    // call the registration script on Apstrata
    client.call("RunScript", request, null, {method: "get"}).then(
      function(response) {
        dojo.cookie("apstrata.token.connection", dojo.toJson(response.result), {expires: 5, path: "/" });
        
        // hide the spinner, although the user should be redirected to a new page
        if(spinner) {
          spinner.stop();
        } else {
          var spinner = dojo.query("#fb-login .spinner")[0];
          if(spinner) {
            dojo.destroy(spinner);
          }
        }

        // redirect to the appropriate page
      }
    );
  },

  updateUserFBId: function(login, uid) {
    var client = new apstrata.sdk.Client(new apstrata.sdk.Connection({
      loginType: apstrata.sdk.Connection.prototype._LOGIN_TYPE_MASTER
    }));

    // get logged in user
    client.call("SaveUser", {"apsdb.update": true, "login": login, "facebook_uid": uid}, null, {method: "post"}).then(
      function(response) {
        console.info("FB uid updated!");
      },
      function(response) {
        console.dir(response);
      }
    );
  }
});

So if the user clicks on the Facebook button and has already connected his Facebook profile to the site, he is logged in to the app. If the user has not connected his Facebook profile but is already registered using the same email, his account is update appropriately and then logged in.

Otherwise, if the user is not yet registered, his info is retrieved from his Facebook profile and a new user is created. Since a password is not provided and it’s required by Apstrata, a random one is generated for the user (although it might never be used by the user himself since they will be using Facebook to sign in to the application in the future).

The logic to determine which scenario we are faced with is handled in the FBLogin.js below:


dojo.ready(function() {
  // initialize the library with the API key
  FB.init({ apiKey: '372870779442595' });

  // handle click event on fb button
  dojo.connect(dojo.byId('connect'), 'click', function() {
    var opts = {
      lines: 11, // The number of lines to draw
      length: 15, // The length of each line
      width: 4, // The line thickness
      radius: 16, // The radius of the inner circle
      rotate: 0, // The rotation offset
      color: '#000', // #rgb or #rrggbb
      speed: 1, // Rounds per second
      trail: 58, // Afterglow percentage
      shadow: true, // Whether to render a shadow
      hwaccel: false, // Whether to use hardware acceleration
      className: 'spinner', // The CSS class to assign to the spinner
      zIndex: 2e9, // The z-index (defaults to 2000000000)
      top: 'auto', // Top position relative to parent in px
      left: 'auto' // Left position relative to parent in px
    };
    var target = document.getElementById('fb-login');
    var spinner = new Spinner(opts).spin(target);
    attemptFBLogin(spinner);
  });
});

// get FB login status
// if the user is already connected, then log them in
// otherwise get them to authenticate the app
function attemptFBLogin(spinner) {
  FB.getLoginStatus(function(response) {
    if (response.status === 'connected') {
      // the user is logged in and has authenticated your
      // app, and response.authResponse supplies
      // the user's ID, a valid access token, a signed
      // request, and the time the access token 
      // and signed request each expire
      var uid = response.authResponse.userID;
      var accessToken = response.authResponse.accessToken;

      // if user already signed in to Facebook and authorized TourTwist
      // then user should be signed in automatically to TourTwist
      var utils = new tourtwist.utils.users.accountsUtils(false);
      utils.loginWithFB(accessToken, uid, spinner);
      

      // the user is logged in to Facebook, 
      // but has not authenticated your app
      // (response.status === 'not_authorized')
      // OR
      // the user isn't logged in to Facebook.
      // call the FB.login() method to get the user to authenticate TourTwist
    } else {
      var options = {scope: 'email,user_about_me,user_birthday,user_location,publish_stream,publish_checkins'};
      FB.login(handleFBLogin, options);
    }
  });
};

// handle a session response from any of the auth related calls
function handleFBLogin(response) {
  FB.api('/me?fields=id,name,first_name,last_name,birthday,picture,email,bio,gender,location', 
    function(response) {
      if(response === undefined) {
        alert("Could not connect to Facebook, please try again later");
        return;
      }

      if (response.error && response.error.type === "OAuthException") {
        return;
      }

      var user = response;
      
      // map user info to request params to be sent to Apstrata
      var request = facebookToApstrataUser(user);
      
      // setup the client and Apstrata elements
      var client = new apstrata.sdk.Client(new apstrata.sdk.Connection({
        loginType: apstrata.sdk.Connection.prototype._LOGIN_TYPE_MASTER
      }));
      var nls = dojo.i18n.getLocalization("apstrata.ui.widgets", "registration-widget");
      var loginUrl = "/?pageId=login";
    
      // call the registration script on Apstrata
      client.call("RunScript", request, null, {method: "get"}).then(
        function(response) {
          // if user is already registered update their facebook_uid and log them in
          if (response.result.metadata.errorCode == "DUPLICATE_USER") {
            var utils = new tourtwist.utils.users.accountsUtils();
            utils.updateUserFBId(user.email, user.id);

            attemptFBLogin();
            return;
          }
        
          // redirect to the URL sent from the response
          window.location = response.result.url;
        }, 
        function(response) {
          console.dir(response)
        }
      );
    }
  );
}

// map facebook user to apstrata fields
function facebookToApstrataUser(user) {
  var request = {
    "apsdb.scriptName": "widgets.Registration.registerUser",
    "user.login": user.email,
    "user.groups": "users",
    "user.name": user.name,
    "user.first_name": user.first_name,
    "user.last_name": user.last_name,
    "user.email": user.email,
    "user.birthday": user.birthday,
    "user.gender": user.gender,
    "user.fb_profile_picture": user.picture,
    "user.birthday.apsdb.fieldType": "date",
    "user.birthday.apsdb.fieldDateFormat": "MM/dd/yyyy",
    "user.password": getPassword(),
    "user.facebook_uid": user.id,
  }
    
  // "user.bio": user.bio,
  // "user.profile_picture.apsdb.fieldType": "file",
  
  return request
};


// generate password since we are using the facebook information to register a user
function getRandomNum(lbound, ubound) {
  return (Math.floor(Math.random() * (ubound - lbound)) + lbound);
}

function getRandomChar() {
  var numberChars = "0123456789";
  var lowerChars = "abcdefghijklmnopqrstuvwxyz";
  var upperChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
  var otherChars = "`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/? ";
  var charSet = "";
  charSet += numberChars;
  charSet += lowerChars;
  charSet += upperChars;
  charSet += otherChars;
  return charSet.charAt(getRandomNum(0, charSet.length));
}

function getPassword() {
  var rc = "";
  for (var idx = 0; idx < 10; ++idx) {
    rc += getRandomChar();
  }
  return rc;
}

So there it is… We have an example of how you can integrate social media to your site which uses Apstrata as its backend as a service. I hope that was helpful.

Tags

Like this Article? Share it!

About the Author

Author Gravatar
Youssef Chaker

web developer, problem solver, doer, for everything else follow me at @ychaker

Related Posts

Comments are closed.