Implementing the user registration view

In this section, we are going to accomplish two tasks. Firstly, we are going to create a view layer that facilitates the registration of new users on the Place Reviewer platform. Secondly, we are going to create suitable controllers and actions to present the user with the registration view and handle registration form submissions. Simple enough right? Glad you think so! Go ahead and create a register.html template in the Place Reviewer project. Recall that all template files belong in the templates directory under resources. Now, add the following template HTML to the file:


<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Register</title>
<link rel="stylesheet" th:href="@{/css/app.css}"/>
<link rel="stylesheet"
href="/webjars/bootstrap/4.0.0-beta.3/css/bootstrap.min.css"/>

<script src="/webjars/jquery/3.2.1/jquery.min.js"></script>
<script src="/webjars/bootstrap/4.0.0-beta.3/
js/bootstrap.min.js"
></script>
</head>
<body>
<nav class="navbar navbar-default nav-enhanced">
<div class="container-fluid container-nav">
<div class="navbar-header">
<div class="navbar-brand">
Place Reviewer
</div>
</div>
<ul class="navbar-nav" th:if="${principal != null}">
<li>
<form th:action="@{/logout}" method="post">
<button class="btn btn-danger" type="submit">
<i class="fa fa-power-off" aria-hidden="true"></i>
Sign Out
</button>
</form>
</li>
</ul>
</div>
</nav>
<div class="container-fluid" style="z-index: 2; position: absolute">
<div class="row mt-5">
<div class="col-sm-4 col-xs-2"> </div>
<div class="col-sm-4 col-xs-8">
<form class="form-group col-sm-12 form-vertical form-app"
id="form-register" method="post"
th:action="@{/users/registrations}">
<div class="col-sm-12 mt-2 lead text-center text-primary">
Create an account
</div>
<hr>
<input class="form-control" type="text" name="username"
placeholder="Username" required/>
<input class="form-control mt-2" type="email" name="email"
placeholder="Email" required/>
<input class="form-control mt-2" type="password" name="password"
placeholder="Password" required/>
<span th:if="${error != null}" class="mt-2 text-danger"
style="font-size: 10px" th:text="${error}"></span>
<button class="btn btn-primary form-control mt-2 mb-3"
type="submit">
Sign Up!
</button>
</form>
</div>
<div class="col-sm-4 col-xs-2"></div>
</div>
</div>
</body>
</html>

In this code snippet, we utilized HTML to create a template for the user registration page. The web page in itself is simple. It contains a navigation bar and a form in which a user will input the required registration details for submission. As this is a Thymeleaf template, it should come as no surprise that we utilized some Thymeleaf-specific attributes. Let's take a look at some of these attributes:

  • th:href: This is an attribute modifier attribute. When it is processed by the templating engine, it computes the link URL to be utilized and sets it in the appropriate tag in which it is used. Examples of tags that this attribute can be used in are <a> and <link>. We used the th:href attribute in the code snippet, as shown here:
        <link rel="stylesheet" th:href="@{/css/app.css}"/>
  • th:action: This attribute works just like the HTML action attribute. It specifies where to send the form data when a form is submitted. The following code snippet specifies that the form data should be sent to an endpoint with the path /users/registrations:
        <form class="form-group col-sm-12 form-vertical form-app" 
id="form-register" method="post"
th:action="@{/users/registrations}">
...
</form>
  • th:text: This attribute is used to specify the text held by a container:
        <span th:text="Hello world"></span>
  • th:if: This attribute can be used to specify whether an HTML tag should be rendered based on the result of a conditional test:
        <span th:if="${error != null}" class="mt-2 text-danger" 
style="font-size: 10px" th:text="${error}"></span>

In this code snippet, if a model attribute error exists and its value is not equal to null, then the span tag is rendered on the HTML page; otherwise, it is not rendered.

We also made use of th:if in our navigation bar to specify when it should display a button permitting a user to log out of their account:

