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. |UserManagement |A class that manages the UserRepository.
|UserRepository |An extension of 'CrudRepository' to store Users. |UserRepository |An extension of 'CrudRepository' to store Users.
|User |A class that allows a person to associate system data with themselves. |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 === Staff

View file

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

Binary file not shown.

View file

@ -9,55 +9,68 @@ package Salespoint {
class UserAccount class UserAccount
class AbstractAggregateRoot class AbstractAggregateRoot
class Role class Role
class Errors
} }
package Spring { package Spring {
class CrudRepository class CrudRepository
class Streamable class Streamable
class WebDataBinder
class Errors
interface Validator
} }
package catering.users { package catering.users {
class User { class User {
- address : String - address : String
+ User(userAccount : UserAccount, address : String) - fullName : String
+ User(userAccount : UserAccount, address : String, fullName : String)
+ User() + User()
+ getAddress() : String + getAddress() : String
+ setAddress(address:String) : String + setAddress(address:String) : String
+ getFullName() : String
+ setFullName(fullName:String) : String
+ getUsername() : String + getUsername() : String
+ setUsername(username:String) : boolean + setUsername(username:String) : boolean
+ getUserAccount() : UserAccount + getUserAccount() : UserAccount
+ getId() : UserIdentifier + getId() : UserIdentifier
+ isEnabled() : boolean + isEnabled() : boolean
+ hasRole(role:String) : boolean + hasRole(role:String) : boolean
+ equals(obj:Object) : boolean
+ hashCode() : int
} }
User ..> Role User ..> WebDataBinder
User --> UserAccount : "-userAccount" User --> UserAccount : "-userAccount"
User --|> AbstractAggregateRoot : <<extends>> User --|> AbstractAggregateRoot : <<extends>>
class UserController { class UserController {
+ UserController(userManagement: UserManagement) + UserController(userManagement: UserManagement)
- initProfileBinder(binder : WebDataBinder) : void
- initRegistrationBinder(binder : WebDataBinder) : void
+ unauthorized() : String + unauthorized() : String
+ register() : String + register() : String
+ register(form:RegistrationForm,result:Erros) : String + register(form:RegistrationForm,result:Errors) : String
+ loginPage() : String + loginPage() : String
+ viewProfile(model:Model,userAccount:UserAccount) : 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 + disableUser(userAccount:UserAccount) : String
+ getCustomer(model:Model) : String + getCustomer(model:Model) : String
+ removeCustomer(user:User,model:Model) : String + removeCustomer(user:User) : String
+ editCustomer(user:User,model:Model) : 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 --> UserManagement : "-userManagement"
UserController ..> User UserController ..> User
UserController ..> UserAccount UserController ..> UserAccount
UserController ..> Role UserController ..> Role
UserController ..> RegistrationForm UserController ..> UserForm
UserController ..> FormValidator
UserController ..> WebDataBinder
class UserManagement { class UserManagement {
+ UserManagement(users:UserRepository,userAccounts:UserAccountManagement) + UserManagement(users:UserRepository,userAccounts:UserAccountManagement)
+ createCustomer(name:String, password:String, address:String) : User + 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 + save(user:User) : User
+ getUsers() : UserRepository + getUsers() : UserRepository
+ disableUserAccount(userAccount:UserAccount) : void + disableUserAccount(userAccount:UserAccount) : void
@ -79,15 +92,22 @@ package catering.users {
UserRepository --|> CrudRepository : <<extends>> UserRepository --|> CrudRepository : <<extends>>
UserRepository ..|> Streamable UserRepository ..|> Streamable
UserRepository o-- User UserRepository o-- User
class RegistrationForm { class UserForm {
- username: String - username: String
- password: String - password: String
- address: String - address: String
+ RegistrationForm(username:String,password:String,address:String) - password: String
+ UserForm(username:String,address:String,fullName:String,password:String)
+ getUsername() : String + getUsername() : String
+ getPassword() : String
+ getAddress() : 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 @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; package catering.users;
import static catering.users.UserForm.RegistrationForm;
import static catering.users.UserForm.ProfileForm;
import org.springframework.ui.Model; import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.salespointframework.useraccount.Role; import org.salespointframework.useraccount.Role;
import org.salespointframework.useraccount.UserAccount; import org.salespointframework.useraccount.UserAccount;
import org.salespointframework.useraccount.web.LoggedIn; import org.salespointframework.useraccount.web.LoggedIn;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller; 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.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
@ -24,13 +27,26 @@ public class UserController {
this.userManagement = userManagerment; 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") @GetMapping("/unauthorized")
String unauthorized(){ String unauthorized(){
return "unauthorized"; return "unauthorized";
} }
@GetMapping("/register") @GetMapping("/register")
String register() { String register(Model model) {
model.addAttribute("registrationForm", RegistrationForm.empty());
return "register"; return "register";
} }
@ -39,9 +55,6 @@ public class UserController {
if (result.hasErrors()){ if (result.hasErrors()){
return "register"; return "register";
} }
if (form.getPassword().chars().anyMatch(Character::isISOControl)) {
return "register";
}
userManagement.createCustomer(form.getUsername(),form.getAddress(),form.getPassword(),form.getFullName()); userManagement.createCustomer(form.getUsername(),form.getAddress(),form.getPassword(),form.getFullName());
return "redirect:/login"; return "redirect:/login";
} }
@ -58,14 +71,18 @@ public class UserController {
return "redirect:/"; return "redirect:/";
} }
User user = userManagement.getUserByAccount(userAccount).get(); User user = userManagement.getUserByAccount(userAccount).get();
model.addAttribute("user", user); model.addAttribute("profileForm", new ProfileForm(user.getUsername(), user.getAddress(), user.getFullName(), ""));
return "profile"; return "profile";
} }
@PostMapping("/profile") @PostMapping("/profile")
@PreAuthorize("isAuthenticated()") @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"; String redirect = "redirect:/logout";
if (result.hasErrors()){
return "profile";
}
User user = userManagement.getUserByAccount(userAccount).get(); User user = userManagement.getUserByAccount(userAccount).get();
if (form.getUsername().equals(user.getUsername())) { if (form.getUsername().equals(user.getUsername())) {
@ -74,10 +91,8 @@ public class UserController {
user.setUsername(form.getUsername()); user.setUsername(form.getUsername());
user.setFullName(form.getFullName()); user.setFullName(form.getFullName());
user.setAddress(form.getAddress()); user.setAddress(form.getAddress());
if (!form.getPassword().get().isEmpty()) { if (!form.getPassword().isEmpty()) {
if (form.getPassword().get().chars().anyMatch(Character::isISOControl)) { userManagement.setPassword(form.getPassword(), user.getUserAccount());
userManagement.setPassword(form.getPassword().get(), user.getUserAccount());
}
} }
userManagement.save(user); 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) { public Optional<User> getUserByAccount(UserAccount userAccount) {
return users.findAll().stream().filter(u -> u.getUserAccount().equals(userAccount)).findFirst(); 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> <body>
<div layout:fragment="content"> <div layout:fragment="content">
<div class="mb-4"> <div class="mb-4">
<form th:action="@{/profile}" method="post"> <form th:object="${profileForm}" th:action="@{/profile}" method="post">
<h2>Authentifizierung</h2> <h2>Authentifizierung</h2>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="username">Nutzername</label> <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>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="password" th:text="Passwort"></label> <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> </div>
<h2>Nutzerinformationen</h2> <h2>Nutzerinformationen</h2>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="fullName">Name</label> <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>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="address">Adresse</label> <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> </div>
<button class="btn btn-primary" type="submit">Bearbeiten</button> <button class="btn btn-primary" type="submit">Bearbeiten</button>
</form> </form>
</div> </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-header text-danger">Danger Zone</div>
<div class="card-body"> <div class="card-body">
<a th:href="@{/profile/disable}"> <a th:href="@{/profile/disable}">

View file

@ -5,22 +5,26 @@
layout:decorate="~{layout.html(title='Registrierung')}"> layout:decorate="~{layout.html(title='Registrierung')}">
<div layout:fragment="content"> <div layout:fragment="content">
<form method="post" th:action="@{/register}"> <form th:object="${registrationForm}" method="post" th:action="@{/register}">
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="username">Nutzername</label> <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>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="password">Passwort</label> <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>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="fullName">Name</label> <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>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="address">Adresse</label> <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> </div>
<button type="submit" class="btn btn-primary">Registrieren</button> <button type="submit" class="btn btn-primary">Registrieren</button>
</form> </form>