Add custom error page, add profile settings page '/profile/'

This commit is contained in:
Dmitry Isaenko 2023-10-06 22:07:15 +03:00
parent 147f590410
commit bb6059d63e
28 changed files with 401 additions and 35 deletions

View file

@ -1,8 +1,10 @@
package ru.redrise.marinesco; package ru.redrise.marinesco;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
//@PreAuthorize("hasRole('USER')")
@Controller @Controller
public class RootController { public class RootController {

View file

@ -7,6 +7,7 @@ import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner; import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.password.PasswordEncoder;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import ru.redrise.marinesco.data.RolesRepository; import ru.redrise.marinesco.data.RolesRepository;
@ -25,13 +26,14 @@ public class ShinyApplicationRunner {
} }
@Bean @Bean
public ApplicationRunner appRunner() { public ApplicationRunner appRunner(PasswordEncoder encoder) {
return args -> { return args -> {
if (isFirstRun()) { if (isFirstRun()) {
log.info("Application first run");
setRoles(); setRoles();
setAdmin(args); setAdmin(args, encoder);
} else } else
log.info("NOT FIRST APPLICATION RUN; DB Already set up"); log.info("Regular run");
}; };
} }
@ -45,7 +47,7 @@ public class ShinyApplicationRunner {
new UserRole(null, "User", UserRole.Type.USER))); new UserRole(null, "User", UserRole.Type.USER)));
} }
private void setAdmin(ApplicationArguments args) { private void setAdmin(ApplicationArguments args, PasswordEncoder encoder) {
List<String> login = args.getOptionValues("admin_login"); List<String> login = args.getOptionValues("admin_login");
List<String> password = args.getOptionValues("admin_password"); List<String> password = args.getOptionValues("admin_password");
@ -53,10 +55,11 @@ public class ShinyApplicationRunner {
if (login == null || login.size() == 0 || password == null || password.size() == 0) { if (login == null || login.size() == 0 || password == null || password.size() == 0) {
log.warn("No administrator credentials provided, using defaults:\n * Login: root\n * Password: root\n Expected: --admin_login LOGIN --admin_password PASSWORD "); // TODO: Move into properties i18n log.warn("No administrator credentials provided, using defaults:\n * Login: root\n * Password: root\n Expected: --admin_login LOGIN --admin_password PASSWORD "); // TODO: Move into properties i18n
var adminUser = new User("root", "root", "SuperAdmin", adminRoleOnlyAthority); var adminUser = new User("root", encoder.encode("root"), "SuperAdmin", adminRoleOnlyAthority);
users.save(adminUser); users.save(adminUser);
return; return;
} }
users.save(new User(login.get(0), password.get(0), "SuperAdmin", adminRoleOnlyAthority)); log.info("SuperAdmin role created\n * Login: {}", login.get(0));
users.save(new User(login.get(0), encoder.encode(password.get(0)), "SuperAdmin", adminRoleOnlyAthority));
} }
} }

View file

