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..743c573 --- /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() != 80) { + 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 0000000..e5aa246 Binary files /dev/null and b/backend/svalyn-studio-application/src/main/resources/images/avatar.png differ diff --git a/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/Account.java b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/Account.java index d63a2b2..3c0c2e3 100644 --- a/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/Account.java +++ b/backend/svalyn-studio-domain/src/main/java/com/svalyn/studio/domain/account/Account.java @@ -64,7 +64,9 @@ public class Account extends AbstractValidatingAggregateRoot 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..4b0f44f --- /dev/null +++ b/backend/svalyn-studio-infrastructure/src/main/java/com/svalyn/studio/infrastructure/avatar/AvatarRetrievalService.java @@ -0,0 +1,93 @@ +/* + * 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 dataBuffers = this.webClient.get() + .uri(imageUrl) + .exchangeToFlux(clientResponse -> clientResponse.body(BodyExtractors.toDataBuffers())) + .doOnError(error -> this.logger.warn(error.getMessage(), error)) + .doFinally(signalType -> { + try { + outputStream.close(); + } catch (IOException exception) { + this.logger.warn(exception.getMessage(), exception); + } + }); + + DataBufferUtils.write(dataBuffers, outputStream).subscribe(DataBufferUtils.releaseConsumer()); + + var optionsClientResponse = this.webClient.get() + .uri(imageUrl) + .retrieve() + .toBodilessEntity() + .block(); + + var contentType = Optional.ofNullable(optionsClientResponse) + .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/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); }