Tutorial #12: A Login Action with Scala Play + Angular + Bootstrap

Here I’m demonstrating a Login flow consisting of the following:
1. showing a Login form to the user
2. submitting the form to the server via Angular and Ajax
3. validating it on the server and returning a JSON response
4. redirecting to a welcome page on successful submission

This is a flow I encountered recently using Spring MVC and, while not a single-page application was effective at displaying whether the login was successful without a page refresh.

A quick refresher of each technology:

Scala Play: Server-side technology – satisfies HTTP requests returning HTML via templates or JSON, XML etc.
AngularJS: Client-side technology which is like an extension to the HTML specification.
Twitter Bootstrap: Gives a professional look-and-feel to a website, helps sites handle different browsers, screen sizes.

Step 1: Create a Skeleton Scala Play App

I outline that here: https://scalaplayschool.wordpress.com/2014/08/10/hello-world-using-scala-play/

I download activator (that runs the Play framework), unzip it, put it on my Path and use these commands:

$ cd
$ activator new lesson12 play-scala
$ cd lesson12
$ activator eclipse
$ activator run

Step 2: Update the main template to include Angular, Bootstrap and jQuery

There is no need to download and extract source files for these technologies – we can reference public hosted versions. Let’s update our app/views/main.scala.html file as shown:

@(title: String)(content: Html)

<!DOCTYPE html>
<html>
    <head>
        <title>@title</title>
        <script src="//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
    <script src="//ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
        <link rel="stylesheet" href="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"/>
        <script type="text/javascript" src="http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js"></script>
        </head>
    <body>
      <div style="padding:20px">
        @content
        </div>
    </body>
</html>

Now we can update our app/views/index.scala.html with a simple test to demonstrate that it’s worked.

@()
@main("Login") {
  <div data-ng-app="">
    <h1>Login Form</h1>
    <form role="form">
      <div class="form-group">
          <label for="username">Username</label>
          <input class="form-control" data-ng-model="username" autofocus>
        </div>
    </form>
    <div class="alert alert-danger well">
        {{ username }}
    </div>
  </div>
}

We took out the title parameter from the index() function so let’s remove it from app/controllers/Application.scala :

package controllers

import play.api._
import play.api.mvc._

object Application extends Controller {

  def index = Action {
    Ok(views.html.index())
  }

}

We should see this:

The sans-serif font and full-width textbox show that Bootstrap has loaded ok and if we type we should see our input reflected in the grey box below the form as we type:

Step 3: Build the Login form

We’ll update our app/views/index.scala.html file again:

@()
@main("Login") {
  <div data-ng-app="" data-ng-controller="LoginController">
    <h1>Login Form</h1>
    <form role="form"
      data-ng-submit="postLoginForm('@routes.Application.login', '@routes.Application.welcome()')">
      <div class="form-group">
        <label for="username">Username</label>
        <input class="form-control" data-ng-model="username"
           data-ng-change="feedback = ''" autofocus>
      </div>
      <div class="form-group">
        <label for="password">Password</label>
        <input class="form-control" data-ng-model="password"
           type="password" data-ng-change="feedback = ''">
      </div>
      <input type="submit" value="Login">
    </form>
    <div class="alert alert-danger well" data-ng-show="feedback != ''">
      {{ feedback }} !
    </div>
  </div>
  <script type="text/javascript"
     src="@routes.Assets.at("javascripts/login.js")"></script>
}

Note the following:
data-ng-app will make Angular aware of this form and data-ng-controller will link to a specific class we are providing. We called it LoginController and it will be in login.js referenced at the end of the template.
role=”form” is to tell Bootstrap about our form so that it can style it correctly.
data-ng-submit=”…” tells Angular about our form and it will handle its submission.
form-group, form-control, alert, alert-danger – all of the classes used are ones used by Bootstrap.
data-ng-show means that AngularJS will show / hide an element based on the expression provided, in this case it is ‘does a non-empty feedback field exist?’ The feedback field belongs to the $scope variable as documented here: https://docs.angularjs.org/guide/scope

