Commit 1c421be9 authored by Ondřej Pavlica's avatar Ondřej Pavlica
Browse files

Merge branch 'feature/balga/userRoles' into 'main'

Feature/balga/user roles

See merge request !36
parents 2ce9437a 21c1f6c3
Pipeline #139043 passed with stages
in 2 minutes and 42 seconds
......@@ -11,6 +11,7 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author Samuel Dudík
......@@ -59,4 +60,8 @@ public class UserDto extends PersistentDtoBase {
public boolean hasRole(String roleName) {
return roles.stream().filter(x -> x.name().equals(roleName)).findFirst().isPresent();
}
public List<UserRoleDto> getRoleDtos() {
return roles.stream().map(x -> UserRoleDto.builder().userId(getId()).role(x).build()).collect(Collectors.toList());
}
}
......@@ -16,4 +16,11 @@ public interface UserRoleService extends PersistenceService<UserRoleDto> {
* @return List of user roles.
*/
List<UserRoleDto> getAll(long userId);
/**
* Set the user's roles.
* @param userId ID of the given user.
* @param roles The roles
*/
void setRoles(long userId, List<UserRoleDto> roles);
}
......@@ -29,4 +29,19 @@ public class UserRoleServiceImpl extends PersistenceServiceImpl<UserRoleDto, Use
var entities = ((UserRoleDao)getDao()).getAll(userId);
return entities.stream().map(this::mapToDto).collect(Collectors.toList());
}
@Override
public void setRoles(long userId, List<UserRoleDto> roles) {
var currentRoles = getAll(userId);
var toDelete = currentRoles.stream().filter(x -> !roles.contains(x)).collect(Collectors.toList());
var toAdd = roles.stream().filter(x -> !currentRoles.contains(x)).collect(Collectors.toList());
for (var role : toDelete) {
delete(role);
}
for (var role : toAdd) {
create(role);
}
}
}
......@@ -22,8 +22,11 @@ import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.List;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@ExtendWith(MockitoExtension.class)
public class UserRoleTests extends PersistenceServiceTestBase<UserRoleDto, UserRole> {
......@@ -47,6 +50,20 @@ public class UserRoleTests extends PersistenceServiceTestBase<UserRoleDto, UserR
Mockito.verify(userRoleDao, Mockito.times(1)).getAll(1);
}
@Test
public void testSetRoles() {
getPersistenceService();
var entityList = List.of(getTestEntity());
Mockito.when(userRoleDao.getAll(1L)).thenReturn(entityList);
var newRoles = List.of((UserRoleDto) UserRoleDto.builder()
.role(UserRoleType.ADMIN)
.userId(1)
.build());
userRoleService.setRoles(1, newRoles);
Mockito.verify(userRoleDao, Mockito.times(1)).delete(any(UserRole.class));
Mockito.verify(userRoleDao, Mockito.times(1)).create(any(UserRole.class));
}
@Override
protected UserRoleDto getTestDto() {
var dto = new UserRoleDto();
......
package cz.muni.fi.pa165.winery.webapp.controllers;
import cz.muni.fi.pa165.winery.dto.user.UserDto;
import cz.muni.fi.pa165.winery.services.user.UserRoleService;
import cz.muni.fi.pa165.winery.services.user.UserService;
import cz.muni.fi.pa165.winery.webapp.models.user.UserInsertViewModel;
import cz.muni.fi.pa165.winery.webapp.models.user.UserListingViewModel;
import cz.muni.fi.pa165.winery.webapp.models.user.UserUpsertViewModel;
import cz.muni.fi.pa165.winery.webapp.models.user.UserUpdateViewModel;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
......@@ -20,9 +23,13 @@ import java.util.ArrayList;
@Controller
public class UserController extends ControllerBase {
private final UserService userService;
private final UserRoleService userRoleService;
private final PasswordEncoder encoder;
public UserController(UserService userService) {
public UserController(UserService userService, UserRoleService userRoleService, PasswordEncoder encoder) {
this.userService = userService;
this.userRoleService = userRoleService;
this.encoder = encoder;
}
@GetMapping("")
......@@ -46,7 +53,7 @@ public class UserController extends ControllerBase {
return notFound();
}
var viewModel = new UserUpsertViewModel(user);
var viewModel = new UserUpdateViewModel(user);
initializeViewModel(viewModel);
return view(viewModel);
......@@ -54,13 +61,17 @@ public class UserController extends ControllerBase {
@PostMapping("/edit")
@Secured("ROLE_ADMIN")
public ModelAndView edit(@Valid @ModelAttribute("model") UserUpsertViewModel viewModel, BindingResult bindingResult) {
public ModelAndView edit(@Valid @ModelAttribute("model") UserUpdateViewModel viewModel, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return view(bindingResult.getModel());
}
var dto = viewModel.toDto();
var dto = viewModel.toDto(encoder);
if (viewModel.getPassword().isEmpty()) {
dto.setPasswordHash(userService.get(dto.getId()).getPasswordHash());
}
userService.update(dto);
userRoleService.setRoles(dto.getId(), dto.getRoleDtos());
initializeViewModel(viewModel);
viewModel.setSuccess(true);
......@@ -71,7 +82,7 @@ public class UserController extends ControllerBase {
@GetMapping("/add")
@Secured("ROLE_ADMIN")
public ModelAndView add() {
var viewModel = new UserUpsertViewModel();
var viewModel = new UserInsertViewModel();
initializeViewModel(viewModel);
return view(viewModel);
......@@ -79,13 +90,14 @@ public class UserController extends ControllerBase {
@PostMapping("/add")
@Secured("ROLE_ADMIN")
public ModelAndView add(@Valid @ModelAttribute("model") UserUpsertViewModel viewModel, BindingResult bindingResult) {
public ModelAndView add(@Valid @ModelAttribute("model") UserInsertViewModel viewModel, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
return view(bindingResult.getModel());
}
var dto = viewModel.toDto();
var dto = viewModel.toDto(encoder);
userService.create(dto);
userRoleService.setRoles(dto.getId(), dto.getRoleDtos());
initializeViewModel(viewModel);
viewModel.setSuccess(true);
......
......@@ -7,12 +7,11 @@ import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.modelmapper.ModelMapper;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
/**
* @author Jakub Balga
......@@ -20,16 +19,13 @@ import java.util.List;
@Getter
@Setter
@NoArgsConstructor
public class UserUpsertViewModel extends ViewModelBase {
public UserUpsertViewModel(UserDto dto) {
var mapper = new ModelMapper();
mapper.map(dto, this);
}
public class UserInsertViewModel extends ViewModelBase {
private long id;
private boolean success;
private boolean regular;
private boolean admin;
/**
* User's first name (forename).
......@@ -51,20 +47,23 @@ public class UserUpsertViewModel extends ViewModelBase {
private String email;
/**
* The hash of the user submitted password
* The user submitted password
*
* Used for authentication
* Not stored directly
*/
@NotBlank
private String passwordHash;
/**
* Roles assigned to this user
*/
private List<UserRoleType> roles = new ArrayList<>();
private String password;
public UserDto toDto() {
public UserDto toDto(PasswordEncoder encoder) {
var mapper = new ModelMapper();
return mapper.map(this, UserDto.class);
var dto = mapper.map(this, UserDto.class);
if (admin) {
dto.getRoles().add(UserRoleType.ADMIN);
}
if (regular) {
dto.getRoles().add(UserRoleType.USER);
}
dto.setPasswordHash(encoder.encode(password));
return dto;
}
}
package cz.muni.fi.pa165.winery.webapp.models.user;
import cz.muni.fi.pa165.winery.dto.user.UserDto;
import cz.muni.fi.pa165.winery.enums.UserRoleType;
import cz.muni.fi.pa165.winery.webapp.models.ViewModelBase;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.security.crypto.password.PasswordEncoder;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
/**
* @author Jakub Balga
*/
@Getter
@Setter
@NoArgsConstructor
public class UserUpdateViewModel extends ViewModelBase {
public UserUpdateViewModel(UserDto dto) {
var mapper = new ModelMapper();
mapper.getConfiguration().setAmbiguityIgnored(true);
mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
mapper.map(dto, this);
if (dto.hasRole(UserRoleType.ADMIN)) {
admin = true;
}
if (dto.hasRole(UserRoleType.USER)) {
regular = true;
}
}
private long id;
private boolean success;
private boolean regular;
private boolean admin;
/**
* User's first name (forename).
*/
@NotNull(message = "Cannot be empty")
private String firstName;
/**
* User's last name (surname)
*/
@NotNull(message = "Cannot be empty")
private String lastName;
/**
* User's email address
*/
@NotNull(message = "Cannot be empty")
@Email
private String email;
/**
* The user submitted password
*
* Not stored directly
*/
private String password;
public UserDto toDto(PasswordEncoder encoder) {
var mapper = new ModelMapper();
var dto = mapper.map(this, UserDto.class);
if (admin) {
dto.getRoles().add(UserRoleType.ADMIN);
}
if (regular) {
dto.getRoles().add(UserRoleType.USER);
}
if (password != null && !password.isEmpty()) {
dto.setPasswordHash(encoder.encode(password));
}
return dto;
}
}
......@@ -35,9 +35,17 @@
<p class="text-danger" th:each="error : ${#fields.errors('email')}" th:text="${error}"></p>
</div>
<div class="form-group">
<label th:for="*{passwordHash}" th:value="*{passwordHash}">Password hash</label>
<input id="passwordHash" class="form-control" type="text" th:field="*{passwordHash}">
<p class="text-danger" th:each="error : ${#fields.errors('passwordHash')}" th:text="${error}"></p>
<label th:for="*{password}" th:value="*{password}">Password</label>
<input id="password" class="form-control" type="password" placeholder="enter new password" th:field="*{password}">
<p class="text-danger" th:each="error : ${#fields.errors('password')}" th:text="${error}"></p>
</div>
<div class="form-group">
<label th:for="*{regular}" th:value="*{regular}">Regular</label>
<input id="regular" type="checkbox" th:field="*{regular}">
</div>
<div class="form-group">
<label th:for="*{admin}" th:value="*{admin}">Admin</label>
<input id="admin" type="checkbox" th:field="*{admin}">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
......
......@@ -20,8 +20,6 @@
<form th:action="@{/user/edit}" th:object="${model}" method="post">
<input type="hidden" th:field="*{id}">
<input type="hidden" th:field="*{roles}">
<div class="form-group">
<label th:for="*{firstName}" th:value="*{firstName}">First name</label>
<input id="firstName" class="form-control" type="text" th:field="*{firstName}">
......@@ -38,9 +36,17 @@
<p class="text-danger" th:each="error : ${#fields.errors('email')}" th:text="${error}"></p>
</div>
<div class="form-group">
<label th:for="*{passwordHash}" th:value="*{passwordHash}">Password hash</label>
<input id="passwordHash" class="form-control" type="text" th:field="*{passwordHash}">
<p class="text-danger" th:each="error : ${#fields.errors('passwordHash')}" th:text="${error}"></p>
<label th:for="*{password}" th:value="*{password}">Password</label>
<input id="password" class="form-control" type="password" placeholder="enter new password" th:field="*{password}">
<p class="text-danger" th:each="error : ${#fields.errors('password')}" th:text="${error}"></p>
</div>
<div class="form-group">
<label th:for="*{regular}" th:value="*{regular}">Regular</label>
<input id="regular" type="checkbox" th:field="*{regular}">
</div>
<div class="form-group">
<label th:for="*{admin}" th:value="*{admin}">Admin</label>
<input id="admin" type="checkbox" th:field="*{admin}">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
......
......@@ -19,6 +19,7 @@
<th scope="col">First name</th>
<th scope="col">Last name</th>
<th scope="col">Email</th>
<th scope="col">Roles</th>
<th scope="col">Actions</th>
</tr>
</thead>
......@@ -27,6 +28,7 @@
<td th:text="${user.firstName}">First name</td>
<td th:text="${user.lastName}">Last name</td>
<td th:text="${user.email}">Email</td>
<td th:text="${user.roles}">Roles</td>
<td> <a th:href="@{/user/edit(id=${user.id})}">Edit</a> | <a href="#" th:attr="data-id=${user.id}" onclick="deleteAndReload(this)">Delete</a></td>
</tr>
</tbody>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment