playframework - Adding login

Posted on by Mandla Mbuli

Login

Login is a little more complex than I thought.

First need to understand how forms work Which needs you to understand actions

then you need to make it pretty

I have also want the controller to be usable with difference packages which is another problem nje

What am I making

Got as far as showing a site with my own styling. What I am making though needs white labelling

I need each site to have it’s own login. The logic will be the same, but the data will be different. I would prefer not to have to copy and paste the code between each site.

Changing the layout

I also wanted to change from the default playframework code layout to one that has it’s own packages.

❯ eza -RTL 3 app
app
├── authentication
│  ├── controllers
│  │  ├── Authenticated.scala
│  │  └── AuthenticationController.scala
│  ├── Fields.scala
│  ├── models
│  │  ├── Global.scala
│  │  ├── LoginForm.scala
│  │  ├── User.scala
│  │  └── UserDAO.scala
│  └── templates
│     ├── fields.scala.html
│     └── loginForm.scala.html
├── base
│  ├── controllers
│  │  ├── AuthenticationController.scala
│  │  ├── HomeController.scala
│  │  └── LandingController.scala
│  └── views
│     ├── index.scala.html
│     ├── landing.scala.html
│     ├── login.scala.html
│     └── main.scala.html
└── website
   ├── AuthenticationController.scala
   ├── IndexController.scala
   └── views
      ├── base.scala.html
      ├── index.scala.html
      └── login.scala.html

This causes it’s own learning opportunities. Making the authentication generic is the first learning I went through.

Generic AuthenticationController

I really don’t like calling it AuthenticationController, it’s only because it also has the logout action. I wanted the AuthenticationController to be reusable between the different sites, but still leave the sites to define their own style. I think the following allows me to do that.

LoginView

The first thing I had trouble figuring out was the LoginView. playframework generates the views as an object and they have their own type from what I can tell. It also has an implicit which I had to do reading up on. I don’t fully understand them yet, but they sounds kinda useful.

I decided that I will make the implicit (viz. MessagesRequestHeader), explicit in my model. This is mainly because I couldn’t figure out how to add the implicit to the type alias. My alias for the view becomes:

type LoginView = (
    Form[models.User],
    Call,
    MessagesRequestHeader
) => Appendable

This is used in the LoginConfig which looks like:

case class LoginConfig(
    formRoute: Call,
    submitRoute: Call,
    landingRoute: Call,
    loginView: LoginView
)

Used with a function so that each subclass can configure it themselves

  def loginConfig = LoginConfig(
    controllers.routes.AuthenticationController.login,
    controllers.routes.LandingController.landing(),
    controllers.routes.AuthenticationController.showLoginForm,
    (form, m, msg) => views.html.login(form, m)(msg)
  )

I don’t like the function (form, m, msg) => views.html.login(form, m)(msg) and I might look into how to represent the implicit in way that I can directly pass the view without the need for this function.

Bringing it all together, I have a base login class which looks like:


package authentication

import javax.inject.Inject

import authentication.Authenticated
import models.{User, Global, UserDAO}

import play.api.mvc._
import play.api.data._
import play.api.data.Forms._
import play.twirl.api.HtmlFormat.Appendable

import authentication.LoginForm

type LoginView = (
    Form[models.User],
    Call,
    MessagesRequestHeader
) => Appendable

case class LoginConfig(
    formRoute: Call,
    submitRoute: Call,
    landingRoute: Call,
    loginView: LoginView
)

class AuthenticationController @Inject() (
    cc: MessagesControllerComponents,
    userDAO: UserDAO,
    authenticated: Authenticated
) extends MessagesAbstractController(cc) {

  def loginConfig = LoginConfig(
    controllers.routes.AuthenticationController.login,
    controllers.routes.LandingController.landing(),
    controllers.routes.AuthenticationController.showLoginForm,
    (form, m, msg) => views.html.login(form, m)(msg)
  )

  private val logger = play.api.Logger(this.getClass)
  private val form = LoginForm.form;

  def showLoginForm = Action { implicit request: MessagesRequest[AnyContent] =>
    Ok(loginConfig.loginView(form, loginConfig.submitRoute, request))
  }

  def login = Action { implicit request: MessagesRequest[AnyContent] =>

    val errorFunction = { (formWithErrors: Form[User]) =>
      // form validation/binding failed...
      BadRequest(loginConfig.loginView(form, loginConfig.submitRoute, request))
    }
    val successFunction = { (user: User) =>
      // form validation/binding succeeded ...
      val foundUser: Boolean = userDAO.lookupUser(user)
      if (foundUser) {
        Redirect(loginConfig.landingRoute)
          .flashing("info" -> "You are logged in.")
          .withSession(Global.SESSION_USERNAME_KEY -> user.username)
      } else {
        Redirect(loginConfig.formRoute)
          .flashing("error" -> "Invalid username/password.")
      }
    }
    val formValidationResult: Form[User] = form.bindFromRequest()
    formValidationResult.fold(errorFunction, successFunction)
  }

  def logout = authenticated { implicit request: Request[AnyContent] =>
    // docs: “withNewSession ‘discards the whole (old) session’”
    Redirect(loginConfig.formRoute)
      .flashing("info" -> "You are logged out.")
      .withNewSession
  }
}

This allows me to write code like:

package website

import javax.inject.Inject

import play.api.mvc._
import play.api.data._
import play.api.data.Forms._

import authentication._
import models.{User, Global, UserDAO}

class AuthenticationController @Inject() (
    cc: MessagesControllerComponents,
    userDAO: UserDAO,
    authenticated: Authenticated
) extends authentication.AuthenticationController(cc, userDAO, authenticated) {

  override def loginConfig = LoginConfig(
    website.routes.AuthenticationController.login,
    website.routes.IndexController.index(),
    website.routes.AuthenticationController.showLoginForm,
    (form, m, msg) => views.html.login(form, m)(msg)
  )

}

I also need to add a template for the login form:

@(form: Form[models.User], postUrl: Call)(implicit request: MessagesRequestHeader)

@base("- login") {
  <div id="content">
    <div id="user-login-form">
      <h1>Login</h1>

      @request.flash.data.map{ case (name, value) =>
        <div>@name: @value</div>
      }

      @* Global errors are not tied to any particular form field *@
      @if(form.hasGlobalErrors) {
        @form.globalErrors.map { (error: FormError) =>
          <div>
            Error: @error.key: @error.message
          </div>
        }
      }

      @helper.form(postUrl, Symbol("id") -> "user-login-form") {
        @helper.CSRF.formField

        @helper.inputText(
          form("username"),
          Symbol("_label") -> "Username",
          Symbol("placeholder") -> "username",
          Symbol("id") -> "username",
          Symbol("size") -> 60
        )

        @helper.inputPassword(
          form("password"),
          Symbol("_label") -> "Password",
          Symbol("placeholder") -> "password",
          Symbol("id") -> "password",
          Symbol("size") -> 60
        )

        <button class="button is-primary">Login</button>
      }

    </div>
  </div>
}

I am hoping I will be able to create a helper for login to make this be just a single line. I wanna figure out the styling first though.

Finally, I update the routes to have:


GET     /website/login              website.AuthenticationController.showLoginForm
POST    /website/login              website.AuthenticationController.login
GET     /website/logout             website.AuthenticationController.logout
GET     /website/landing            website.IndexController.index()

Then I have login that works.

How does the login work actually?

I just modified the code from here by Alvin Alexander, who wrote Author of

  • “Scala Cookbook”,
  • “Functional Programming, Simplified”,
  • “Learn Scala3 The Fast Way”, and more.

Next

Styling the login.


Mandla