@ -1,11 +1,12 @@
package ru.redrise.marinesco; package ru.redrise.marinesco;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetails;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
@ -14,14 +15,12 @@ import jakarta.persistence.Table;
import lombok.AccessLevel; import lombok.AccessLevel;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import ru.redrise.marinesco.security.UserRole; import ru.redrise.marinesco.security.UserRole;
@Data @Data
@Entity @Entity
@Table(name = "\"USER\"") @Table(name = "\"USER\"")
@NoArgsConstructor(access = AccessLevel.PRIVATE, force = true) @NoArgsConstructor(access = AccessLevel.PRIVATE, force = true)
@RequiredArgsConstructor
public class User implements UserDetails{ public class User implements UserDetails{
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
@ -31,12 +30,19 @@ public class User implements UserDetails{
private Long id; private Long id;
private final String username; private final String username;
private final String password; private String password;
private final String displayname; private String displayname;
@ManyToMany @ManyToMany(cascade = CascadeType.REMOVE, fetch = FetchType.EAGER)
private final List<UserRole> authorities; private final List<UserRole> authorities;
public User(String username, String password, String displayname, List<UserRole> authorities){
this.username = username;
this.password = password;
this.displayname = displayname;
this.authorities = authorities;
}
@Override @Override
public boolean isAccountNonExpired() { public boolean isAccountNonExpired() {
return true; return true;

View file

@ -0,0 +1,23 @@
package ru.redrise.marinesco;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Data
public class UserSettingsForm {
@NotNull
@NotEmpty(message = "Display name could not be blank")
public String displayname;
public String newPassword;
public boolean isNewPasswordSet(){
return ! newPassword.isBlank();
}
public boolean isNewPasswordValid(){
final int newPasswordLength = newPassword.length();
return newPasswordLength > 8 && newPasswordLength < 32;
}
}

View file

@ -2,10 +2,14 @@ package ru.redrise.marinesco.security;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping; 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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.validation.Valid;
import ru.redrise.marinesco.data.RolesRepository; import ru.redrise.marinesco.data.RolesRepository;
import ru.redrise.marinesco.data.UserRepository; import ru.redrise.marinesco.data.UserRepository;
@ -16,20 +20,33 @@ public class RegistrationController {
private RolesRepository rolesRepo; private RolesRepository rolesRepo;
private PasswordEncoder passwordEncoder; private PasswordEncoder passwordEncoder;
public RegistrationController(UserRepository userRepo, RolesRepository rolesRepo, PasswordEncoder passwordEncoder){ public RegistrationController(UserRepository userRepo, RolesRepository rolesRepo, PasswordEncoder passwordEncoder) {
this.userRepo = userRepo; this.userRepo = userRepo;
this.rolesRepo = rolesRepo; this.rolesRepo = rolesRepo;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
} }
@ModelAttribute(name = "registrationForm")
public RegistrationForm form() {
return new RegistrationForm();
}
@GetMapping @GetMapping
public String registerForm(){ public String registerForm() {
return "registration"; return "registration";
} }
@PostMapping @PostMapping
public String postMethodName(RegistrationForm registrationForm) { public String postMethodName(@Valid RegistrationForm registerForm, Errors errors, Model model) {
userRepo.save(registrationForm.toUser(passwordEncoder, rolesRepo)); if (registerForm.isPasswordsNotEqual()){
model.addAttribute("passwordsMismatch", "Passwords must be the same.");
return "registration";
}
if (errors.hasErrors()) {
return "registration";
}
userRepo.save(registerForm.toUser(passwordEncoder, rolesRepo));
return "redirect:/login"; return "redirect:/login";
} }
} }

View file

@ -2,20 +2,28 @@ package ru.redrise.marinesco.security;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Data; import lombok.Data;
import ru.redrise.marinesco.User; import ru.redrise.marinesco.User;
import ru.redrise.marinesco.data.RolesRepository; import ru.redrise.marinesco.data.RolesRepository;
@Data @Data
public class RegistrationForm { public class RegistrationForm {
@NotNull @NotNull
@Size(min=3, max=32, message="Username must be at least 3 characters long. Should not exceed 32 characters.")
private String username; private String username;
@NotNull @NotNull
@Size(min=8, max = 32, message="Password must be at least 8 characters long. Should not exceed 32 characters.")
private String password; private String password;
private String passwordConfirm;
@NotNull @NotNull
private String fullname; @NotEmpty(message = "Display name could not be blank")
@NotNull
private String displayname; private String displayname;
public User toUser(PasswordEncoder passwordEncoder, RolesRepository rolesRepo){ public User toUser(PasswordEncoder passwordEncoder, RolesRepository rolesRepo){
@ -25,4 +33,7 @@ public class RegistrationForm {
displayname, displayname,
rolesRepo.findByType(UserRole.Type.USER)); rolesRepo.findByType(UserRole.Type.USER));
} }
public boolean isPasswordsNotEqual(){
return ! password.equals(passwordConfirm);
}
} }

View file

@ -45,19 +45,25 @@ public class SecurityConfig {
public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception { public SecurityFilterChain filterChain(HttpSecurity http, MvcRequestMatcher.Builder mvc) throws Exception {
return http return http
.authorizeHttpRequests(autorize -> autorize .authorizeHttpRequests(autorize -> autorize
.requestMatchers(mvc.pattern("/favicon.ico")).permitAll()
.requestMatchers(mvc.pattern("/jquery.js")).permitAll()
.requestMatchers(mvc.pattern("/styles/**")).permitAll() .requestMatchers(mvc.pattern("/styles/**")).permitAll()
.requestMatchers(mvc.pattern("/images/*")).permitAll() .requestMatchers(mvc.pattern("/images/*")).permitAll()
.requestMatchers(mvc.pattern("/register")).permitAll() .requestMatchers(mvc.pattern("/register")).permitAll()
.requestMatchers(mvc.pattern("/login")).permitAll() .requestMatchers(mvc.pattern("/login")).permitAll()
.requestMatchers(mvc.pattern("/error")).permitAll()
.requestMatchers(mvc.pattern("/")).hasAnyRole("ADMIN", "USER")
.requestMatchers(mvc.pattern("/profile/**")).hasAnyRole("ADMIN", "USER")
.requestMatchers(PathRequest.toH2Console()).permitAll() .requestMatchers(PathRequest.toH2Console()).permitAll()
//.requestMatchers(mvc.pattern("/design/**")).hasRole("USER") //.requestMatchers(mvc.pattern("/design/**")).hasRole("USER")
.anyRequest().denyAll()) .anyRequest().denyAll())
//.anyRequest().permitAll())
.formLogin(formLoginConfigurer -> formLoginConfigurer .formLogin(formLoginConfigurer -> formLoginConfigurer
.loginPage("/login") .loginPage("/login")
.loginProcessingUrl("/auth") //.loginProcessingUrl("/auth")
.usernameParameter("login") .usernameParameter("login")
.passwordParameter("pwd") .passwordParameter("pwd")
//.defaultSuccessUrl("/", true) .defaultSuccessUrl("/")
) )
// .formLogin(Customizer.withDefaults()) // .formLogin(Customizer.withDefaults())
//.oauth2Login(c -> c.loginPage("/login")) //.oauth2Login(c -> c.loginPage("/login"))

View file

@ -0,0 +1,82 @@
package ru.redrise.marinesco.security;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
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;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import ru.redrise.marinesco.User;
import ru.redrise.marinesco.UserSettingsForm;
import ru.redrise.marinesco.data.UserRepository;
@Slf4j
@Controller
@RequestMapping("/profile")
public class UserSettingsController {
private final UserRepository userRepo;
private final PasswordEncoder passwordEncoder;
public UserSettingsController(UserRepository userRepo, PasswordEncoder passwordEncoder){
this.userRepo = userRepo;
this.passwordEncoder = passwordEncoder;
}
@ModelAttribute
public void addMisc(Model model, @AuthenticationPrincipal User user){
final String displayName = user.getDisplayname();
model.addAttribute("header_text", "Welcome " + displayName);
UserSettingsForm form = new UserSettingsForm();
form.setDisplayname(displayName);
model.addAttribute( "userSettingsForm", form);
}
@ModelAttribute
public UserSettingsForm addSettingsForm(){
return new UserSettingsForm();
}
@GetMapping
public String getPage(){
return "redirect:/profile/settings";
}
@GetMapping("/settings")
public String getSettingsFirstPage(){
return "user_settings";
}
@PostMapping("/settings")
public String getSettingsPage(@Valid UserSettingsForm userSettingsForm,
Errors errors,
@AuthenticationPrincipal User user,
Model model){
if (errors.hasErrors())
return "user_settings";
if (! user.getDisplayname().equals(userSettingsForm.getDisplayname()))
user.setDisplayname(userSettingsForm.getDisplayname());
if (userSettingsForm.isNewPasswordSet()){
if (userSettingsForm.isNewPasswordValid()){
user.setPassword(passwordEncoder.encode(userSettingsForm.getNewPassword()));
}
else{
model.addAttribute("password_incorrect", "Password must be at least 8 characters long. Should not exceed 32 characters.");
return "user_settings";
}
}
log.info("{} {}", userSettingsForm.getDisplayname(), userSettingsForm.getNewPassword());
userRepo.save(user);
return "user_settings";
}
}

View file

@ -0,0 +1,24 @@
package ru.redrise.marinesco.web;
import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.servlet.RequestDispatcher;
import jakarta.servlet.http.HttpServletRequest;
@Controller
public class HandyErrorController implements ErrorController{
@ModelAttribute(name = "code")
public String addMisc(HttpServletRequest request){
return request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE).toString();
}
@RequestMapping("/error")
public String handleError(){
return "error";
}
}

2
src/main/resources/static/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type=text/css>
html {
height: 100%;
}
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #212121;
color: #cfcfcf;
font-family: Terminus;
}
.error {
border-width: 3px;
border-style: double;
padding: 8px;
margin: auto;
}
.bli {
padding: 3px;
background-color: #D00000;
animation: blinker 2.5s ease infinite;
}
@keyframes blinker {
50% {
opacity: 0.0;
}
}
</style>
<title th:text="${code}">title</title>
</head>
<body>
<div class='error'>
ОШИБКА: <span class='bli' th:text="${code}"></span>
</div>
</body>
</html>

View file

@ -16,6 +16,7 @@
<br /><a href="/h2">H2</a> <br /><a href="/h2">H2</a>
<form class="form-signin" method="post" action="/login"> <form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2> <h2 class="form-signin-heading">Please sign in</h2>
<br /><span class="validationError" th:if="${param.error}">Unable to login. Check your username and password.</span>
<p> <p>
<label for="username" class="sr-only">Username</label> <label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="login" class="form-control" placeholder="Username" required autofocus> <input type="text" id="username" name="login" class="form-control" placeholder="Username" required autofocus>

View file

@ -5,20 +5,34 @@
<title>Marinesco - registration form</title> <title>Marinesco - registration form</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico"> <link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" th:href="@{/styles/styles.css}" /> <link rel="stylesheet" th:href="@{/styles/styles.css}" />
<script src="@{/jquery.js}"></script>
</head> </head>
<body> <body>
<h1>Register</h1> <h1>Register</h1>
<img th:src="@{/images/logo.svg}" /> <img th:src="@{/images/logo.svg}" />
<form method="POST" th:action="@{/register}" id="registerForm"> <form class="regForm" method="POST" th:action="@{/register}" th:object="${registrationForm}">
<span class="validationError" th:if="${#fields.hasErrors('username')}" th:errors="*{username}">Error</span>
<br />
<label for="username">Username: </label> <label for="username">Username: </label>
<input type="text" name="login" /><br /> <input type="text" name="username" id="username"/><br />
<span class="validationError" th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Error</span>
<br />
<label for="password">Password: </label> <label for="password">Password: </label>
<input type="password" name="pwd" /><br /> <input type="password" name="password" id="password"/><br />
<span class="validationError" th:if="${passwordsMismatch} != null" th:text="${passwordsMismatch}">false</span>
<br />
<label for="confirm">Confirm password: </label> <label for="confirm">Confirm password: </label>
<input type="password" name="confirm" /><br /> <input type="password" name="passwordConfirm" id="passwordConfirm" /><br />
<span class="validationError" th:if="${#fields.hasErrors('displayname')}" th:errors="*{displayname}">Error</span>
<br />
<label for="displayname">Displayed name: </label> <label for="displayname">Displayed name: </label>
<input type="text" name="fullname" /><br /> <input type="text" name="displayname" id="displayname"/><br />
<input type="submit" value="Register" /> <input type="submit" value="Register" />
</form> </form>
</body> </body>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Marinesco - Profile settings</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" th:href="@{/styles/styles.css}" />
</head>
<body>
<h1 th:text="${header_text}">welcome</h1>
<form method="POST" th:action="@{/profile/settings}" th:object="${userSettingsForm}"> <!-- -->
<span class="validationError" th:if="${#fields.hasErrors('displayname')}" th:errors="*{displayname}">Error</span>
<br />
<label for="displayname">Displayed name: </label>
<input type="text" name="displayname" id="displayname" th:value="${userSettingsForm.displayname}" /><br />
<span class="validationError" th:if="${password_incorrect} != null" th:text="${password_incorrect}">false</span>
<br />
<label for="password">New password: </label>
<input type="password" name="newPassword" id="newPassword" /><br />
<input type="submit" value="Save Changes" />
</form>
</body>
</html>

2
target/classes/static/jquery.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,53 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style type=text/css>
html {
height: 100%;
}
body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background-color: #212121;
color: #cfcfcf;
font-family: Terminus;
}
.error {
border-width: 3px;
border-style: double;
padding: 8px;
margin: auto;
}
.bli {
padding: 3px;
background-color: #D00000;
animation: blinker 2.5s ease infinite;
}
@keyframes blinker {
50% {
opacity: 0.0;
}
}
</style>
<title th:text="${code}">title</title>
</head>
<body>
<div class='error'>
ОШИБКА: <span class='bli' th:text="${code}"></span>
</div>
</body>
</html>

View file

@ -16,6 +16,7 @@
<br /><a href="/h2">H2</a> <br /><a href="/h2">H2</a>
<form class="form-signin" method="post" action="/login"> <form class="form-signin" method="post" action="/login">
<h2 class="form-signin-heading">Please sign in</h2> <h2 class="form-signin-heading">Please sign in</h2>
<br /><span class="validationError" th:if="${param.error}">Unable to login. Check your username and password.</span>
<p> <p>
<label for="username" class="sr-only">Username</label> <label for="username" class="sr-only">Username</label>
<input type="text" id="username" name="login" class="form-control" placeholder="Username" required autofocus> <input type="text" id="username" name="login" class="form-control" placeholder="Username" required autofocus>

View file

@ -5,20 +5,34 @@
<title>Marinesco - registration form</title> <title>Marinesco - registration form</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico"> <link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" th:href="@{/styles/styles.css}" /> <link rel="stylesheet" th:href="@{/styles/styles.css}" />
<script src="@{/jquery.js}"></script>
</head> </head>
<body> <body>
<h1>Register</h1> <h1>Register</h1>
<img th:src="@{/images/logo.svg}" /> <img th:src="@{/images/logo.svg}" />
<form method="POST" th:action="@{/register}" id="registerForm"> <form class="regForm" method="POST" th:action="@{/register}" th:object="${registrationForm}">
<span class="validationError" th:if="${#fields.hasErrors('username')}" th:errors="*{username}">Error</span>
<br />
<label for="username">Username: </label> <label for="username">Username: </label>
<input type="text" name="login" /><br /> <input type="text" name="username" id="username"/><br />
<span class="validationError" th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Error</span>
<br />
<label for="password">Password: </label> <label for="password">Password: </label>
<input type="password" name="pwd" /><br /> <input type="password" name="password" id="password"/><br />
<span class="validationError" th:if="${passwordsMismatch} != null" th:text="${passwordsMismatch}">false</span>
<br />
<label for="confirm">Confirm password: </label> <label for="confirm">Confirm password: </label>
<input type="password" name="confirm" /><br /> <input type="password" name="passwordConfirm" id="passwordConfirm" /><br />
<span class="validationError" th:if="${#fields.hasErrors('displayname')}" th:errors="*{displayname}">Error</span>
<br />
<label for="displayname">Displayed name: </label> <label for="displayname">Displayed name: </label>
<input type="text" name="fullname" /><br /> <input type="text" name="displayname" id="displayname"/><br />
<input type="submit" value="Register" /> <input type="submit" value="Register" />
</form> </form>
</body> </body>

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
<title>Marinesco - Profile settings</title>
<link rel="icon" type="image/x-icon" href="/favicon.ico">
<link rel="stylesheet" th:href="@{/styles/styles.css}" />
</head>
<body>
<h1 th:text="${header_text}">welcome</h1>
<form method="POST" th:action="@{/profile/settings}" th:object="${userSettingsForm}"> <!-- -->
<span class="validationError" th:if="${#fields.hasErrors('displayname')}" th:errors="*{displayname}">Error</span>
<br />
<label for="displayname">Displayed name: </label>
<input type="text" name="displayname" id="displayname" th:value="${userSettingsForm.displayname}" /><br />
<span class="validationError" th:if="${password_incorrect} != null" th:text="${password_incorrect}">false</span>
<br />
<label for="password">New password: </label>
<input type="password" name="newPassword" id="newPassword" /><br />
<input type="submit" value="Save Changes" />
</form>
</body>
</html>