playframework - Adding login
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 = (
[models.User],
Form,
Call
MessagesRequestHeader) => Appendable
This is used in the LoginConfig
which looks like:
case class LoginConfig(
: Call,
formRoute: Call,
submitRoute: Call,
landingRoute: LoginView
loginView)
Used with a function so that each subclass can configure it themselves
def loginConfig = LoginConfig(
.routes.AuthenticationController.login,
controllers.routes.LandingController.landing(),
controllers.routes.AuthenticationController.showLoginForm,
controllers(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 = (
[models.User],
Form,
Call
MessagesRequestHeader) => Appendable
case class LoginConfig(
: Call,
formRoute: Call,
submitRoute: Call,
landingRoute: LoginView
loginView)
class AuthenticationController @Inject() (
: MessagesControllerComponents,
cc: UserDAO,
userDAO: Authenticated
authenticated) extends MessagesAbstractController(cc) {
def loginConfig = LoginConfig(
.routes.AuthenticationController.login,
controllers.routes.LandingController.landing(),
controllers.routes.AuthenticationController.showLoginForm,
controllers(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()
.fold(errorFunction, successFunction)
formValidationResult}
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() (
: MessagesControllerComponents,
cc: UserDAO,
userDAO: Authenticated
authenticated) extends authentication.AuthenticationController(cc, userDAO, authenticated) {
override def loginConfig = LoginConfig(
.routes.AuthenticationController.login,
website.routes.IndexController.index(),
website.routes.AuthenticationController.showLoginForm,
website(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
Mandla