<ul class="navbar-nav" th:if="${principal != null}">
<li>
<form th:action="@{/logout}" method="post">
<button class="btn btn-danger" type="submit">
<i class="fa fa-power-off" aria-hidden="true"></i> Sign Out
</button>
</form>
</li>
</ul>

If the principal model attribute is set in the template and it is not null, then the sign out button is displayed. The principal will always be null unless the user is logged in to their account.

How we added the navigation bar to our template directly may appear to be all right at first glance but it is important we put more thought into what we did. It is not uncommon to make use of a navigation bar DOM element more than once within an application. In fact, this is done very often! We do not want to have to keep rewriting this same code for a navigation bar over and over again in our templates. To avoid this unnecessary repetition, we need to implement the navigation bar as a fragment that can be included at any time within a template.

Create a fragments directory within templates and add a navbar.html file with the following code to it:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
</head>
<body>
<nav class="navbar navbar-default nav-enhanced" th:fragment="navbar">
<div class="container-fluid container-nav">
<div class="navbar-header">
<div class="navbar-brand">
Place Reviewer
</div>
</div>
<ul class="navbar-nav" th:if="${principal != null}">
<li>
<form th:action="@{/logout}" method="post">
<button class="btn btn-danger" type="submit">
<i class="fa fa-power-off" aria-hidden="true"></i> Sign Out
</button>
</form>
</li>
</ul>
</div>
</nav>
</body>
</html>

In this code snippet, we defined a navigation bar fragment available for inclusion in templates with the th:fragment attribute. A defined fragment can be inserted at any time within a template with the use of th:insert. Modify the inner HTML of the <body> tag in register.html to make use of the newly defined fragment as follows:

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Register</title>
<link rel="stylesheet" th:href="@{/css/app.css}"/>
<link rel="stylesheet"
href="/webjars/bootstrap/4.0.0-beta.3/css/bootstrap.min.css"/>
<script src="/webjars/jquery/3.2.1/jquery.min.js"></script>
<script src="/webjars/bootstrap/4.0.0-beta.3/
js/bootstrap.min.js"></script>
</head>
<body>
<div th:insert="fragments/navbar :: navbar"></div>
<!-- inserting navbar fragment -->
<div class="container-fluid" style="z-index: 2; position: absolute">
<div class="row mt-5">
<div class="col-sm-4 col-xs-2">
</div>
<div class="col-sm-4 col-xs-8">
<form class="form-group col-sm-12 form-vertical form-app"
id="form-register" method="post"
th:action="@{/users/registrations}">
<div class="col-sm-12 mt-2 lead text-center text-primary">
Create an account
</div>
<hr>
<input class="form-control" type="text" name="username"
placeholder="Username" required/>
<input class="form-control mt-2" type="email" name="email"
placeholder="Email" required/>
<input class="form-control mt-2" type="password"
name="password" placeholder="Password" required/>
<span th:if="${error != null}" class="mt-2 text-danger"
style="font-size: 10px" th:text="${error}"></span>
<button class="btn btn-primary form-control mt-2 mb-3"
type="submit">
Sign Up!
</button>
</form>
</div>
<div class="col-sm-4 col-xs-2"></div>
</div>
</div>
</body>
</html>

As can already be seen, the separation of our navigation bar HTML into a fragment has made our code more succinct and will contribute positively to the quality of our developed templates.

Having created the necessary template for the user registration page, we need to create a controller that will render this template to a visitor of the site. Let's create an application controller. Its job will be to render the web pages of the Place Reviewer application to a user upon request.

Add the ApplicationController class, shown here, to the controller package:

package com.example.placereviewer.controller

import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping

@Controller
class ApplicationController {

@GetMapping("/register")
fun register(): String {
return "register"
}
}

Nothing special is being done in the code snippet here. We created an MVC controller with a single action that handles a HTTP GET request to the /register path by rendering the register.html view to the user.

We are almost ready to view our newly created registration page. Before we check it out, we must add the app.css file required by register.html. Static resources such as CSS files should be added to the static directory within the application resource directory. Add a css directory within the static directory and add an app.css file containing the code shown here to it:

