From 79d419d366581fa23f5ced5b63286197a7e5adb7 Mon Sep 17 00:00:00 2001 From: Stefan Haun Date: Sat, 20 Jul 2024 16:41:51 +0200 Subject: [PATCH 1/7] Add a System Role enum for fixed roles --- .../gartenplus/auth/role/SystemRole.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 src/main/java/com/penguineering/gartenplus/auth/role/SystemRole.java diff --git a/src/main/java/com/penguineering/gartenplus/auth/role/SystemRole.java b/src/main/java/com/penguineering/gartenplus/auth/role/SystemRole.java new file mode 100644 index 0000000..eccd84b --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/auth/role/SystemRole.java @@ -0,0 +1,18 @@ +package com.penguineering.gartenplus.auth.role; + +import lombok.Getter; + +@Getter +public enum SystemRole { + ADMINISTRATOR("administrator", "Administrator"), + MEMBER("member", "Gartenfreund"), + FRIEND("friend", "Gartenfreund-Freund"); + + private final String handle; + private final String displayName; + + SystemRole(String handle, String displayName) { + this.handle = handle; + this.displayName = displayName; + } +} From f9f52c7b08028689ccfcac3fedcc76ebe504d823 Mon Sep 17 00:00:00 2001 From: Stefan Haun Date: Sat, 20 Jul 2024 16:42:04 +0200 Subject: [PATCH 2/7] Add the role mapping entity --- .../gartenplus/auth/role/RoleMapping.java | 29 ++++++++++++++++ .../gartenplus/auth/role/RoleMappingKey.java | 34 +++++++++++++++++++ .../auth/role/RoleMappingRepository.java | 10 ++++++ 3 files changed, 73 insertions(+) create mode 100644 src/main/java/com/penguineering/gartenplus/auth/role/RoleMapping.java create mode 100644 src/main/java/com/penguineering/gartenplus/auth/role/RoleMappingKey.java create mode 100644 src/main/java/com/penguineering/gartenplus/auth/role/RoleMappingRepository.java diff --git a/src/main/java/com/penguineering/gartenplus/auth/role/RoleMapping.java b/src/main/java/com/penguineering/gartenplus/auth/role/RoleMapping.java new file mode 100644 index 0000000..63b42ef --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/auth/role/RoleMapping.java @@ -0,0 +1,29 @@ +package com.penguineering.gartenplus.auth.role; + +import jakarta.persistence.*; +import lombok.*; + +import java.util.UUID; + +@Entity +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@EqualsAndHashCode(of = {"roleHandle", "userId"}) +@IdClass(RoleMappingKey.class) +@Table(name = "role_mapping") +public class RoleMapping { + @Id + @Column(name = "role_handle") + private String roleHandle; + + @Id + @Column(name = "user_id") + private UUID userId; + + @Override + public String toString() { + return roleHandle + "/" + userId; + } +} diff --git a/src/main/java/com/penguineering/gartenplus/auth/role/RoleMappingKey.java b/src/main/java/com/penguineering/gartenplus/auth/role/RoleMappingKey.java new file mode 100644 index 0000000..de9e64c --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/auth/role/RoleMappingKey.java @@ -0,0 +1,34 @@ +package com.penguineering.gartenplus.auth.role; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.springframework.web.server.WebSession; + +import java.io.Serializable; +import java.util.Optional; +import java.util.UUID; + +@Getter +@Setter +@NoArgsConstructor +@EqualsAndHashCode +public class RoleMappingKey implements Serializable { + private String roleHandle; + private UUID userId; + + public static RoleMappingKey of(String roleHandle, UUID userId) { + return new RoleMappingKey(roleHandle, userId); + } + + public RoleMappingKey(String roleHandle, UUID userId) { + this.roleHandle = roleHandle; + this.userId = userId; + } + + @Override + public String toString() { + return roleHandle + "/" + userId; + } +} diff --git a/src/main/java/com/penguineering/gartenplus/auth/role/RoleMappingRepository.java b/src/main/java/com/penguineering/gartenplus/auth/role/RoleMappingRepository.java new file mode 100644 index 0000000..a85f155 --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/auth/role/RoleMappingRepository.java @@ -0,0 +1,10 @@ +package com.penguineering.gartenplus.auth.role; + +import org.springframework.data.repository.CrudRepository; + +import java.util.List; +import java.util.UUID; + +public interface RoleMappingRepository extends CrudRepository { + List findByUserId(UUID userId); +} From b9f138ce57323cbc6669aae551acb78f85acbe3d Mon Sep 17 00:00:00 2001 From: Stefan Haun Date: Sat, 20 Jul 2024 16:42:17 +0200 Subject: [PATCH 3/7] Add a service for handling system roles --- .../auth/role/SystemRoleService.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/main/java/com/penguineering/gartenplus/auth/role/SystemRoleService.java diff --git a/src/main/java/com/penguineering/gartenplus/auth/role/SystemRoleService.java b/src/main/java/com/penguineering/gartenplus/auth/role/SystemRoleService.java new file mode 100644 index 0000000..51c1709 --- /dev/null +++ b/src/main/java/com/penguineering/gartenplus/auth/role/SystemRoleService.java @@ -0,0 +1,73 @@ +package com.penguineering.gartenplus.auth.role; + +import com.penguineering.gartenplus.auth.user.UserRepository; +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.transaction.Transactional; +import org.springframework.stereotype.Service; + +import java.util.*; +import java.util.stream.Collectors; + +@Service +public class SystemRoleService { + private static final Map HANDLES; + + static { + HANDLES = Arrays.stream(SystemRole.values()) + .map(role -> Map.entry(role.getHandle(), role)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + private final RoleMappingRepository roleMappingRepository; + private final UserRepository userRepository; + + @PersistenceContext + private EntityManager entityManager; + + public SystemRoleService( + RoleMappingRepository roleMappingRepository, + UserRepository userRepository) { + this.roleMappingRepository = roleMappingRepository; + this.userRepository = userRepository; + } + + @Transactional + public Set getRolesForUser(UUID userId) { + return Optional.ofNullable(userId) + .filter(userRepository::existsById) + .map(roleMappingRepository::findByUserId) + .map(l -> l.stream() + .map(RoleMapping::getRoleHandle) + // ignore unknown handles + .filter(HANDLES.keySet()::contains) + .map(HANDLES::get) + .collect(Collectors.toSet())) + .orElse(Set.of()); + } + + @Transactional + public void setRoleForUser(SystemRole role, UUID userId) { + var mapping = new RoleMapping(role.getHandle(), userId); + roleMappingRepository.save(mapping); + } + + @Transactional + public void clearRoleForUser(SystemRole role, UUID userId) { + var mapping = new RoleMapping(role.getHandle(), userId); + roleMappingRepository.delete(mapping); + } + + @Transactional + public void updateRoles(UUID userId, Set roles) { + var currentRoles = getRolesForUser(userId); + + var toAdd = new HashSet<>(roles); + toAdd.removeAll(currentRoles); + toAdd.forEach(role -> setRoleForUser(role, userId)); + + var toRemove = new HashSet<>(currentRoles); + toRemove.removeAll(roles); + toRemove.forEach(role -> clearRoleForUser(role, userId)); + } +} From c9b11349257fdcf24adcb41f2f3987a32b76f96f Mon Sep 17 00:00:00 2001 From: Stefan Haun Date: Sat, 20 Jul 2024 16:42:40 +0200 Subject: [PATCH 4/7] Extend the user entity service to save DTOs --- .../auth/user/UserEntityService.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/main/java/com/penguineering/gartenplus/auth/user/UserEntityService.java b/src/main/java/com/penguineering/gartenplus/auth/user/UserEntityService.java index 2dc2f6c..d35f46c 100644 --- a/src/main/java/com/penguineering/gartenplus/auth/user/UserEntityService.java +++ b/src/main/java/com/penguineering/gartenplus/auth/user/UserEntityService.java @@ -7,6 +7,7 @@ import org.springframework.stereotype.Service; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.UUID; import java.util.stream.StreamSupport; @@ -50,4 +51,27 @@ public Optional getUserWithGroups(UUID userId) { public UserEntity save(UserEntity user) { return userRepository.save(user); } + + @Transactional + public UserDTO saveDTO(UserDTO user) { + return Optional.ofNullable(user) + .map(UserDTO::id) + //load or create entity + .flatMap(id -> Optional.of(id) + .map(userRepository::findById) + .filter(Optional::isPresent) + .map(Optional::get) + .or(() -> Optional.of(new UserEntity()))) + // update entity + .map(e -> { + e.setDisplayName(user.displayName()); + e.setEmail(user.email()); + e.setAvatarUrl(user.avatarUrl()); + return e; + }) + // save entity + .map(userRepository::save) + .map(UserEntity::toDTO) + .orElse(null); + } } From a7290e8edb83b45d530e2ae2300b42440c309efe Mon Sep 17 00:00:00 2001 From: Stefan Haun Date: Sat, 20 Jul 2024 16:42:57 +0200 Subject: [PATCH 5/7] Extend the User Editor for role handling --- .../admin/settings/users/UserEditor.java | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/penguineering/gartenplus/ui/content/admin/settings/users/UserEditor.java b/src/main/java/com/penguineering/gartenplus/ui/content/admin/settings/users/UserEditor.java index bdec9ea..27a2aac 100644 --- a/src/main/java/com/penguineering/gartenplus/ui/content/admin/settings/users/UserEditor.java +++ b/src/main/java/com/penguineering/gartenplus/ui/content/admin/settings/users/UserEditor.java @@ -1,8 +1,10 @@ package com.penguineering.gartenplus.ui.content.admin.settings.users; +import com.penguineering.gartenplus.auth.role.SystemRole; import com.penguineering.gartenplus.auth.user.UserDTO; import com.vaadin.flow.component.button.Button; import com.vaadin.flow.component.button.ButtonVariant; +import com.vaadin.flow.component.checkbox.CheckboxGroup; import com.vaadin.flow.component.orderedlayout.HorizontalLayout; import com.vaadin.flow.component.orderedlayout.VerticalLayout; import com.vaadin.flow.component.textfield.TextField; @@ -12,19 +14,20 @@ import lombok.Setter; import java.net.URI; -import java.util.Objects; -import java.util.UUID; -import java.util.function.Consumer; +import java.util.*; +import java.util.function.BiConsumer; public class UserEditor extends VerticalLayout { - private final Consumer saveAction; + private final BiConsumer> saveAction; private final Runnable cancelAction; private final Binder binder = new Binder<>(UserBean.class); private UserBean user; + private final CheckboxGroup rolesCheckgroup; + public UserEditor( - Consumer saveAction, + BiConsumer> saveAction, Runnable cancelAction ) { this.saveAction = saveAction; @@ -51,7 +54,14 @@ public UserEditor( TextField avatarUrl = new TextField("Avatar-URL"); avatarUrl.setWidthFull(); - fieldsLayout.add(id, displayName, email, avatarUrl); + rolesCheckgroup = new CheckboxGroup<>(); + rolesCheckgroup.setLabel("System-Rollen"); + rolesCheckgroup.setItems(SystemRole.values()); + rolesCheckgroup.setItemLabelGenerator(SystemRole::getDisplayName); + rolesCheckgroup.addValueChangeListener(e -> user.setRoles(e.getValue())); + rolesCheckgroup.setReadOnly(Objects.isNull(saveAction)); + + fieldsLayout.add(id, displayName, email, avatarUrl, rolesCheckgroup); HorizontalLayout buttonsLayout = new HorizontalLayout(); buttonsLayout.setWidthFull(); @@ -79,16 +89,21 @@ public UserEditor( .bind(UserBean::getDisplayName, null); binder.forField(email).bind(UserBean::getEmail, null); binder.forField(avatarUrl).bind(UserBean::getAvatarUrl, null); + binder.forField(rolesCheckgroup).bind(UserBean::getRoles, UserBean::setRoles); - setUser(null); + setUser(null, null); } - public void setUser(UserDTO userDTO) { + public void setUser(UserDTO userDTO, Set roles) { this.user = Objects.isNull(userDTO) ? new UserBean(null, "", "", "") : new UserBean(userDTO.id().toString(), userDTO.displayName(), userDTO.email(), userDTO.avatarUrl().toASCIIString()); + this.user.setRoles( + new HashSet<>( + Objects.requireNonNullElse(roles, Set.of()))); binder.readBean(user); + rolesCheckgroup.setValue(this.user.getRoles()); } private void save() { @@ -99,7 +114,7 @@ private void save() { user.getId() == null ? null : UUID.fromString(user.getId()), user.getDisplayName(), user.getEmail(), user.getAvatarUrl() == null ? null : URI.create(user.getAvatarUrl())); - this.saveAction.accept(newUser); + this.saveAction.accept(newUser, user.getRoles()); } } catch (ValidationException e) { // Handle validation errors @@ -119,12 +134,14 @@ private static class UserBean { private String displayName; private String email; private String avatarUrl; + private Set roles; public UserBean(String id, String displayName, String email, String avatarUrl) { this.id = id; this.displayName = displayName; this.email = email; this.avatarUrl = avatarUrl; + this.roles = new HashSet<>(); } } } From 636fa52ccee334d1a7e54bd46e4ec3557e7d2146 Mon Sep 17 00:00:00 2001 From: Stefan Haun Date: Sat, 20 Jul 2024 16:43:09 +0200 Subject: [PATCH 6/7] Enable saving and role handling on the user settings page --- .../settings/users/UsersSettingsPage.java | 26 ++++++++++++++----- 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/penguineering/gartenplus/ui/content/admin/settings/users/UsersSettingsPage.java b/src/main/java/com/penguineering/gartenplus/ui/content/admin/settings/users/UsersSettingsPage.java index dbed2f9..9baed6f 100644 --- a/src/main/java/com/penguineering/gartenplus/ui/content/admin/settings/users/UsersSettingsPage.java +++ b/src/main/java/com/penguineering/gartenplus/ui/content/admin/settings/users/UsersSettingsPage.java @@ -1,5 +1,8 @@ package com.penguineering.gartenplus.ui.content.admin.settings.users; +import com.penguineering.gartenplus.auth.role.SystemRole; +import com.penguineering.gartenplus.auth.role.SystemRoleService; +import com.penguineering.gartenplus.auth.user.UserDTO; import com.penguineering.gartenplus.auth.user.UserEntity; import com.penguineering.gartenplus.auth.user.UserEntityService; import com.penguineering.gartenplus.ui.appframe.GartenplusPage; @@ -10,6 +13,7 @@ import com.vaadin.flow.router.Route; import jakarta.annotation.security.PermitAll; +import java.util.Set; import java.util.UUID; import java.util.function.Consumer; @@ -19,14 +23,18 @@ public class UsersSettingsPage extends GartenplusPage { private final UserEntityService userEntityService; + private final SystemRoleService systemRoleService; private final Consumer userEditorVisibility; private final UserEditor userEditor; private final UsersGrid usersGrid; - public UsersSettingsPage(UserEntityService userEntityService) { + public UsersSettingsPage( + UserEntityService userEntityService, + SystemRoleService systemRoleService) { this.userEntityService = userEntityService; + this.systemRoleService = systemRoleService; Div userEditorDiv = new Div(); userEditorDiv.getStyle() @@ -37,7 +45,7 @@ public UsersSettingsPage(UserEntityService userEntityService) { userEditorDiv.add(new H3("Benutzer bearbeiten")); - userEditor = new UserEditor(null, this::closeUserEditor); + userEditor = new UserEditor(this::saveUser, this::closeUserEditor); userEditorDiv.add(userEditor); @@ -58,16 +66,22 @@ private void editUser(UUID userId) { var user = userEntityService.getUser(userId) .map(UserEntity::toDTO) .orElse(null); - this.userEditor.setUser(user); + var roles = systemRoleService.getRolesForUser(userId); + this.userEditor.setUser(user, roles); this.userEditorVisibility.accept(true); } - private void closeUserEditor() { - this.userEditorVisibility.accept(false); + private void saveUser(UserDTO user, Set roles) { + userEntityService.saveDTO(user); + systemRoleService.updateRoles(user.id(), roles); - this.userEditor.setUser(null); + closeUserEditor(); } + private void closeUserEditor() { + this.userEditorVisibility.accept(false); + this.userEditor.setUser(null, null); + } } From ac7a1cc054cebcf5d9a1b9c3ef03a2ca4cb22ed8 Mon Sep 17 00:00:00 2001 From: Stefan Haun Date: Sat, 20 Jul 2024 16:43:22 +0200 Subject: [PATCH 7/7] Show current roles on the profile page --- .../gartenplus/ui/content/admin/ProfilePage.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/penguineering/gartenplus/ui/content/admin/ProfilePage.java b/src/main/java/com/penguineering/gartenplus/ui/content/admin/ProfilePage.java index 1d97dfa..4078ba4 100644 --- a/src/main/java/com/penguineering/gartenplus/ui/content/admin/ProfilePage.java +++ b/src/main/java/com/penguineering/gartenplus/ui/content/admin/ProfilePage.java @@ -1,6 +1,8 @@ package com.penguineering.gartenplus.ui.content.admin; import com.penguineering.gartenplus.auth.group.GroupEntity; +import com.penguineering.gartenplus.auth.role.SystemRole; +import com.penguineering.gartenplus.auth.role.SystemRoleService; import com.penguineering.gartenplus.auth.user.UserDTO; import com.penguineering.gartenplus.auth.user.UserEntity; import com.penguineering.gartenplus.auth.user.UserEntityService; @@ -24,7 +26,8 @@ @PageTitle("GartenPlus | Benutzerprofil") public class ProfilePage extends GartenplusPage { public ProfilePage(@Qualifier("user") Supplier currentUser, - UserEntityService userEntityService) { + UserEntityService userEntityService, + SystemRoleService systemRoleService) { // reload user from database with groups Optional userEntityOpt = @@ -68,5 +71,15 @@ public ProfilePage(@Qualifier("user") Supplier currentUser, .map(s -> "Du bist in folgenden Gruppen: " + s) .orElse("Du bist in keinen Gruppen"); add(new Paragraph(groupString)); + + var roleString = userEntityOpt + .map(UserEntity::getId) + .map(systemRoleService::getRolesForUser) + .flatMap(l -> l.stream() + .map(SystemRole::getDisplayName) + .reduce((a, b) -> a + ", " + b)) + .map(s -> "Du hast folgende Rollen: " + s) + .orElse("Du hast keine Rollen"); + add(new Paragraph(roleString)); } }