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
- After the user logs in, store the login details in a cookie

- After login, the user is able to navigate between pages.

- 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.
- 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:
- 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.
- The closeBrowser cookie will expire when the browser is closed. Next time the user visits our sproutcore app, they will have to login.
- 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.
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
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
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
Hi John,
Checkout this link from the wiki. http://wiki.sproutcore.com/w/page/12413070/Todos%2008-Localizing
Veebs