//app.css
.nav-enhanced {
background-color: #00BFFF;
border-color: blueviolet;
box-shadow: 0 0 3px black;
}

.container-nav {
height: 10%;
width: 100%;
margin-bottom: 0;
}

.form-app {
background-color: white;
box-shadow: 0 0 1px black;
margin-top: 50px !important;
padding: 10px 0;
}

Great work! Now, go ahead and run the Place Reviewer application. Upon starting the app, open your favorite browser and access the web page residing at http://localhost:5000/register.

Now, we must implement the logic involved in registering the user. To do this, we must declare an action that accepts the form data sent by the registration form and appropriately processes the data, with the goal of registering the user successfully on the platform. If you recall, we specified that the form data should be sent via POST to /users/registrations. Consequently, we need an action that handles such a HTTP request. Add a UserController class to the com.example.placereviewer.controller package with the code shown here:

package com.example.placereviewer.controller

import com.example.placereviewer.component.UserValidator
import com.example.placereviewer.data.model.User
import com.example.placereviewer.service.SecurityService
import com.example.placereviewer.service.UserService
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.validation.BindingResult
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.ModelAttribute
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestMapping

@Controller
@RequestMapping("/users")
class UserController(val userValidator: UserValidator,
val
userService: UserService, val securityService: SecurityService) {

@PostMapping("/registrations")
fun create(@ModelAttribute form: User, bindingResult: BindingResult,
model: Model): String {
userValidator.validate(form, bindingResult)

if (bindingResult.hasErrors()) {
model.addAttribute("error", bindingResult.allErrors.first()
.defaultMessage)
model.addAttribute("username", form.username)
model.addAttribute("email", form.email)
model.addAttribute("password", form.password)

return "register"
}

userService.register(form.username, form.email, form.password)
securityService.autoLogin(form.username, form.password)

return "redirect:/home"
}
}

create() handles HTTP POST requests sent to /users/registrations. It takes three arguments. The first is form, which is an object of the User class. @ModelAttribute is used to annotate form. @ModelAttribute indicates that the argument should be retrieved by the model. The form model attribute is populated by data submitted by the form to the endpoint. The username, email, and password parameters are all submitted by the registration form. All objects of type User have username, email, and password properties, hence the data submitted by the form is assigned to the corresponding model properties.

The second argument of the function is an instance of BindingResult. BindingResult serves as a result holder for DataBinder. In this case, we used it to bind results of the validation process done by a UserValidator, which we are going to create in a bit. The third argument is a Model. We use this to add attributes to our model for subsequent access by the view layer.

Before proceeding further with explanations pertaining to the logic implemented in the create() action, we must implement both UserValidator and SecurityService. UserValidator has the sole task of validating user information submitted to the backend. Create a com.example.placereviewer.component package and include the UserValidator class here to it:

package com.example.placereviewer.component

import com.example.placereviewer.data.model.User
import com.example.placereviewer.data.repository.UserRepository
import org.springframework.stereotype.Component
import org.springframework.validation.Errors
import org.springframework.validation.ValidationUtils
import org.springframework.validation.Validator

@Component
class UserValidator(private val userRepository: UserRepository) : Validator {

override fun supports(aClass: Class<*>?): Boolean {
return User::class == aClass
}

override fun validate(obj: Any?, errors: Errors) {
val user: User = obj as User

Validating that submitted user parameters are not empty. An empty parameter is rejected with an error code and error message:


ValidationUtils.rejectIfEmptyOrWhitespace(errors, "username",
"Empty.userForm.username", "Username cannot be empty")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "password",
"Empty.userForm.password", "Password cannot be empty")
ValidationUtils.rejectIfEmptyOrWhitespace(errors, "email",
"Empty.userForm.email", "Email cannot be empty")


Validating the length of a submitted username. A username whose length is less than 6 is rejected:


if (user.username.length < 6) {
errors.rejectValue("username", "Length.userForm.username",
"Username must be at least 6 characters in length")
}

Validating the submitted username does not already exist. A username already taken by a user is rejected:


if (userRepository.findByUsername(user.username) != null) {
errors.rejectValue("username", "Duplicate.userForm.username",
"Username unavailable")
}

Validating the length of a submitted password. Passwords less than 8 characters in length are rejected:


if (user.password.length < 8) {
errors.rejectValue("password", "Length.userForm.password",
"Password must be at least 8 characters in length")
}
}
}

