From 4644fc16850f58a706506ae05239d46a981e9227 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20B=C3=A9gaudeau?= Date: Wed, 26 Jul 2023 00:57:19 +0200 Subject: [PATCH] [211] Store profile images retrieved using OAuth2 metadata in our database MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: https://github.com/svalyn/svalyn-studio/issues/211 Signed-off-by: Stéphane Bégaudeau --- backend/svalyn-studio-application/pom.xml | 4 + .../controllers/account/AvatarController.java | 74 ++++++++++++++ .../services/account/AccountService.java | 10 +- .../services/account/AvatarUrlService.java | 42 ++++++++ .../account/api/IAvatarUrlService.java | 28 ++++++ .../services/activity/ActivityService.java | 8 +- .../services/history/BranchService.java | 10 +- .../history/ChangeProposalService.java | 16 +-- .../services/history/ChangeService.java | 10 +- .../notification/NotificationService.java | 12 ++- .../organization/InvitationService.java | 12 ++- .../organization/MembershipService.java | 12 ++- .../organization/OrganizationService.java | 10 +- .../services/project/ProjectService.java | 10 +- .../src/main/resources/images/avatar.png | Bin 0 -> 15552 bytes .../svalyn/studio/domain/account/Account.java | 31 ++++-- .../repositories/IAccountRepository.java | 4 +- .../studio/domain/authentication/IUser.java | 2 - .../avatar/AvatarRetrievalService.java | 94 ++++++++++++++++++ .../infrastructure/avatar/api/AvatarData.java | 28 ++++++ .../avatar/api/IAvatarRetrievalService.java | 31 ++++++ .../initialization/Initializer.java | 1 - .../AccountEventToMessageConverter.java | 11 +- .../security/DefaultAccountsInitializer.java | 1 - .../OAuth2UserServiceConfiguration.java | 4 +- .../security/SecurityConfiguration.java | 1 + .../security/SvalynOAuth2User.java | 8 -- .../security/SvalynOAuth2UserService.java | 16 ++- .../security/SvalynSystemUser.java | 5 - .../security/SvalynUserDetails.java | 13 +-- .../security/SvalynUserDetailsService.java | 1 - .../db/changelog/db.changelog-v2023.8.0.xml | 11 ++ ...thMockPrincipalSecurityContextFactory.java | 5 - .../account/AccountIntegrationTests.java | 5 +- .../src/test/resources/scripts/initialize.sql | 20 ++-- 35 files changed, 448 insertions(+), 102 deletions(-) create mode 100644 backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/AvatarController.java create mode 100644 backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AvatarUrlService.java create mode 100644 backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/api/IAvatarUrlService.java create mode 100644 backend/svalyn-studio-application/src/main/resources/images/avatar.png create mode 100644 backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/AvatarRetrievalService.java create mode 100644 backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/api/AvatarData.java create mode 100644 backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/api/IAvatarRetrievalService.java diff --git a/backend/svalyn-studio-application/pom.xml b/backend/svalyn-studio-application/pom.xml index c2fb4a8..32c7d76 100644 --- a/backend/svalyn-studio-application/pom.xml +++ b/backend/svalyn-studio-application/pom.xml @@ -41,6 +41,10 @@ + + org.springframework.boot + spring-boot-starter-web + org.springframework.boot spring-boot-starter-graphql diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/AvatarController.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/AvatarController.java new file mode 100644 index 0000000..54581f6 --- /dev/null +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/controllers/account/AvatarController.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.application.controllers.account; + +import com.svalyn.studio.domain.account.repositories.IAccountRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import java.io.IOException; +import java.util.Objects; + +/** + * Controller used to retrieve avatar image. + * + * @author sbegaudeau + */ +@Controller +public class AvatarController { + + private final IAccountRepository accountRepository; + + private final Logger logger = LoggerFactory.getLogger(AvatarController.class); + + public AvatarController(IAccountRepository accountRepository) { + this.accountRepository = Objects.requireNonNull(accountRepository); + } + + @GetMapping(value = "/api/avatars/{username}") + public ResponseEntity getAvatar(@PathVariable String username) { + return this.accountRepository.findByUsername(username) + .filter(account -> account.getImage() != null && account.getImageContentType() != null) + .map(account -> ResponseEntity.ok() + .header(HttpHeaders.CONTENT_TYPE, account.getImageContentType()) + .body(account.getImage())) + .orElse(this.defaultAvatar()); + } + + public ResponseEntity defaultAvatar() { + byte[] content = new byte[] {}; + var avatar = new ClassPathResource("images/avatar.png"); + try { + content = avatar.getContentAsByteArray(); + } catch (IOException exception) { + logger.warn(exception.getMessage(), exception); + } + + return ResponseEntity.ok() + .header(HttpHeaders.CONTENT_TYPE, "image/png") + .body(content); + } +} diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AccountService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AccountService.java index 0f76adb..37a3a67 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AccountService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AccountService.java @@ -22,6 +22,7 @@ import com.svalyn.studio.application.controllers.dto.ProfileDTO; import com.svalyn.studio.application.controllers.viewer.Viewer; import com.svalyn.studio.application.services.account.api.IAccountService; +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; import com.svalyn.studio.domain.account.repositories.IAccountRepository; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -40,19 +41,22 @@ public class AccountService implements IAccountService { private final IAccountRepository accountRepository; - public AccountService(IAccountRepository accountRepository) { + private final IAvatarUrlService avatarUrlService; + + public AccountService(IAccountRepository accountRepository, IAvatarUrlService avatarUrlService) { this.accountRepository = Objects.requireNonNull(accountRepository); + this.avatarUrlService = Objects.requireNonNull(avatarUrlService); } @Override @Transactional(readOnly = true) public Optional findViewerById(UUID id) { - return this.accountRepository.findById(id).map(account -> new Viewer(account.getName(), account.getUsername(), account.getImageUrl())); + return this.accountRepository.findById(id).map(account -> new Viewer(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); } @Override @Transactional(readOnly = true) public Optional findProfileByUsername(String username) { - return this.accountRepository.findByUsername(username).map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + return this.accountRepository.findByUsername(username).map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); } } diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AvatarUrlService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AvatarUrlService.java new file mode 100644 index 0000000..ef5ff58 --- /dev/null +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/AvatarUrlService.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.svalyn.studio.application.services.account; + +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +/** + * Used to compute the url of the avatar of a profile. + * + * @author sbegaudeau + */ +@Service +public class AvatarUrlService implements IAvatarUrlService { + @Override + public String imageUrl(String username) { + var currentUri = ServletUriComponentsBuilder.fromCurrentRequestUri().build().toUri(); + var uri = currentUri.getScheme() + "://" + currentUri.getHost(); + if (currentUri.getPort() != -1) { + uri = uri + ":" + currentUri.getPort(); + } + uri = uri + "/api/avatars/" + username; + return uri; + } +} diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/api/IAvatarUrlService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/api/IAvatarUrlService.java new file mode 100644 index 0000000..f0cfe14 --- /dev/null +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/account/api/IAvatarUrlService.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.svalyn.studio.application.services.account.api; + +/** + * Used to compute the url of the avatar of a profile. + * + * @author sbegaudeau + */ +public interface IAvatarUrlService { + String imageUrl(String username); +} diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/activity/ActivityService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/activity/ActivityService.java index 6621b1d..92b5780 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/activity/ActivityService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/activity/ActivityService.java @@ -21,6 +21,7 @@ import com.svalyn.studio.application.controllers.activity.dto.ActivityEntryDTO; import com.svalyn.studio.application.controllers.dto.ProfileDTO; +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; import com.svalyn.studio.application.services.activity.api.IActivityService; import com.svalyn.studio.domain.account.Account; import com.svalyn.studio.domain.account.repositories.IAccountRepository; @@ -49,15 +50,18 @@ public class ActivityService implements IActivityService { private final IActivityEntryRepository activityEntryRepository; + private final IAvatarUrlService avatarUrlService; - public ActivityService(IAccountRepository accountRepository, IActivityEntryRepository activityEntryRepository) { + + public ActivityService(IAccountRepository accountRepository, IActivityEntryRepository activityEntryRepository, IAvatarUrlService avatarUrlService) { this.accountRepository = Objects.requireNonNull(accountRepository); this.activityEntryRepository = Objects.requireNonNull(activityEntryRepository); + this.avatarUrlService = Objects.requireNonNull(avatarUrlService); } private Optional toDTO(ActivityEntry activityEntry) { var optionalCreatedByProfile = this.accountRepository.findById(activityEntry.getCreatedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); return optionalCreatedByProfile.map(createdBy -> new ActivityEntryDTO( activityEntry.getId(), diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/BranchService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/BranchService.java index 7147158..6e0f2e0 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/BranchService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/BranchService.java @@ -21,6 +21,7 @@ import com.svalyn.studio.application.controllers.dto.ProfileDTO; import com.svalyn.studio.application.controllers.history.dto.BranchDTO; +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; import com.svalyn.studio.application.services.history.api.IBranchService; import com.svalyn.studio.domain.account.repositories.IAccountRepository; import com.svalyn.studio.domain.history.Branch; @@ -47,9 +48,12 @@ public class BranchService implements IBranchService { private final IAccountRepository accountRepository; private final IBranchRepository branchRepository; - public BranchService(IAccountRepository accountRepository, IBranchRepository branchRepository) { + private final IAvatarUrlService avatarUrlService; + + public BranchService(IAccountRepository accountRepository, IBranchRepository branchRepository, IAvatarUrlService avatarUrlService) { this.accountRepository = Objects.requireNonNull(accountRepository); this.branchRepository = Objects.requireNonNull(branchRepository); + this.avatarUrlService = Objects.requireNonNull(avatarUrlService); } @Override @@ -71,9 +75,9 @@ public Optional findByProjectIdAndName(UUID projectId, String name) { private Optional toDTO(Branch branch) { var optionalCreatedByProfile = this.accountRepository.findById(branch.getCreatedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalLastModifiedByProfile = this.accountRepository.findById(branch.getLastModifiedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var changeId = Optional.ofNullable(branch.getChange()).map(AggregateReference::getId).orElse(null); return optionalCreatedByProfile.flatMap(createdBy -> diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/ChangeProposalService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/ChangeProposalService.java index 613eb62..8d16900 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/ChangeProposalService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/ChangeProposalService.java @@ -33,6 +33,7 @@ import com.svalyn.studio.application.controllers.dto.IPayload; import com.svalyn.studio.application.controllers.dto.ProfileDTO; import com.svalyn.studio.application.controllers.dto.SuccessPayload; +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; import com.svalyn.studio.application.services.history.api.IChangeProposalService; import com.svalyn.studio.domain.Failure; import com.svalyn.studio.domain.Success; @@ -44,7 +45,6 @@ import com.svalyn.studio.domain.history.services.api.IChangeProposalCreationService; import com.svalyn.studio.domain.history.services.api.IChangeProposalDeletionService; import com.svalyn.studio.domain.history.services.api.IChangeProposalUpdateService; -import com.svalyn.studio.domain.resource.repositories.IResourceRepository; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -74,22 +74,22 @@ public class ChangeProposalService implements IChangeProposalService { private final IChangeProposalDeletionService changeProposalDeletionService; - private final IResourceRepository resourceRepository; + private final IAvatarUrlService avatarUrlService; - public ChangeProposalService(IAccountRepository accountRepository, IChangeProposalRepository changeProposalRepository, IChangeProposalCreationService changeProposalCreationService, IChangeProposalUpdateService changeProposalUpdateService, IChangeProposalDeletionService changeProposalDeletionService, IResourceRepository resourceRepository) { + public ChangeProposalService(IAccountRepository accountRepository, IChangeProposalRepository changeProposalRepository, IChangeProposalCreationService changeProposalCreationService, IChangeProposalUpdateService changeProposalUpdateService, IChangeProposalDeletionService changeProposalDeletionService, IAvatarUrlService avatarUrlService) { this.accountRepository = Objects.requireNonNull(accountRepository); this.changeProposalRepository = Objects.requireNonNull(changeProposalRepository); this.changeProposalCreationService = Objects.requireNonNull(changeProposalCreationService); this.changeProposalUpdateService = Objects.requireNonNull(changeProposalUpdateService); this.changeProposalDeletionService = Objects.requireNonNull(changeProposalDeletionService); - this.resourceRepository = Objects.requireNonNull(resourceRepository); + this.avatarUrlService = Objects.requireNonNull(avatarUrlService); } private Optional toDTO(ChangeProposal changeProposal) { var optionalCreatedByProfile = this.accountRepository.findById(changeProposal.getCreatedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalLastModifiedByProfile = this.accountRepository.findById(changeProposal.getLastModifiedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); return optionalCreatedByProfile.flatMap(createdBy -> optionalLastModifiedByProfile.map(lastModifiedBy -> @@ -144,9 +144,9 @@ public IPayload createChangeProposal(CreateChangeProposalInput input) { private Optional toDTO(Review review) { var optionalCreatedByProfile = this.accountRepository.findById(review.getCreatedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalLastModifiedByProfile = this.accountRepository.findById(review.getLastModifiedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); return optionalCreatedByProfile.flatMap(createdBy -> optionalLastModifiedByProfile.map(lastModifiedBy -> diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/ChangeService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/ChangeService.java index c06a98c..76f7232 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/ChangeService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/history/ChangeService.java @@ -24,6 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.svalyn.studio.application.controllers.dto.ProfileDTO; import com.svalyn.studio.application.controllers.history.dto.ChangeDTO; +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; import com.svalyn.studio.application.services.history.api.IChangeService; import com.svalyn.studio.domain.account.repositories.IAccountRepository; import com.svalyn.studio.domain.history.Change; @@ -50,18 +51,21 @@ public class ChangeService implements IChangeService { private final IChangeRepository changeRepository; + private final IAvatarUrlService avatarUrlService; + private final Logger logger = LoggerFactory.getLogger(ChangeService.class); - public ChangeService(IAccountRepository accountRepository, IChangeRepository changeRepository) { + public ChangeService(IAccountRepository accountRepository, IChangeRepository changeRepository, IAvatarUrlService avatarUrlService) { this.accountRepository = Objects.requireNonNull(accountRepository); this.changeRepository = Objects.requireNonNull(changeRepository); + this.avatarUrlService = Objects.requireNonNull(avatarUrlService); } private Optional toDTO(Change change) { var optionalCreatedByProfile = this.accountRepository.findById(change.getCreatedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalLastModifiedByProfile = this.accountRepository.findById(change.getLastModifiedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); return optionalCreatedByProfile.flatMap(createdBy -> optionalLastModifiedByProfile.map(lastModifiedBy -> diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/notification/NotificationService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/notification/NotificationService.java index 59d1302..d64b6fc 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/notification/NotificationService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/notification/NotificationService.java @@ -25,6 +25,7 @@ import com.svalyn.studio.application.controllers.dto.SuccessPayload; import com.svalyn.studio.application.controllers.notification.dto.NotificationDTO; import com.svalyn.studio.application.controllers.notification.dto.UpdateNotificationsStatusInput; +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; import com.svalyn.studio.application.services.notification.api.INotificationService; import com.svalyn.studio.domain.Failure; import com.svalyn.studio.domain.Success; @@ -56,21 +57,24 @@ public class NotificationService implements INotificationService { private final INotificationRepository notificationRepository; + private final IAvatarUrlService avatarUrlService; + private final INotificationUpdateService notificationUpdateService; - public NotificationService(IAccountRepository accountRepository, INotificationRepository notificationRepository, INotificationUpdateService notificationUpdateService) { + public NotificationService(IAccountRepository accountRepository, INotificationRepository notificationRepository, IAvatarUrlService avatarUrlService, INotificationUpdateService notificationUpdateService) { this.accountRepository = Objects.requireNonNull(accountRepository); this.notificationRepository = Objects.requireNonNull(notificationRepository); + this.avatarUrlService = Objects.requireNonNull(avatarUrlService); this.notificationUpdateService = Objects.requireNonNull(notificationUpdateService); } private Optional toDTO(Notification notification) { var optionalOwnedByProfile = this.accountRepository.findById(notification.getOwnedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalCreatedByProfile = this.accountRepository.findById(notification.getCreatedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalLastModifiedByProfile = this.accountRepository.findById(notification.getLastModifiedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); return optionalOwnedByProfile.flatMap(ownedBy -> optionalCreatedByProfile.flatMap(createdBy -> diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/InvitationService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/InvitationService.java index c63eb9d..a07b94c 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/InvitationService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/InvitationService.java @@ -29,6 +29,7 @@ import com.svalyn.studio.application.controllers.organization.dto.InviteMemberInput; import com.svalyn.studio.application.controllers.organization.dto.OrganizationDTO; import com.svalyn.studio.application.controllers.organization.dto.RevokeInvitationInput; +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; import com.svalyn.studio.application.services.organization.api.IInvitationService; import com.svalyn.studio.domain.Failure; import com.svalyn.studio.domain.Success; @@ -65,22 +66,25 @@ public class InvitationService implements IInvitationService { private final IOrganizationUpdateService organizationUpdateService; + private final IAvatarUrlService avatarUrlService; + private final IMessageService messageService; - public InvitationService(IAccountRepository accountRepository, IOrganizationRepository organizationRepository, IOrganizationUpdateService organizationUpdateService, IMessageService messageService) { + public InvitationService(IAccountRepository accountRepository, IOrganizationRepository organizationRepository, IOrganizationUpdateService organizationUpdateService, IAvatarUrlService avatarUrlService, IMessageService messageService) { this.accountRepository = Objects.requireNonNull(accountRepository); this.organizationRepository = Objects.requireNonNull(organizationRepository); this.organizationUpdateService = Objects.requireNonNull(organizationUpdateService); + this.avatarUrlService = Objects.requireNonNull(avatarUrlService); this.messageService = Objects.requireNonNull(messageService); } private Optional toDTO(Invitation invitation, UUID organizationId) { var optionalCreatedByProfile = this.accountRepository.findById(invitation.getCreatedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalLastModifiedByProfile = this.accountRepository.findById(invitation.getLastModifiedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalMemberProfile = this.accountRepository.findById(invitation.getMemberId().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); return optionalMemberProfile.flatMap(member -> optionalCreatedByProfile.flatMap(createdBy -> diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/MembershipService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/MembershipService.java index 334ef88..79ec5eb 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/MembershipService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/MembershipService.java @@ -26,6 +26,7 @@ import com.svalyn.studio.application.controllers.organization.dto.MembershipDTO; import com.svalyn.studio.application.controllers.organization.dto.OrganizationDTO; import com.svalyn.studio.application.controllers.organization.dto.RevokeMembershipsInput; +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; import com.svalyn.studio.application.services.organization.api.IMembershipService; import com.svalyn.studio.domain.Failure; import com.svalyn.studio.domain.Success; @@ -59,19 +60,22 @@ public class MembershipService implements IMembershipService { private final IOrganizationUpdateService organizationUpdateService; - public MembershipService(IAccountRepository accountRepository, IOrganizationRepository organizationRepository, IOrganizationUpdateService organizationUpdateService) { + private final IAvatarUrlService avatarUrlService; + + public MembershipService(IAccountRepository accountRepository, IOrganizationRepository organizationRepository, IOrganizationUpdateService organizationUpdateService, IAvatarUrlService avatarUrlService) { this.accountRepository = Objects.requireNonNull(accountRepository); this.organizationRepository = Objects.requireNonNull(organizationRepository); this.organizationUpdateService = Objects.requireNonNull(organizationUpdateService); + this.avatarUrlService = Objects.requireNonNull(avatarUrlService); } private Optional toDTO(Membership membership) { var optionalCreatedByProfile = this.accountRepository.findById(membership.getCreatedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalLastModifiedByProfile = this.accountRepository.findById(membership.getLastModifiedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalMemberProfile = this.accountRepository.findById(membership.getMemberId().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); return optionalMemberProfile.flatMap(member -> optionalCreatedByProfile.flatMap(createdBy -> diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/OrganizationService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/OrganizationService.java index ada0f02..1021da4 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/OrganizationService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/organization/OrganizationService.java @@ -29,6 +29,7 @@ import com.svalyn.studio.application.controllers.organization.dto.LeaveOrganizationInput; import com.svalyn.studio.application.controllers.organization.dto.OrganizationDTO; import com.svalyn.studio.application.controllers.organization.dto.UpdateOrganizationNameInput; +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; import com.svalyn.studio.application.services.organization.api.IOrganizationService; import com.svalyn.studio.domain.Failure; import com.svalyn.studio.domain.Success; @@ -61,6 +62,8 @@ public class OrganizationService implements IOrganizationService { private final IAccountRepository accountRepository; + private final IAvatarUrlService avatarUrlService; + private final IOrganizationRepository organizationRepository; private final IOrganizationCreationService organizationCreationService; @@ -71,8 +74,9 @@ public class OrganizationService implements IOrganizationService { private final IOrganizationPermissionService organizationPermissionService; - public OrganizationService(IAccountRepository accountRepository, IOrganizationRepository organizationRepository, IOrganizationCreationService organizationCreationService, IOrganizationUpdateService organizationUpdateService, IOrganizationDeletionService organizationDeletionService, IOrganizationPermissionService organizationPermissionService) { + public OrganizationService(IAccountRepository accountRepository, IAvatarUrlService avatarUrlService, IOrganizationRepository organizationRepository, IOrganizationCreationService organizationCreationService, IOrganizationUpdateService organizationUpdateService, IOrganizationDeletionService organizationDeletionService, IOrganizationPermissionService organizationPermissionService) { this.accountRepository = Objects.requireNonNull(accountRepository); + this.avatarUrlService = Objects.requireNonNull(avatarUrlService); this.organizationRepository = Objects.requireNonNull(organizationRepository); this.organizationCreationService = Objects.requireNonNull(organizationCreationService); this.organizationUpdateService = Objects.requireNonNull(organizationUpdateService); @@ -82,9 +86,9 @@ public OrganizationService(IAccountRepository accountRepository, IOrganizationRe private Optional toDTO(Organization organization) { var optionalCreatedByProfile = this.accountRepository.findById(organization.getCreatedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalLastModifiedByProfile = this.accountRepository.findById(organization.getLastModifiedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var userId = UserIdProvider.get().getId(); return optionalCreatedByProfile.flatMap(createdBy -> diff --git a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/project/ProjectService.java b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/project/ProjectService.java index e6e0bc6..fdc9ceb 100644 --- a/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/project/ProjectService.java +++ b/backend/svalyn-studio-application/src/main/java/com/svalyn/studio/application/services/project/ProjectService.java @@ -30,6 +30,7 @@ import com.svalyn.studio.application.controllers.project.dto.UpdateProjectDescriptionInput; import com.svalyn.studio.application.controllers.project.dto.UpdateProjectNameInput; import com.svalyn.studio.application.controllers.project.dto.UpdateProjectReadMeInput; +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; import com.svalyn.studio.application.services.project.api.IProjectService; import com.svalyn.studio.domain.Failure; import com.svalyn.studio.domain.Success; @@ -68,19 +69,22 @@ public class ProjectService implements IProjectService { private final IProjectDeletionService projectDeletionService; - public ProjectService(IAccountRepository accountRepository, IProjectRepository projectRepository, IProjectCreationService projectCreationService, IProjectUpdateService projectUpdateService, IProjectDeletionService projectDeletionService) { + private final IAvatarUrlService avatarUrlService; + + public ProjectService(IAccountRepository accountRepository, IProjectRepository projectRepository, IProjectCreationService projectCreationService, IProjectUpdateService projectUpdateService, IProjectDeletionService projectDeletionService, IAvatarUrlService avatarUrlService) { this.accountRepository = Objects.requireNonNull(accountRepository); this.projectRepository = Objects.requireNonNull(projectRepository); this.projectCreationService = Objects.requireNonNull(projectCreationService); this.projectUpdateService = Objects.requireNonNull(projectUpdateService); this.projectDeletionService = Objects.requireNonNull(projectDeletionService); + this.avatarUrlService = Objects.requireNonNull(avatarUrlService); } private Optional toDTO(Project project) { var optionalCreatedByProfile = this.accountRepository.findById(project.getCreatedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); var optionalLastModifiedByProfile = this.accountRepository.findById(project.getLastModifiedBy().getId()) - .map(account -> new ProfileDTO(account.getName(), account.getUsername(), account.getImageUrl())); + .map(account -> new ProfileDTO(account.getName(), account.getUsername(), this.avatarUrlService.imageUrl(account.getUsername()))); return optionalCreatedByProfile.flatMap(createdBy -> optionalLastModifiedByProfile.map(lastModifiedBy -> diff --git a/backend/svalyn-studio-application/src/main/resources/images/avatar.png b/backend/svalyn-studio-application/src/main/resources/images/avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..e5aa246e1ea17c8f5999cc7655b8bb91ef8fea16 GIT binary patch literal 15552 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 implements private String email; - private String imageUrl; + private byte[] image; + + private String imageContentType; @MappedCollection(idColumn = "account_id") private Set passwordCredentials = new LinkedHashSet<>(); @@ -99,8 +101,12 @@ public String getEmail() { return email; } - public String getImageUrl() { - return imageUrl; + public byte[] getImage() { + return image; + } + + public String getImageContentType() { + return imageContentType; } public Set getPasswordCredentials() { @@ -128,9 +134,8 @@ public boolean isNew() { return this.isNew; } - public void updateDetails(String name, String imageUrl) { + public void updateName(String name) { this.name = Objects.requireNonNull(name); - this.imageUrl = Objects.requireNonNull(imageUrl); this.lastModifiedOn = Instant.now(); var createdBy = new Profile(this.id, this.name, this.username); @@ -185,7 +190,9 @@ public static final class Builder { private String email; - private String imageUrl; + private byte[] image; + + private String imageContentType; private Set passwordCredentials = new LinkedHashSet<>(); @@ -221,8 +228,13 @@ public Builder email(String email) { return this; } - public Builder imageUrl(String imageUrl) { - this.imageUrl = Objects.requireNonNull(imageUrl); + public Builder image(byte[] image) { + this.image = Objects.requireNonNull(image); + return this; + } + + public Builder imageContentType(String imageContentType) { + this.imageContentType = Objects.requireNonNull(imageContentType); return this; } @@ -234,7 +246,8 @@ public Account build() { account.username = Objects.requireNonNull(username); account.name = Objects.requireNonNull(name); account.email = Objects.requireNonNull(email); - account.imageUrl = Objects.requireNonNull(imageUrl); + account.image = image; + account.imageContentType = imageContentType; account.passwordCredentials = Objects.requireNonNull(passwordCredentials); account.oAuth2Metadata = Objects.requireNonNull(oAuth2Metadata); diff --git a/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/repositories/IAccountRepository.java b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/repositories/IAccountRepository.java index 8c41b6d..f6afd1e 100644 --- a/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/repositories/IAccountRepository.java +++ b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/repositories/IAccountRepository.java @@ -39,14 +39,14 @@ public interface IAccountRepository extends PagingAndSortingRepository findByEmail(String email); @Query(""" - SELECT * FROM account account + SELECT account.* FROM account account JOIN oauth2_metadata oauth2 ON account.id = oauth2.account_id WHERE oauth2.provider = :provider AND oauth2.provider_id = :providerId """) Optional findByOAuth2Metadata(String provider, String providerId); @Query(""" - SELECT * FROM account account + SELECT account.* FROM account account JOIN authentication_token authenticationToken ON account.id = authenticationToken.account_id WHERE authenticationToken.access_key = :accessKey AND authenticationToken.status = 'ACTIVE' """) diff --git a/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/authentication/IUser.java b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/authentication/IUser.java index 09883eb..6c31164 100644 --- a/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/authentication/IUser.java +++ b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/authentication/IUser.java @@ -31,6 +31,4 @@ public interface IUser { String getUsername(); String getName(); - - String getImageUrl(); } diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/AvatarRetrievalService.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/AvatarRetrievalService.java new file mode 100644 index 0000000..0962605 --- /dev/null +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/AvatarRetrievalService.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.infrastructure.avatar; + +import com.svalyn.studio.infrastructure.avatar.api.AvatarData; +import com.svalyn.studio.infrastructure.avatar.api.IAvatarRetrievalService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.core.io.buffer.DataBufferUtils; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.reactive.function.BodyExtractors; +import org.springframework.web.reactive.function.client.WebClient; + +import java.io.IOException; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.util.Objects; +import java.util.Optional; + +/** + * Used to download avatars. + * + * @author sbegaudeau + */ +@Service +public class AvatarRetrievalService implements IAvatarRetrievalService { + + private final WebClient webClient; + + private final Logger logger = LoggerFactory.getLogger(AvatarRetrievalService.class); + + public AvatarRetrievalService(WebClient webClient) { + this.webClient = Objects.requireNonNull(webClient); + } + + @Override + public Optional getData(String imageUrl) { + Optional optionalImageData = Optional.empty(); + + try ( + var outputStream = new PipedOutputStream(); + var inputStream = new PipedInputStream(outputStream); + ) { + var responseEntity = this.webClient.get() + .uri(imageUrl) + .retrieve() + .toEntityFlux(BodyExtractors.toDataBuffers()) + .doOnError(error -> this.logger.warn(error.getMessage(), error)) + .block(); + + if (responseEntity != null && responseEntity.hasBody()) { + DataBufferUtils.write(responseEntity.getBody(), outputStream) + .doFinally(signalType -> { + try { + outputStream.close(); + } catch (IOException exception) { + this.logger.warn(exception.getMessage(), exception); + } + }).subscribe(DataBufferUtils.releaseConsumer()); + + var contentType = Optional.of(responseEntity) + .map(ResponseEntity::getHeaders) + .map(HttpHeaders::getContentType) + .map(Object::toString) + .orElse(null); + + optionalImageData = Optional.of(new AvatarData(inputStream.readAllBytes(), contentType)); + } + } catch (IOException exception) { + this.logger.warn(exception.getMessage(), exception); + } + + return optionalImageData; + } +} diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/api/AvatarData.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/api/AvatarData.java new file mode 100644 index 0000000..5c9ab54 --- /dev/null +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/api/AvatarData.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.infrastructure.avatar.api; + +/** + * The data of the avatar. + * + * @author sbegaudeau + */ +public record AvatarData(byte[] content, String contentType) { +} diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/api/IAvatarRetrievalService.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/api/IAvatarRetrievalService.java new file mode 100644 index 0000000..a5d4fb8 --- /dev/null +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/api/IAvatarRetrievalService.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2023 Stéphane Bégaudeau. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and + * associated documentation files (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial + * portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT + * LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package com.svalyn.studio.infrastructure.avatar.api; + +import java.util.Optional; + +/** + * Used to download avatars. + * + * @author sbegaudeau + */ +public interface IAvatarRetrievalService { + Optional getData(String imageUrl); +} diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/initialization/Initializer.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/initialization/Initializer.java index ee84691..d885734 100644 --- a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/initialization/Initializer.java +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/initialization/Initializer.java @@ -69,7 +69,6 @@ public void run(String... args) throws Exception { .username("system") .name("System") .email("system@svalyn.com") - .imageUrl("") .build(); return this.accountRepository.save(account); }); diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/kafka/converters/AccountEventToMessageConverter.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/kafka/converters/AccountEventToMessageConverter.java index c8f1f21..4b4a9fd 100644 --- a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/kafka/converters/AccountEventToMessageConverter.java +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/kafka/converters/AccountEventToMessageConverter.java @@ -19,6 +19,7 @@ package com.svalyn.studio.infrastructure.kafka.converters; +import com.svalyn.studio.application.services.account.api.IAvatarUrlService; import com.svalyn.studio.domain.IDomainEvent; import com.svalyn.studio.domain.Profile; import com.svalyn.studio.domain.account.Account; @@ -32,6 +33,7 @@ import com.svalyn.studio.message.account.AccountSummaryMessage; import org.springframework.stereotype.Service; +import java.util.Objects; import java.util.Optional; import java.util.UUID; @@ -42,6 +44,13 @@ */ @Service public class AccountEventToMessageConverter implements IDomainEventToMessageConverter { + + private final IAvatarUrlService avatarUrlService; + + public AccountEventToMessageConverter(IAvatarUrlService avatarUrlService) { + this.avatarUrlService = Objects.requireNonNull(avatarUrlService); + } + @Override public Optional convert(IDomainEvent event) { Optional optionalMessage = Optional.empty(); @@ -76,7 +85,7 @@ private Optional toMessage(AccountModifiedEvent accountModifiedEvent) { private AccountMessage toMessage(Account account) { return new AccountMessage( account.getId(), - account.getImageUrl(), + this.avatarUrlService.imageUrl(account.getUsername()), account.getCreatedOn(), account.getLastModifiedOn() ); diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/DefaultAccountsInitializer.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/DefaultAccountsInitializer.java index 9f4f365..fd4b22f 100644 --- a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/DefaultAccountsInitializer.java +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/DefaultAccountsInitializer.java @@ -84,7 +84,6 @@ public void run(String... args) throws Exception { .passwordCredentials(Set.of(passwordCredentials)) .name("Admin") .email("admin@svalyn.com") - .imageUrl("") .build(); this.accountRepository.save(adminAccount); diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/OAuth2UserServiceConfiguration.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/OAuth2UserServiceConfiguration.java index 96fe1d9..1eb5be3 100644 --- a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/OAuth2UserServiceConfiguration.java +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/OAuth2UserServiceConfiguration.java @@ -36,7 +36,9 @@ public class OAuth2UserServiceConfiguration { @Bean public WebClient webClient(ClientRegistrationRepository clients, OAuth2AuthorizedClientRepository authz) { var oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(clients, authz); - return WebClient.builder().filter(oauth2).build(); + return WebClient.builder() + .filter(oauth2) + .build(); } } diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SecurityConfiguration.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SecurityConfiguration.java index dea34af..c8e14c5 100644 --- a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SecurityConfiguration.java +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SecurityConfiguration.java @@ -70,6 +70,7 @@ public SecurityConfiguration(HttpCookieOAuth2AuthorizationRequestRepository http public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests((authz) -> { authz.requestMatchers("/api/graphql").authenticated(); + authz.requestMatchers("/api/avatars").authenticated(); authz.requestMatchers("/**").permitAll(); }); diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynOAuth2User.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynOAuth2User.java index bde7af9..29ea797 100644 --- a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynOAuth2User.java +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynOAuth2User.java @@ -45,8 +45,6 @@ public class SvalynOAuth2User implements OAuth2User, IUser { private final String email; - private final String imageUrl; - private final Map attributes; private final Collection authorities; @@ -56,7 +54,6 @@ public SvalynOAuth2User(Account account, OAuth2User oAuth2User, OAuth2AccessToke this.username = account.getUsername(); this.name = account.getName(); this.email = account.getEmail(); - this.imageUrl = account.getImageUrl(); this.attributes = oAuth2User.getAttributes(); var authorities = new ArrayList(); @@ -88,11 +85,6 @@ public String getEmail() { return email; } - @Override - public String getImageUrl() { - return imageUrl; - } - @Override public Map getAttributes() { return this.attributes; diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynOAuth2UserService.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynOAuth2UserService.java index f497384..5a73d29 100644 --- a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynOAuth2UserService.java +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynOAuth2UserService.java @@ -22,6 +22,8 @@ import com.svalyn.studio.domain.account.AccountRole; import com.svalyn.studio.domain.account.OAuth2Metadata; import com.svalyn.studio.domain.account.repositories.IAccountRepository; +import com.svalyn.studio.infrastructure.avatar.api.AvatarData; +import com.svalyn.studio.infrastructure.avatar.api.IAvatarRetrievalService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.oauth2.client.OAuth2AuthorizedClient; @@ -52,12 +54,15 @@ public class SvalynOAuth2UserService extends DefaultOAuth2UserService { private final IAccountRepository accountRepository; + private final IAvatarRetrievalService avatarRetrievalService; + private final WebClient webClient; private final Logger logger = LoggerFactory.getLogger(SvalynOAuth2UserService.class); - public SvalynOAuth2UserService(IAccountRepository accountRepository, WebClient webClient) { + public SvalynOAuth2UserService(IAccountRepository accountRepository, IAvatarRetrievalService avatarRetrievalService, WebClient webClient) { this.accountRepository = Objects.requireNonNull(accountRepository); + this.avatarRetrievalService = Objects.requireNonNull(avatarRetrievalService); this.webClient = Objects.requireNonNull(webClient); } @@ -119,8 +124,8 @@ private Account updateAccount(Account account, OAuth2UserRequest oAuth2UserReque .build(); account.addOAuth2Metadata(oAuth2Metadata); - } else if (!account.getName().equals(oAuth2UserInfo.getName()) || !account.getImageUrl().equals(oAuth2UserInfo.getImageUrl())) { - account.updateDetails(oAuth2UserInfo.getName(), oAuth2UserInfo.getImageUrl()); + } else if (!account.getName().equals(oAuth2UserInfo.getName())) { + account.updateName(oAuth2UserInfo.getName()); } return this.accountRepository.save(account); @@ -134,12 +139,15 @@ private Account createAccount(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInf .providerId(oAuth2UserInfo.getId()) .build(); + var optionalImageData = this.avatarRetrievalService.getData(oAuth2UserInfo.getImageUrl()); + var account = Account.newAccount() .role(AccountRole.USER) .username(oAuth2UserInfo.getUsername()) .name(oAuth2UserInfo.getName()) .email(oAuth2UserInfo.getEmail()) - .imageUrl(oAuth2UserInfo.getImageUrl()) + .image(optionalImageData.map(AvatarData::content).orElse(null)) + .imageContentType(optionalImageData.map(AvatarData::contentType).orElse(null)) .oAuth2Metadata(Set.of(oAuth2Metadata)) .build(); diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynSystemUser.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynSystemUser.java index 52b9a81..f4710bd 100644 --- a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynSystemUser.java +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynSystemUser.java @@ -51,9 +51,4 @@ public String getUsername() { public String getName() { return "System"; } - - @Override - public String getImageUrl() { - return ""; - } } diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynUserDetails.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynUserDetails.java index 7cf5f9c..dbba736 100644 --- a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynUserDetails.java +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynUserDetails.java @@ -38,21 +38,17 @@ public class SvalynUserDetails extends User implements IUser { private final String name; - private final String imageUrl; - - public SvalynUserDetails(UUID id, String name, String imageUrl, String username, String password, Collection authorities) { + public SvalynUserDetails(UUID id, String name, String username, String password, Collection authorities) { super(username, password, authorities); this.id = Objects.requireNonNull(id); this.name = Objects.requireNonNull(name); - this.imageUrl = Objects.requireNonNull(imageUrl); } @SuppressWarnings("checkstyle:ParameterNumber") - public SvalynUserDetails(UUID id, String name, String imageUrl, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection authorities) { + public SvalynUserDetails(UUID id, String name, String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection authorities) { super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities); this.id = Objects.requireNonNull(id); this.name = Objects.requireNonNull(name); - this.imageUrl = Objects.requireNonNull(imageUrl); } @Override @@ -64,9 +60,4 @@ public UUID getId() { public String getName() { return this.name; } - - @Override - public String getImageUrl() { - return this.imageUrl; - } } diff --git a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynUserDetailsService.java b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynUserDetailsService.java index 78277c5..72422cb 100644 --- a/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynUserDetailsService.java +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/security/SvalynUserDetailsService.java @@ -78,7 +78,6 @@ private UserDetails toUserDetails(Account account, String username, String passw return new SvalynUserDetails( account.getId(), account.getName(), - account.getImageUrl(), username, password, true, diff --git a/backend/svalyn-studio-infrastructure/src/main/resources/db/changelog/db.changelog-v2023.8.0.xml b/backend/svalyn-studio-infrastructure/src/main/resources/db/changelog/db.changelog-v2023.8.0.xml index 987f352..eb5fe1d 100644 --- a/backend/svalyn-studio-infrastructure/src/main/resources/db/changelog/db.changelog-v2023.8.0.xml +++ b/backend/svalyn-studio-infrastructure/src/main/resources/db/changelog/db.changelog-v2023.8.0.xml @@ -35,5 +35,16 @@ ALTER TABLE project ADD COLUMN textsearchable_generated tsvector GENERATED ALWAYS AS ( to_tsvector('english', identifier || ' ' || name || '' || description) ) STORED CREATE INDEX project_search_index ON project USING GIN(textsearchable_generated) + + + + + + + + + + + \ No newline at end of file diff --git a/backend/svalyn-studio/src/test/java/com/svalyn/studio/WithMockPrincipalSecurityContextFactory.java b/backend/svalyn-studio/src/test/java/com/svalyn/studio/WithMockPrincipalSecurityContextFactory.java index 4b39d5f..dd26b72 100644 --- a/backend/svalyn-studio/src/test/java/com/svalyn/studio/WithMockPrincipalSecurityContextFactory.java +++ b/backend/svalyn-studio/src/test/java/com/svalyn/studio/WithMockPrincipalSecurityContextFactory.java @@ -63,11 +63,6 @@ public String getUsername() { public String getName() { return account.getName(); } - - @Override - public String getImageUrl() { - return account.getImageUrl(); - } }; securityContext.setAuthentication(new UsernamePasswordAuthenticationToken(user, "password", List.of())); diff --git a/backend/svalyn-studio/src/test/java/com/svalyn/studio/domain/account/AccountIntegrationTests.java b/backend/svalyn-studio/src/test/java/com/svalyn/studio/domain/account/AccountIntegrationTests.java index ff68320..d8c9daa 100644 --- a/backend/svalyn-studio/src/test/java/com/svalyn/studio/domain/account/AccountIntegrationTests.java +++ b/backend/svalyn-studio/src/test/java/com/svalyn/studio/domain/account/AccountIntegrationTests.java @@ -60,7 +60,6 @@ public void givenAnAccount_whenPersisted_thenHasAnId() { .username("username") .name("John Doe") .email("john.doe@example.org") - .imageUrl("https://www.example.org/image/avatar.png") .build(); var savedAccount = this.accountRepository.save(account); assertThat(savedAccount.getId()).isNotNull(); @@ -74,7 +73,6 @@ public void givenAnAccount_whenPersisted_thenAnEventIsPublished() { .username("username") .name("John Doe") .email("john.doe@example.org") - .imageUrl("https://www.example.org/image/avatar.png") .build(); this.accountRepository.save(account); assertThat(this.domainEvents.getDomainEvents().stream().filter(AccountCreatedEvent.class::isInstance).count()).isEqualTo(1); @@ -88,12 +86,11 @@ public void givenAnAccount_whenDetailsUpdated_thenAnEventIsPublished() { .username("username") .name("John Doe") .email("john.doe@example.org") - .imageUrl("https://www.example.org/image/avatar.png") .build(); this.accountRepository.save(account); account = this.accountRepository.findById(account.getId()).get(); - account.updateDetails("John T. Doe", "https://www.example.org/image/avatar.png+v2"); + account.updateName("John T. Doe"); this.accountRepository.save(account); assertThat(this.domainEvents.getDomainEvents().stream().filter(AccountModifiedEvent.class::isInstance).count()).isEqualTo(1); } diff --git a/backend/svalyn-studio/src/test/resources/scripts/initialize.sql b/backend/svalyn-studio/src/test/resources/scripts/initialize.sql index 8bd29f6..64e1360 100644 --- a/backend/svalyn-studio/src/test/resources/scripts/initialize.sql +++ b/backend/svalyn-studio/src/test/resources/scripts/initialize.sql @@ -1,17 +1,17 @@ -INSERT INTO account (id, role, username, name, email, image_url, created_on, last_modified_on) VALUES -('7ba7bda7-13b9-422a-838b-e45a3597e952', 'USER', 'johndoe', 'John Doe', 'johndoe@example.org', 'https://www.example.org/image/avatar.png', '2022-09-17 21:37:52.616', '2022-09-17 21:37:52.616'); +INSERT INTO account (id, role, username, name, email, created_on, last_modified_on) VALUES +('7ba7bda7-13b9-422a-838b-e45a3597e952', 'USER', 'johndoe', 'John Doe', 'johndoe@example.org', '2022-09-17 21:37:52.616', '2022-09-17 21:37:52.616'); -INSERT INTO account (id, role, username, name, email, image_url, created_on, last_modified_on) VALUES -('1116f75f-2ceb-43cf-b6a6-c11dabbc5977', 'USER', 'janedoe', 'Jane Doe', 'janedoe@example.org', 'https://www.example.org/image/avatar.png', '2022-09-25 14:25:52.616', '2022-09-25 14:25:52.616'); +INSERT INTO account (id, role, username, name, email, created_on, last_modified_on) VALUES +('1116f75f-2ceb-43cf-b6a6-c11dabbc5977', 'USER', 'janedoe', 'Jane Doe', 'janedoe@example.org', '2022-09-25 14:25:52.616', '2022-09-25 14:25:52.616'); -INSERT INTO account (id, role, username, name, email, image_url, created_on, last_modified_on) VALUES -('5e45aead-48f2-462b-a50e-1191ace697bd', 'USER', 'julesdoe', 'Jules Doe', 'julesdoe@example.org', 'https://www.example.org/image/avatar.png', '2022-09-27 08:25:52.616', '2022-09-27 08:25:52.616'); +INSERT INTO account (id, role, username, name, email, created_on, last_modified_on) VALUES +('5e45aead-48f2-462b-a50e-1191ace697bd', 'USER', 'julesdoe', 'Jules Doe', 'julesdoe@example.org', '2022-09-27 08:25:52.616', '2022-09-27 08:25:52.616'); -INSERT INTO account (id, role, username, name, email, image_url, created_on, last_modified_on) VALUES -('a685c7d7-b4a4-4d58-8f76-ef05e6392fe4', 'USER', 'jamesdoe', 'James Doe', 'jamesdoe@example.org', 'https://www.example.org/image/avatar.png', '2022-09-28 17:29:52.616', '2022-09-28 17:29:52.616'); +INSERT INTO account (id, role, username, name, email, created_on, last_modified_on) VALUES +('a685c7d7-b4a4-4d58-8f76-ef05e6392fe4', 'USER', 'jamesdoe', 'James Doe', 'jamesdoe@example.org', '2022-09-28 17:29:52.616', '2022-09-28 17:29:52.616'); -INSERT INTO account (id, role, username, name, email, image_url, created_on, last_modified_on) VALUES -('4164c661-e0cb-4071-b25d-8516440bb8e8', 'ADMIN', 'admin', 'Admin', 'admin@example.org', 'https://www.example.org/image/avatar.png', '2022-11-12 18:29:52.616', '2022-11-12 18:29:52.616'); +INSERT INTO account (id, role, username, name, email, created_on, last_modified_on) VALUES +('4164c661-e0cb-4071-b25d-8516440bb8e8', 'ADMIN', 'admin', 'Admin', 'admin@example.org', '2022-11-12 18:29:52.616', '2022-11-12 18:29:52.616'); INSERT INTO organization (id, identifier, name, created_by, created_on, last_modified_by, last_modified_on) VALUES