Add form validator to users package

Closes #35
Closes #59
Closes #64
Closes #71

Co-authored-by: Simon Bruder <simon.bruder@mailbox.tu-dresden.de>
This commit is contained in:
Denis Natusch 2023-11-22 00:05:03 +01:00
parent b72fa87445
commit d7c4482200
No known key found for this signature in database
GPG key ID: 5E57BD8EDACFA985
13 changed files with 228 additions and 129 deletions

View file

@ -285,7 +285,8 @@ image:models/design/user.svg[class design diagram - User]
|UserManagement |A class that manages the UserRepository.
|UserRepository |An extension of 'CrudRepository' to store Users.
|User |A class that allows a person to associate system data with themselves.
|RegistrationForm |A Form to cache a user input that was made while registration.
|UserForm |A Form to cache a user input that was made during registration or updating the profile.
|FormValidator |A validator that validates the UserForm.
|===
=== Staff

View file

@ -38,7 +38,9 @@ UserController -> RegistrationForm : getPassword()
UserController <-- RegistrationForm : String
UserController -> RegistrationForm : getAddress()
UserController <-- RegistrationForm : String
UserController -> UserManagement : createCustomer(String,String,String)
UserController -> RegistrationForm : getFullName()
UserController <-- RegistrationForm : String
UserController -> UserManagement : createCustomer(String,String,String,String)
activate UserManagement
UserManagement -> UserRepository : "save(User:customer)"
activate User_customer
@ -55,7 +57,7 @@ deactivate Spring
== Disable Customer by Customer ==
Spring -> UserController : disableUser()
Spring -> UserController : disableUser(UserAccount:userAccount)
activate User_customer
activate Spring
activate UserController
@ -88,9 +90,15 @@ UserManagement -> UserRepository : findAll()
activate UserRepository
UserManagement <-- UserRepository : Streamble
deactivate UserRepository
UserController <-- UserManagement : Optional<User:user>
UserController <-- UserManagement : Optional<User>
UserController -> User : getUsername()
UserController <-- User : String
UserController -> User : getFullName()
UserController <-- User : String
UserController -> User : getAddress()
UserController <-- User : String
deactivate UserManagement
UserController -> Spring : model.addAttribute("user",User:user)
UserController -> Spring : model.addAttribute("profileForm",ProfileForm)
UserController <-- Spring : Model
Spring <-- UserController : "profile"
deactivate Spring
@ -99,7 +107,7 @@ deactivate User
== Edit Profile ==
Spring -> UserController : editProfile(UserAccount:LoggedIn,String:password,String:address,String:username)
Spring -> UserController : editProfile(UserAccount:LoggedIn,ProfileForm:form,Erros:result,Model:model)
activate User
activate Spring
activate UserController
@ -111,23 +119,20 @@ UserManagement <-- UserRepository : Streamble
deactivate UserRepository
UserController <-- UserManagement : Optional<User:user>
deactivate UserManagement
UserController -> User : [!String:username.isBlank()] setUsername(String:username)
UserController -> User : setUsername(String:username)
UserController <-- User : boolean
UserController -> User : [!String:address.isBlank()] setAddress(String:address)
UserController -> User : setAddress(String:address)
UserController <-- User : boolean
UserController -> User : [!String:password.isBlank()] setPassword(String:password)
UserController -> User : [!form.getUsername().equals(user.getUsername())] setPassword(String:password)
UserController <-- User : boolean
UserController -> UserManagement : save(User:LoggedIn)
UserManagement -> UserRepository : "save(User:LoggedIn)"
activate UserManagement
UserManagement -> UserRepository : save(User:LoggedIn)
activate UserRepository
UserManagement <-- UserRepository : User
deactivate UserRepository
UserController <-- UserManagement : User
activate UserManagement
UserController <-- UserManagement : User
deactivate UserManagement
Spring <-- UserController : [!username.isBlank()] "redirect:/profile"
Spring <-- UserController : "redirect:/profile"
deactivate Spring
deactivate UserController
deactivate User

Binary file not shown.

View file