UserValidator implements the Validator interface, which is used to validate objects. As such, it overrides two methods: supports(Class<*>?) and validate(Any?, Errors). supports() is used to assert that the validator can validate the object supplied to it. In the case of UserValidator, supports() asserts that the supplied object is an instance of the User class. Hence, all objects of type User are supported for validation by UserValidator.

validate() validates the provided objects. In cases where validation rejections occur, it registers the error with the provided Error object. Ensure you read through the comments placed within the body of the validate() method to get a better grasp of what is going on within the method.

Now, we shall work on SecurityService. We will implement a SecurityService to facilitate the identification of the currently logged in user and the automatic login of a user after their registration.

Add the SecurityService interface here to com.example.placereviewer.service:

package com.example.placereviewer.service

interface SecurityService {
fun findLoggedInUser(): String?
fun autoLogin(username: String, password: String)
}

Now, add a SecurityServiceImpl class to com.example.placereviewer.service. As the name suggests, SecurityServiceImpl implements SecurityService:

package com.example.placereviewer.service

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.security.authentication.AuthenticationManager
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.stereotype.Service


@Service
class SecurityServiceImpl(private val userDetailsService: AppUserDetailsService)
: SecurityService {

@Autowired
lateinit var authManager: AuthenticationManager

override fun findLoggedInUser(): String? {
val userDetails = SecurityContextHolder.getContext()
.authentication.details

if (userDetails is UserDetails) {
return userDetails.username
}

return null
}

override fun autoLogin(username: String, password: String) {
val userDetails: UserDetails = userDetailsService
.loadUserByUsername(username)

val usernamePasswordAuthenticationToken =
UsernamePasswordAuthenticationToken(userDetails, password,
userDetails.authorities)

authManager.authenticate(usernamePasswordAuthenticationToken)

if (usernamePasswordAuthenticationToken.isAuthenticated) {
SecurityContextHolder.getContext().authentication =
usernamePasswordAuthenticationToken
}
}
}

findLoggedInUser() returns the username of the currently logged in user. Username retrieval is done with the help of Spring Framework's SecurityContextHolder class. An instance of UserDetails is retrieved by accessing the logged in user's authentication details with a call to SecurityContextHolder.getContext().authentication.details. It is important to note that SecurityContextHolder.getContext().authentication.details returns an Object and not an instance of UserDetails. As such, we must do a type check to assert that the object retrieved conforms to the type UserDetails as well. If it does, we return the username of the currently logged in user. Otherwise, we return null.

The autoLogin() method will be used for the simple task of authenticating a user after registering on the platform. The submitted username and password of the user are passed as arguments to autoLogin(), after which an instance of UsernamePasswordAuthenticationToken is created for the registered user. Once an instance of UsernamePasswordAuthenticationToken is created, we utilize AuthenticationManager to authenticate the user's token. If UsernamePasswordAuthenticationToken is successfully authenticated, we set the authentication property of the current user to UsernamePasswordAuthenticationToken.

Having made our necessary class additions, let's return to our UserController to finish up our explanation of the create action. Within create(), first and foremost the submitted form input is validated with an instance of UserValidator. Errors arising during the course of form data validation are all bound to the instance of BindingResult injected into our controller by Spring. Consider these lines of code:

if (bindingResult.hasErrors()) {
model.addAttribute("error", bindingResult.allErrors
.first().defaultMessage)
model.addAttribute("username", form.username)
model.addAttribute("email", form.email)
model.addAttribute("password", form.password)

return "register"
}

bindingResult is first checked to assert whether any errors occurred during form data validation. If errors occurred, we retrieve the message of the first error detected and set a model attribute error to hold the error message for later access by the view. In addition, we create model attributes to hold each input submitted by the user. Lastly, we re-render the registration view to the user.

