I have a form for editing a user, the user has roles, which are a list of objects of type Authority, I want to be able to use checkboxes (optional) to set the roles that the user will have, but I have no idea how to implement the form in thymeleaf and how to pass the user object with the given roles to the controller.
It's my user
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private Long id;
@Column(name="username", nullable = false, unique = true)
@NotBlank @Size(min=5, message = "Не менeе 5 знаков")
private String username;
@NotBlank @Size(min=5, message = "Не менeе 5 знаков")
@Column(name = "password")
private String password;
@Column(name = "enabled")
private boolean enabled;
@Column(name = "name")
private String name;
@Column(name = "surname")
private String surname;
@Column(name = "email", nullable = false, unique = true)
private String email;
@ManyToMany(cascade = {
CascadeType.PERSIST,
CascadeType.MERGE,
CascadeType.DETACH,
CascadeType.REFRESH
}, fetch = FetchType.EAGER)
@JoinTable(
name = "users_authorities",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "authority_id")
)
private Set<Authority> authorities = new HashSet<>();
public User(String username, String password, boolean enabled,
String name, String surname, String email) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.name = name;
this.surname = surname;
this.email = email;
}
public void addAuthority(Authority authority) {
this.authorities.add(authority);
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
}
It's my edit user form contoller
@GetMapping("/users/{id}/edit")
public String editUser(@PathVariable("id") Long id, Model model) {
model.addAttribute("user", userService.findById(id));
model.addAttribute("allAuthorities", authorityService.findAll());
return "users/edit-user";
}
It's my edit user form view
<body>
<form th:method="PUT" th:action="@{/admin/users/{id}(id=${user.getId()})}" th:object="${user}">
<input type="hidden" th:field="*{id}" id="id">
<label for="username">Username: </label>
<input type="text" th:field="*{username}" id="username" placeholder="username">
<br><br>
<div sec:authorize="hasRole('ROLE_ADMIN')">
<label for="enabled">Enabled </label>
<input type="checkbox" name="enabled" th:field="*{enabled}" id="enabled">
<br><br>
</div>
<label for="name">Name: </label>
<input type="text" th:field="*{name}" id="name" placeholder="Name">
<br><br>
<label for="surname">Surname: </label>
<input type="text" th:field="*{surname}" id="surname" placeholder="Surname">
<br><br>
<label for="email">Email: </label>
<input type="text" th:field="*{email}" id="email" placeholder="Email">
<br><br>
<div th:each="auth:${allAuthorities}">
<label>
<span th:text="${auth.authority}"></span>
<input type="checkbox" name="authorities" th:checked="${user.authorities.contains(auth)}">
</label>
</div>
<input type="submit" value="Edit">
</form>
</body>
It's put contoller, it getting the data from my form
@PutMapping("/users/{id}")
public String editUser(@PathVariable("id") Long id,
@ModelAttribute("user") User user,
@RequestParam("authorities") List<Authority> authorities) {
user.setId(id);
userService.update(user);
return "redirect:/admin/users";
}
And it's my Authority class if you need
@Entity
@Table(name = "authorities")
@Data
@NoArgsConstructor
public class Authority implements GrantedAuthority {
@Id
private Long id;
@Column(name = "authority")
private String authority;
@Transient
@ManyToMany(mappedBy = "authorities")
private Set<User> users;
public Authority(Long id, String authority) {
this.id = id;
this.authority = authority;
}
}
I'm try to pass the list of roles separately from the user object, but this also doesn't work and gives a bad request error.
To solve the problem, I added a new checked field to Entity authority
@Entity
@Table(name = "authorities")
@Data
@NoArgsConstructor
public class Authority implements GrantedAuthority {
@Id
private Long id;
@Column(name = "authority", nullable = false, unique = true)
private String authority;
@Transient
private boolean checked;
public Authority(Long id, String authority) {
this.id = id;
this.authority = authority;
}
}
Before sending the view to it, I changed the authorites field of the user object.
@GetMapping("/users/{id}/edit")
public String editUser(@PathVariable("id") Long id, Model model) {
User user = userService.findById(id);
List<Authority> allAuthorities = authorityService.findAll();
for(Authority auth : allAuthorities) {
if(user.getAuthorities().contains(auth)) {
auth.setChecked(true);
}
}
user.setAuthorities(allAuthorities);
model.addAttribute("user", user);
return "users/edit-user";
}
<form th:method="PUT" th:action="@{/admin/users/{id}(id=${user.getId()})}" th:object="${user}">
<label for="username">Username: </label>
<input type="text" th:field="*{username}" id="username" placeholder="username">
<br><br>
<div sec:authorize="hasRole('ROLE_ADMIN')">
<label for="enabled">Enabled </label>
<input type="checkbox" name="enabled" th:field="*{enabled}" id="enabled">
<br><br>
</div>
<label for="name">Name: </label>
<input type="text" th:field="*{name}" id="name" placeholder="Name">
<br><br>
<label for="surname">Surname: </label>
<input type="text" th:field="*{surname}" id="surname" placeholder="Surname">
<br><br>
<label for="email">Email: </label>
<input type="text" th:field="*{email}" id="email" placeholder="Email">
<br><br>
<div th:each="auth, itemStat: ${user.authorities}">
<label>
<span th:text="${auth.authority}"></span>
<input type="hidden"
th:field="*{authorities[__${itemStat.index}__].id}">
<input type="hidden"
th:field="*{authorities[__${itemStat.index}__].authority}">
<input type="checkbox" th:checked="${auth.checked}"
th:field="*{authorities[__${itemStat.index}__].checked}">
</label>
</div>
<input type="submit" value="Edit">
</form>
My put controller looks like this
@PutMapping("/users/{id}")
public String editUser(@PathVariable("id") Long id,
@ModelAttribute("user") User user) {
List<Authority> userAuthorities = user.getAuthorities();
userAuthorities.removeIf(auth -> !auth.isChecked());
user.setId(id);
userService.update(user);
return "redirect:/admin/users";
}
@Override
@Transactional
public User save(User user) {
Optional<User> userFromDB = userRepository.findByUsername(user.getUsername());
Optional<Authority> userRole;
if(userFromDB.isPresent()) {
return null;
}
userRole = authorityRepository.findByAuthority("ROLE_USER");
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
userRole.ifPresent(user::addAuthority);
return userRepository.save(user);
}
@Override
@Transactional
public User update(User user) {
Optional<User> userFromDB = userRepository.findById(user.getId());
if(userFromDB.isPresent()){
//Adding the password for successfully update user in DB
user.setPassword(userFromDB.get().getPassword());
return userRepository.save(user);
}
return null;
}
This may not be the most elegant solution but I haven't found another one yet