@ -9,55 +9,68 @@ package Salespoint {
class UserAccount
class AbstractAggregateRoot
class Role
class Errors
}
package Spring {
class CrudRepository
class Streamable
class WebDataBinder
class Errors
interface Validator
}
package catering.users {
class User {
- address : String
+ User(userAccount : UserAccount, address : String)
- fullName : String
+ User(userAccount : UserAccount, address : String, fullName : String)
+ User()
+ getAddress() : String
+ setAddress(address:String) : String
+ getFullName() : String
+ setFullName(fullName:String) : String
+ getUsername() : String
+ setUsername(username:String) : boolean
+ getUserAccount() : UserAccount
+ getId() : UserIdentifier
+ isEnabled() : boolean
+ hasRole(role:String) : boolean
+ equals(obj:Object) : boolean
+ hashCode() : int
}
User ..> Role
User ..> WebDataBinder
User --> UserAccount : "-userAccount"
User --|> AbstractAggregateRoot : <<extends>>
class UserController {
+ UserController(userManagement: UserManagement)
- initProfileBinder(binder : WebDataBinder) : void
- initRegistrationBinder(binder : WebDataBinder) : void
+ unauthorized() : String
+ register() : String
+ register(form:RegistrationForm,result:Erros) : String
+ register(form:RegistrationForm,result:Errors) : String
+ loginPage() : String
+ viewProfile(model:Model,userAccount:UserAccount) : String
+ editProfile(userAccount:UserAccount,username:String,password:String,address:String) : String
+ editProfile(userAccount:UserAccount,form:ProfileForm,result:Errors,model:Model) : String
+ disableUser(userAccount:UserAccount) : String
+ getCustomer(model:Model) : String
+ removeCustomer(user:User,model:Model) : String
+ removeCustomer(user:User) : String
+ editCustomer(user:User,model:Model) : String
+ updateCustomer(user:User,username:String,address:String,model:Model)
+ updateCustomer(user:User,username:String,address:String)
}
UserController --> UserManagement : "-userManagement"
UserController ..> User
UserController ..> UserAccount
UserController ..> Role
UserController ..> RegistrationForm
UserController ..> UserForm
UserController ..> FormValidator
UserController ..> WebDataBinder
class UserManagement {
+ UserManagement(users:UserRepository,userAccounts:UserAccountManagement)
+ createCustomer(name:String, password:String, address:String) : User
+ createAdmin(name:String, password:String, address:String) : User
+ createAdmin(name:String, password:String, address:String,fullName:String) : User
+ save(user:User) : User
+ getUsers() : UserRepository
+ disableUserAccount(userAccount:UserAccount) : void
@ -79,15 +92,22 @@ package catering.users {
UserRepository --|> CrudRepository : <<extends>>
UserRepository ..|> Streamable
UserRepository o-- User
class RegistrationForm {
class UserForm {
- username: String
- password: String
- address: String
+ RegistrationForm(username:String,password:String,address:String)
- password: String
+ UserForm(username:String,address:String,fullName:String,password:String)
+ getUsername() : String
+ getPassword() : String
+ getAddress() : String
+ validate(errors:Erros) : void
+ getFullName() : String
+ getPassword() : String
}
class FormValidator {
supports(c:Class<?>) : boolean
validate(o:Object,e:Errors) : void
}
FormValidator ..> Validator
FormValidator --> Errors
}
@enduml

BIN
src/main/asciidoc/models/design/user.svg (Stored with Git LFS)

Binary file not shown.

View file

@ -0,0 +1,25 @@
package catering.users;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
@Component
public class FormValidator<F extends UserForm> implements Validator {
@Override
public boolean supports(Class<?> c) {
return UserForm.class.isAssignableFrom(c);
}
@Override
@SuppressWarnings("unchecked")
public void validate(Object o, Errors e) {
F form = (F) o;
ValidationUtils.rejectIfEmptyOrWhitespace(e, "username", "username must not be empty");
ValidationUtils.rejectIfEmptyOrWhitespace(e, "address", "address must not be empty");
ValidationUtils.rejectIfEmptyOrWhitespace(e, "fullName", "fullName must not be empty");
form.validatePassword(e);
}
}

View file

@ -1,38 +0,0 @@
package catering.users;
import jakarta.validation.constraints.NotEmpty;
import java.util.Optional;
import org.springframework.validation.Errors;
public class ProfileForm {
private final @NotEmpty String username, address, fullName;
private final Optional<String> password;
public ProfileForm(String username, Optional<String> password, String address, String fullName) {
this.username = username;
this.address = address;
this.fullName = fullName;
this.password = password;
}
public String getUsername() {
return username;
}
public Optional<String> getPassword() {
return password;
}
public String getAddress() {
return address;
}
public String getFullName() {
return fullName;
}
public void validate(Errors errors) {
}
}

View file

@ -1,36 +0,0 @@
package catering.users;
import jakarta.validation.constraints.NotEmpty;
import org.springframework.validation.Errors;
public class RegistrationForm {
private final @NotEmpty String password, username, address, fullName;
public RegistrationForm(String username, String password, String address, String fullName) {
this.username = username;
this.address = address;
this.fullName = fullName;
this.password = password;
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public String getAddress() {
return address;
}
public String getFullName() {
return fullName;
}
public void validate(Errors errors) {
}
}

View file

@ -1,14 +1,17 @@
package catering.users;
import static catering.users.UserForm.RegistrationForm;
import static catering.users.UserForm.ProfileForm;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.salespointframework.useraccount.Role;
import org.salespointframework.useraccount.UserAccount;
import org.salespointframework.useraccount.web.LoggedIn;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
@ -24,13 +27,26 @@ public class UserController {
this.userManagement = userManagerment;
};
// the binder associates the form validator with the corresponding form
// as a result, it overrides every validator annotation
@InitBinder("profileForm")
private void initProfileBinder(WebDataBinder binder) {
binder.setValidator(new FormValidator<ProfileForm>());
}
@InitBinder("registrationForm")
private void initRegistrationBinder(WebDataBinder binder) {
binder.setValidator(new FormValidator<RegistrationForm>());
}
@GetMapping("/unauthorized")
String unauthorized(){
return "unauthorized";
}
@GetMapping("/register")
String register() {
String register(Model model) {
model.addAttribute("registrationForm", RegistrationForm.empty());
return "register";
}
@ -39,9 +55,6 @@ public class UserController {
if (result.hasErrors()){
return "register";
}
if (form.getPassword().chars().anyMatch(Character::isISOControl)) {
return "register";
}
userManagement.createCustomer(form.getUsername(),form.getAddress(),form.getPassword(),form.getFullName());
return "redirect:/login";
}
@ -58,14 +71,18 @@ public class UserController {
return "redirect:/";
}
User user = userManagement.getUserByAccount(userAccount).get();
model.addAttribute("user", user);
model.addAttribute("profileForm", new ProfileForm(user.getUsername(), user.getAddress(), user.getFullName(), ""));
return "profile";
}
@PostMapping("/profile")
@PreAuthorize("isAuthenticated()")
public String editProfile(@LoggedIn UserAccount userAccount, @Valid ProfileForm form) {
public String editProfile(@LoggedIn UserAccount userAccount, @Valid ProfileForm form, Errors result, Model model) {
String redirect = "redirect:/logout";
if (result.hasErrors()){
return "profile";
}
User user = userManagement.getUserByAccount(userAccount).get();
if (form.getUsername().equals(user.getUsername())) {
@ -74,10 +91,8 @@ public class UserController {
user.setUsername(form.getUsername());
user.setFullName(form.getFullName());
user.setAddress(form.getAddress());
if (!form.getPassword().get().isEmpty()) {
if (form.getPassword().get().chars().anyMatch(Character::isISOControl)) {
userManagement.setPassword(form.getPassword().get(), user.getUserAccount());
}
if (!form.getPassword().isEmpty()) {
userManagement.setPassword(form.getPassword(), user.getUserAccount());
}
userManagement.save(user);

View file

@ -0,0 +1,97 @@
package catering.users;
import org.springframework.validation.ValidationUtils;
import java.util.Optional;
import org.springframework.validation.Errors;
/**
* An abstract class to hold form input for {@link User} data.
*
* It can be extended to validate input for different use cases.
*/
public abstract class UserForm {
private final String username, address, fullName, password;
public UserForm(String username, String address, String fullName, String password) {
this.username = username;
this.address = address;
this.fullName = fullName;
this.password = password;
}
public String getUsername() {
return username;
}
public String getAddress() {
return address;
}
public String getFullName() {
return fullName;
}
public String getPassword() {
return password;
}
/**
* Template Method for validating the password.
*
* @param e {@link Errors} from {@link FormValidator}
*/
public void validatePassword(Errors e) {
validatePasswordGeneric(e);
Optional.ofNullable(e.getFieldValue("password")).map(s -> (String) s).ifPresent(s -> {
if (s.chars().anyMatch(Character::isISOControl)) {
e.rejectValue("password", "password must only contain printable characters");
}
});
}
/**
* Primitive Operation for validating the password depending on the subclass.
*
* @param e {@link Errors} from {@link FormValidator}
*/
public abstract void validatePasswordGeneric(Errors e);
/**
* An extension of {@link UserForm} to validate input during registration.
*
* It requires the password to be not empty.
*/
public static class RegistrationForm extends UserForm {
RegistrationForm(String username, String address, String fullName, String password) {
super(username, address, fullName, password);
}
public static RegistrationForm empty() {
return new UserForm.RegistrationForm("", "", "", "");
}
public void validatePasswordGeneric(Errors e) {
ValidationUtils.rejectIfEmptyOrWhitespace(e, "password", "password must not be empty");
}
}
/**
* An extension of {@link UserForm} to validate input during profile editing.
*
* The password can be empty.
*/
public static class ProfileForm extends UserForm {
ProfileForm(String username, String address, String fullName, String password) {
super(username, address, fullName, password);
}
public void validatePasswordGeneric(Errors e) {
if (e.getFieldValue("password") == null) {
e.rejectValue("password", "password must not be null");
}
}
}
}

View file

@ -56,4 +56,8 @@ public class UserManagement {
public Optional<User> getUserByAccount(UserAccount userAccount) {
return users.findAll().stream().filter(u -> u.getUserAccount().equals(userAccount)).findFirst();
}
public Optional<User> getUserByName(String username) {
return userAccounts.findByUsername(username).flatMap(ua -> getUserByAccount(ua));
}
}

View file

@ -8,32 +8,34 @@
<body>
<div layout:fragment="content">
<div class="mb-4">
<form th:action="@{/profile}" method="post">
<form th:object="${profileForm}" th:action="@{/profile}" method="post">
<h2>Authentifizierung</h2>
<div class="mb-3">
<label class="form-label" for="username">Nutzername</label>
<input class="form-control" name="username" th:value="${user.username}" type="text">
<input class="form-control" th:field="*{username}" th:errorclass="is-invalid" type="text" required>
<div th:if="${#fields.hasErrors('username')}" class="invalid-feedback">Ungültiger Nutzername</div>
</div>
<div class="mb-3">
<label class="form-label" for="password" th:text="Passwort"></label>
<input class="form-control" name="password" type="password">
<input class="form-control" th:field="*{password}" th:errorclass="is-invalid" type="password">
<div th:if="${#fields.hasErrors('password')}" class="invalid-feedback">Ungültiges Passwort</div>
</div>
<h2>Nutzerinformationen</h2>
<div class="mb-3">
<label class="form-label" for="fullName">Name</label>
<input class="form-control" name="fullName" th:value="${user.fullName}" type="text">
<input class="form-control" th:field="*{fullName}" th:errorclass="is-invalid" type="text" required>
<div th:if="${#fields.hasErrors('fullName')}" class="invalid-feedback">Ungültiger Name</div>
</div>
<div class="mb-3">
<label class="form-label" for="address">Adresse</label>
<textarea class="form-control" name="address" th:text="${user.address}" rows="3" required></textarea>
<textarea class="form-control" th:field="*{address}" th:errorclass="is-invalid" th:placeholder="*{address}" rows="3" required></textarea>
<div th:if="${#fields.hasErrors('address')}" class="invalid-feedback">Ungültige Addresse</div>
</div>
<button class="btn btn-primary" type="submit">Bearbeiten</button>
</form>
</div>
<div th:if="${user.hasRole('CUSTOMER')}" class="card">
<div sec:authorize="hasRole('CUSTOMER')" class="card">
<div class="card-header text-danger">Danger Zone</div>
<div class="card-body">
<a th:href="@{/profile/disable}">

View file

@ -5,22 +5,26 @@
layout:decorate="~{layout.html(title='Registrierung')}">
<div layout:fragment="content">
<form method="post" th:action="@{/register}">
<form th:object="${registrationForm}" method="post" th:action="@{/register}">
<div class="mb-3">
<label class="form-label" for="username">Nutzername</label>
<input class="form-control" name="username" type="text" required>
<input class="form-control" th:field="*{username}" th:errorclass="is-invalid" type="text" required>
<div th:if="${#fields.hasErrors('username')}" class="invalid-feedback">Ungültiger Nutzername</div>
</div>
<div class="mb-3">
<label class="form-label" for="password">Passwort</label>
<input class="form-control" name="password" type="password" required>
<input class="form-control" th:field="*{password}" th:errorclass="is-invalid" type="password" required>
<div th:if="${#fields.hasErrors('password')}" class="invalid-feedback">Ungültiges Passwort</div>
</div>
<div class="mb-3">
<label class="form-label" for="fullName">Name</label>
<input class="form-control" name="fullName" type="text" required>
<input class="form-control" th:field="*{fullName}" th:errorclass="is-invalid" type="text" required>
<div th:if="${#fields.hasErrors('fullName')}" class="invalid-feedback">Ungültiger Name</div>
</div>
<div class="mb-3">
<label class="form-label" for="address">Adresse</label>
<textarea class="form-control" name="address" rows="3" required></textarea>
<textarea class="form-control" th:field="*{address}" th:errorclass="is-invalid" rows="3" required></textarea>
<div th:if="${#fields.hasErrors('address')}" class="invalid-feedback">Ungültige Addresse</div>
</div>
<button type="submit" class="btn btn-primary">Registrieren</button>
</form>