Notice how we made multiple method invocations for the same Model instance in the previous code snippet. There is a much cleaner way we can do this. This involves the use of Kotlin's with function:

if (bindingResult.hasErrors()) {
with (model) {
addAttribute("error", bindingResult.allErrors.first().defaultMessage)
addAttribute("error", form.username)
addAttribute("email", form.email)
addAttribute("password", form.password)
}
return "register"
}

See how easy and convenient the function is to use? Go ahead and modify UserController to make use of with, as shown in the preceding code.

You may be wondering why we decided to store a user's submitted data in model attributes. We did this to have a way to reset the data contained in the registration form to what was originally submitted after the re-rendering of the registration view. It will certainly be frustrating for a user to have to input all form data over and over, even if only one form input entered is invalid.

When no input submitted by the user is invalid, the following code runs:

userService.register(form.username, form.email, form.password)
securityService.autoLogin(form.username, form.password)
return "redirect:/home"

As expected, when the data submitted by a user is valid, he is registered on the platform and logged in to his account automatically. Lastly, he is redirected to his home page. Before we try out our registration form, we must do two things:

  • Utilize the model attributes specified in register.html
  • Create a home.html template and a controller to render the template

Luckily for us, both are rather simple to do. First, to utilize the model attributes. Modify the form contained in register.html as follows:

<form class="form-group col-sm-12 form-vertical form-app" 
id="form-register" method="post" th:action="@{/users/registrations}">
<div class="col-sm-12 mt-2 lead text-center text-primary">
Create an account
</div>
<hr>
<!-- utilized model attributes with th:value -->
<input class="form-control" type="text" name="username"
placeholder="Username" th:value="${username}" required/>
<input class="form-control mt-2" type="email" name="email"
placeholder="Email" th:value="${email}" required/>
<input class="form-control mt-2" type="password" name="password"
placeholder="Password" th:value="${password}" required/>
<span th:if="${error != null}" class="mt-2 text-danger"
style="font-size: 10px" th:text="${error}"></span>
<button class="btn btn-primary form-control mt-2 mb-3" type="submit">
Sign Up!
</button>
</form>

Can you spot the changes we made? If you said we used Thymeleaf's th:value template attribute to preset the value held by form inputs to their respective model attribute values, you are right. Now, let's make a simple home.html template. Add the home.html template here to the templates directory:

<html>
<head>
<title> Home</title>
</head>
<body>
You have been successfully registered and are now in your home page.
</body>
</html>

Now, update ApplicationController to include an action handling GET requests to /home, as shown here:

package com.example.placereviewer.controller

import com.example.placereviewer.service.ReviewService
import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import java.security.Principal
import javax.servlet.http.HttpServletRequest

@Controller
class ApplicationController(val reviewService: ReviewService) {

@GetMapping("/register")
fun register(): String {
return "register"
}

@GetMapping("/home")
fun home(request: HttpServletRequest, model: Model,
principal: Principal): String {
val reviews = reviewService.listReviews()

model.addAttribute("reviews", reviews)
model.addAttribute("principal", principal)

return "home"
}
}

The home action retrieves a list of all reviews stored within the database. In addition, the home action sets a model attribute that holds a principal containing information on the currently logged in user. Lastly, the home action renders the home page to the user.

Having done what's necessary, let's register a user on the Place Reviewer platform. Build and run the application, and access the registration page from your browser (http://localhost:5000/register). Firstly, we want to check whether our form validations work by inputting and submitting invalid form data.

As can be seen, the error was detected by UserValidator and was successfully bound to BindingResult, then rendered appropriately as an error in the view. Feel free to enter invalid data for other form inputs and ensure the other validations we implemented work as expected. Now to verify that our registration logic works. Input king.kevin, [email protected], and Kingsman406 in the username, email, and password fields, then click Sign Up! A new account will be created and you will be presented with the home page:

It will come as no surprise to you that we are going to make serious modifications to the home page over the course of this chapter. However, for now let's turn our attention towards creating a suitable user login page.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.236.27