We need to update our conf/routes file, adding /login for the processing action and a /welcome to show to authenticated users:

GET   /              controllers.Application.index
POST  /login         controllers.Application.login
GET   /welcome       controllers.Application.welcome
GET   /assets/*file  controllers.Assets.at(path="/public", file)

Now we need the other view, app/views/welcome.scala.html :

@(username:String)
  
@main(s"Welcome $username") {
  <h1>Welcome @username</h1>
  Got your name from the session.
}

And our JavaScript file, public/javascripts/login.js which contains the postLoginForm function we bind to our form with data-ng-submit. This function performs the HTTP POST and redirects on successful submission or shows an error message.

function LoginController($scope, $http) {
 
  $scope.feedback = '';
   
  $scope.postLoginForm = function(loginUrl, successUrl) {
    var data = {
      username : $scope.username,
      password : $scope.password
    };
    $scope.successUrl = successUrl;
    $http.post(loginUrl, data).success(function(data, status, headers, config) {
      if (data.valid) {
        document.location.href = successUrl;
      } else {
        $scope.feedback = "Invalid username / password. Try again.";
      }
    }).error(function(data, status, headers, config) {
      $scope.feedback = 'error: ' + data + ", " + status;
    });
  };
}

Last but not least is a hefty app/controllers/Application.scala :

package controllers
 
import play.api.libs.functional.syntax.functionalCanBuildApplicative
import play.api.libs.functional.syntax.toFunctionalBuilderOps
import play.api.libs.json._
import play.api.libs.json.JsPath
import play.api.libs.json.Json.toJson
import play.api.libs.json.Reads
import play.api.libs.json.Reads._
import play.api.libs.json.Reads.StringReads
import play.api.libs.json.Reads.functorReads
import play.api.mvc.Action
import play.api.mvc.Controller
 
object Application extends Controller {
 
  // Shows the login screen and empties the session:
  def index = Action {
    Ok(views.html.index()).withNewSession
  }
 
  // Handles the username-password sent as JSON:
  def login = Action(parse.json) { request =>
 
    // Creates a reader for the JSON - turns it into a LoginRequest
    implicit val loginRequest: Reads[LoginRequest] = Json.reads[LoginRequest]
 
    /*
     * Call validate and if ok we return valid=true and put username in session
     */
    request.body.validate[LoginRequest] match {
      case s: JsSuccess[LoginRequest] if (s.get.authenticate) => {
        Ok(toJson(Map("valid" -> true))).withSession("user" -> s.get.username)
      }
      // Not valid
      case _ => Ok(toJson(Map("valid" -> false)))
    }
  }
 
  def welcome = Action { implicit request =>
    request.session.get("user").map {
      user =>
        {
          Ok(views.html.welcome(user))
        }
    }.getOrElse(Redirect(routes.Application.index()))
  }
}
 
case class LoginRequest(username: String, password: String) {
   
  // Simple username-password map in place of a database:
  val validUsers = Map("sysadmin" -> "password1", "root" -> "god")
   
  def authenticate = validUsers.exists(_ == (username, password))
}

It does a lot. I’ll explain each action:
index() – shows the login form using the index.scala.html template and empties out the user’s session if it exists (so we can demonstrate handling users not logged in)
login() – handles the form submission. Turns the JSON input into a LoginRequest object using a Reader (see the Play documentation) and checks the simple authenticate() method. It returns a JSON object with the key ‘valid’ and the value returned by authenticate(). If a valid user it stores the username in the session, to be used by the welcome action.
welcome() – if the username is present in the session it shows a welcome page, otherwise it redirects back to the login screen.

Let’s run it:

If we type in values other than those hard-coded to work ( root/god and sysadmin/password1) and submit it (pressing Enter or clicking Login) then we should see our error message:

But if we provide a valid login we get a welcome page:

What happens if we go back to the login page and refresh it? We show the page again but removing our session. If we now try to get to /welcome we are sent back to the login page.

Advertisements