diff --git a/backend/src/docs/asciidoc/admin.adoc b/backend/src/docs/asciidoc/admin.adoc new file mode 100644 index 00000000..3d2137c4 --- /dev/null +++ b/backend/src/docs/asciidoc/admin.adoc @@ -0,0 +1,29 @@ +== 관리자 기능 + +=== 전체 회원 조회 + +operation::admin-controller-test/find-all-member-details[snippets='http-request,http-response'] + +=== 회원 상세 조회 + +operation::admin-controller-test/find-member[snippets='http-request,http-response'] + +=== 회원 차단(삭제) + +operation::admin-controller-test/delete-member[snippets='http-request,http-response'] + +=== 토픽 삭제 + +operation::admin-controller-test/delete-topic[snippets='http-request,http-response'] + +=== 토픽 이미지 삭제 + +operation::admin-controller-test/delete-topic-image[snippets='http-request,http-response'] + +=== 핀 삭제 + +operation::admin-controller-test/delete-pin[snippets='http-request,http-response'] + +=== 핀 이미지 삭제 + +operation::admin-controller-test/delete-pin-image[snippets='http-request,http-response'] \ No newline at end of file diff --git a/backend/src/docs/asciidoc/index.adoc b/backend/src/docs/asciidoc/index.adoc index c15272d2..a90d89df 100644 --- a/backend/src/docs/asciidoc/index.adoc +++ b/backend/src/docs/asciidoc/index.adoc @@ -15,3 +15,4 @@ include::member.adoc[] include::permission.adoc[] include::oauth.adoc[] include::bookmark.adoc[] +include::admin.adoc[] diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java new file mode 100644 index 00000000..0dfaf518 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminCommandService.java @@ -0,0 +1,127 @@ +package com.mapbefine.mapbefine.admin.application; + +import static com.mapbefine.mapbefine.permission.exception.PermissionErrorCode.PERMISSION_FORBIDDEN_BY_NOT_ADMIN; +import static com.mapbefine.mapbefine.topic.exception.TopicErrorCode.TOPIC_NOT_FOUND; + +import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.member.domain.Status; +import com.mapbefine.mapbefine.permission.domain.PermissionRepository; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinImageRepository; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import com.mapbefine.mapbefine.topic.exception.TopicException; +import java.util.List; +import java.util.NoSuchElementException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional +public class AdminCommandService { + + private final MemberRepository memberRepository; + private final TopicRepository topicRepository; + private final PinRepository pinRepository; + private final PinImageRepository pinImageRepository; + private final PermissionRepository permissionRepository; + private final AtlasRepository atlasRepository; + private final BookmarkRepository bookmarkRepository; + + public AdminCommandService( + MemberRepository memberRepository, + TopicRepository topicRepository, + PinRepository pinRepository, + PinImageRepository pinImageRepository, + PermissionRepository permissionRepository, + AtlasRepository atlasRepository, + BookmarkRepository bookmarkRepository + ) { + this.memberRepository = memberRepository; + this.topicRepository = topicRepository; + this.pinRepository = pinRepository; + this.pinImageRepository = pinImageRepository; + this.permissionRepository = permissionRepository; + this.atlasRepository = atlasRepository; + this.bookmarkRepository = bookmarkRepository; + } + + public void blockMember(AuthMember authMember, Long memberId) { + validateAdminPermission(authMember); + + Member member = findMemberById(memberId); + member.updateStatus(Status.BLOCKED); + + deleteAllRelatedMember(member); + } + + private void deleteAllRelatedMember(Member member) { + List pinIds = extractPinIdsByMember(member); + Long memberId = member.getId(); + + pinImageRepository.deleteAllByPinIds(pinIds); + topicRepository.deleteAllByMemberId(memberId); + pinRepository.deleteAllByMemberId(memberId); + permissionRepository.deleteAllByMemberId(memberId); + atlasRepository.deleteAllByMemberId(memberId); + bookmarkRepository.deleteAllByMemberId(memberId); + } + + private Member findMemberById(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("findMemberByAuthMember; member not found; id=" + id)); + } + + private void validateAdminPermission(AuthMember authMember) { + if (authMember.isRole(Role.ADMIN)) { + return; + } + + throw new PermissionForbiddenException(PERMISSION_FORBIDDEN_BY_NOT_ADMIN); + } + + private List extractPinIdsByMember(Member member) { + return member.getCreatedPins() + .stream() + .map(Pin::getId) + .toList(); + } + + public void deleteTopic(AuthMember authMember, Long topicId) { + validateAdminPermission(authMember); + + topicRepository.deleteById(topicId); + } + + public void deleteTopicImage(AuthMember authMember, Long topicId) { + validateAdminPermission(authMember); + + Topic topic = findTopicById(topicId); + topic.removeImage(); + } + + private Topic findTopicById(Long topicId) { + return topicRepository.findById(topicId) + .orElseThrow(() -> new TopicException.TopicNotFoundException(TOPIC_NOT_FOUND, List.of(topicId))); + } + + public void deletePin(AuthMember authMember, Long pinId) { + validateAdminPermission(authMember); + + pinRepository.deleteById(pinId); + } + + public void deletePinImage(AuthMember authMember, Long pinImageId) { + validateAdminPermission(authMember); + + pinImageRepository.deleteById(pinImageId); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java new file mode 100644 index 00000000..f7ca346a --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/application/AdminQueryService.java @@ -0,0 +1,61 @@ +package com.mapbefine.mapbefine.admin.application; + +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.permission.exception.PermissionErrorCode; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.topic.domain.Topic; +import java.util.List; +import java.util.NoSuchElementException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@Transactional(readOnly = true) +public class AdminQueryService { + + private final MemberRepository memberRepository; + + public AdminQueryService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + public List findAllMemberDetails(AuthMember authMember) { + validateAdminPermission(authMember); + + List members = memberRepository.findAllByMemberInfoRole(Role.USER); + + return members.stream() + .map(AdminMemberResponse::from) + .toList(); + } + + private Member findMemberById(Long id) { + return memberRepository.findById(id) + .orElseThrow(() -> new NoSuchElementException("findMemberByAuthMember; member not found; id=" + id)); + } + + private void validateAdminPermission(AuthMember authMember) { + if (authMember.isRole(Role.ADMIN)) { + return; + } + + throw new PermissionForbiddenException(PermissionErrorCode.PERMISSION_FORBIDDEN_BY_NOT_ADMIN); + } + + public AdminMemberDetailResponse findMemberDetail(AuthMember authMember, Long memberId) { + validateAdminPermission(authMember); + + Member findMember = findMemberById(memberId); + List topics = findMember.getCreatedTopics(); + List pins = findMember.getCreatedPins(); + + return AdminMemberDetailResponse.of(findMember, topics, pins); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java new file mode 100644 index 00000000..fbd5e04d --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberDetailResponse.java @@ -0,0 +1,48 @@ +package com.mapbefine.mapbefine.admin.dto; + +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberInfo; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.dto.response.PinResponse; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.time.LocalDateTime; +import java.util.List; + +public record AdminMemberDetailResponse( + Long id, + String nickName, + String email, + String imageUrl, + List topics, + List pins, + LocalDateTime updatedAt +) { + + // TODO: 2023/09/12 topics, pins 모두 member를 통해 얻어올 수 있다. Service에서 꺼내서 넘겨줄 것인가 ? 아니면 DTO에서 꺼내올 것인가 ? + public static AdminMemberDetailResponse of( + Member member, + List topics, + List pins + ) { + MemberInfo memberInfo = member.getMemberInfo(); + List topicResponses = topics.stream() + .map(TopicResponse::fromGuestQuery) + .toList(); + List pinResponses = pins.stream() + .map(PinResponse::from) + .toList(); + + return new AdminMemberDetailResponse( + member.getId(), + memberInfo.getNickName(), + memberInfo.getEmail(), + memberInfo.getImageUrl(), + topicResponses, + pinResponses, + member.getUpdatedAt() + ); + } + + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java new file mode 100644 index 00000000..f041f8f1 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/dto/AdminMemberResponse.java @@ -0,0 +1,27 @@ +package com.mapbefine.mapbefine.admin.dto; + +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberInfo; +import java.time.LocalDateTime; + +public record AdminMemberResponse( + Long id, + String nickName, + String email, + String imageUrl, + LocalDateTime updatedAt +) { + + public static AdminMemberResponse from(Member member) { + MemberInfo memberInfo = member.getMemberInfo(); + + return new AdminMemberResponse( + member.getId(), + memberInfo.getNickName(), + memberInfo.getEmail(), + memberInfo.getImageUrl(), + member.getUpdatedAt() + ); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java new file mode 100644 index 00000000..2a34fec1 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/admin/presentation/AdminController.java @@ -0,0 +1,78 @@ +package com.mapbefine.mapbefine.admin.presentation; + +import com.mapbefine.mapbefine.admin.application.AdminCommandService; +import com.mapbefine.mapbefine.admin.application.AdminQueryService; +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin") +public class AdminController { + + private final AdminQueryService adminQueryService; + private final AdminCommandService adminCommandService; + + + public AdminController(AdminQueryService adminQueryService, AdminCommandService adminCommandService) { + this.adminQueryService = adminQueryService; + this.adminCommandService = adminCommandService; + } + + @GetMapping("/members") + public ResponseEntity> findAllMembers(AuthMember authMember) { + List responses = adminQueryService.findAllMemberDetails(authMember); + + return ResponseEntity.ok(responses); + } + + @DeleteMapping("/members/{memberId}") + public ResponseEntity deleteMember(AuthMember authMember, @PathVariable Long memberId) { + adminCommandService.blockMember(authMember, memberId); + + return ResponseEntity.noContent().build(); + } + + @GetMapping("/members/{memberId}") + public ResponseEntity findMember(AuthMember authMember, @PathVariable Long memberId) { + AdminMemberDetailResponse response = adminQueryService.findMemberDetail(authMember, memberId); + + return ResponseEntity.ok(response); + } + + @DeleteMapping("/topics/{topicId}") + public ResponseEntity deleteTopic(AuthMember authMember, @PathVariable Long topicId) { + adminCommandService.deleteTopic(authMember, topicId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/topics/{topicId}/images") + public ResponseEntity deleteTopicImage(AuthMember authMember, @PathVariable Long topicId) { + adminCommandService.deleteTopicImage(authMember, topicId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/pins/{pinId}") + public ResponseEntity deletePin(AuthMember authMember, @PathVariable Long pinId) { + adminCommandService.deletePin(authMember, pinId); + + return ResponseEntity.noContent().build(); + } + + @DeleteMapping("/pins/images/{imageId}") + public ResponseEntity deletePinImage(AuthMember authMember, @PathVariable Long imageId) { + adminCommandService.deletePinImage(authMember, imageId); + + return ResponseEntity.noContent().build(); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java index 242d6992..bd37a9ba 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/atlas/domain/AtlasRepository.java @@ -10,4 +10,5 @@ public interface AtlasRepository extends JpaRepository { void deleteByMemberIdAndTopicId(Long memberId, Long topicId); + void deleteAllByMemberId(Long memberId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java index 538e1244..11b2e343 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/application/AuthService.java @@ -8,6 +8,7 @@ import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.List; import java.util.Objects; +import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -60,4 +61,15 @@ private List getCreatedTopics(Member member) { .toList(); } + public boolean isAdmin(Long memberId) { + if (Objects.isNull(memberId)) { + return false; + } + + Optional member = memberRepository.findById(memberId); + + return member.map(Member::isAdmin) + .orElse(false); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java index c795b1ec..d27f7cc2 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/AuthMember.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.auth.domain; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.List; @@ -27,6 +28,8 @@ protected AuthMember( public abstract boolean canPinCreateOrUpdate(Topic topic); + public abstract boolean isRole(Role role); + public Long getMemberId() { return memberId; } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java index 3d97522c..f6a54648 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Admin.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import java.util.Collections; @@ -34,4 +35,9 @@ public boolean canPinCreateOrUpdate(Topic topic) { return true; } + @Override + public boolean isRole(Role role) { + return Role.ADMIN == role; + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java index 6c8c0edc..dcd8dbeb 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/Guest.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicStatus; import java.util.Collections; @@ -36,4 +37,8 @@ public boolean canPinCreateOrUpdate(Topic topic) { return false; } + @Override + public boolean isRole(Role role) { + return Role.GUEST == role; + } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java index d2444d49..ed2ea7b8 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/domain/member/User.java @@ -1,6 +1,7 @@ package com.mapbefine.mapbefine.auth.domain.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.member.domain.Role; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicStatus; import java.util.List; @@ -54,4 +55,9 @@ private boolean hasPermission(Long topicId) { return createdTopic.contains(topicId) || topicsWithPermission.contains(topicId); } + @Override + public boolean isRole(Role role) { + return Role.USER == role; + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java new file mode 100644 index 00000000..7cbf7c20 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthErrorCode.java @@ -0,0 +1,21 @@ +package com.mapbefine.mapbefine.auth.exception; + +import lombok.Getter; + +@Getter +public enum AuthErrorCode { + ILLEGAL_MEMBER_ID("03100", "로그인에 실패하였습니다."), + ILLEGAL_TOKEN("03101", "로그인에 실패하였습니다."), + FORBIDDEN_ADMIN_ACCESS("03102", "로그인에 실패하였습니다."), + BLOCKING_MEMBER_ACCESS("03103", "로그인에 실패하였습니다."), + ; + + private final String code; + private final String message; + + AuthErrorCode(String code, String message) { + this.code = code; + this.message = message; + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java new file mode 100644 index 00000000..4f384f9e --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/auth/exception/AuthException.java @@ -0,0 +1,23 @@ +package com.mapbefine.mapbefine.auth.exception; + +import com.mapbefine.mapbefine.common.exception.ErrorCode; +import com.mapbefine.mapbefine.common.exception.ForbiddenException; +import com.mapbefine.mapbefine.common.exception.UnauthorizedException; + +public class AuthException { + + public static class AuthUnauthorizedException extends UnauthorizedException { + + public AuthUnauthorizedException(AuthErrorCode errorCode) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } + + public static class AuthForbiddenException extends ForbiddenException { + + public AuthForbiddenException(AuthErrorCode errorCode) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage())); + } + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java b/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java index 02cb0062..c972f561 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/config/AuthConfig.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.common.config; +import com.mapbefine.mapbefine.common.interceptor.AdminAuthInterceptor; import com.mapbefine.mapbefine.common.interceptor.AuthInterceptor; import com.mapbefine.mapbefine.common.resolver.AuthArgumentResolver; import java.util.List; @@ -11,20 +12,26 @@ @Configuration public class AuthConfig implements WebMvcConfigurer { + private final AdminAuthInterceptor adminAuthInterceptor; private final AuthInterceptor authInterceptor; private final AuthArgumentResolver authArgumentResolver; public AuthConfig( + AdminAuthInterceptor adminAuthInterceptor, AuthInterceptor authInterceptor, AuthArgumentResolver authArgumentResolver ) { + this.adminAuthInterceptor = adminAuthInterceptor; this.authInterceptor = authInterceptor; this.authArgumentResolver = authArgumentResolver; } @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(authInterceptor); + registry.addInterceptor(authInterceptor) + .excludePathPatterns("/admin/**"); + registry.addInterceptor(adminAuthInterceptor) + .addPathPatterns("/admin/**"); } @Override diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java new file mode 100644 index 00000000..9f5c8424 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AdminAuthInterceptor.java @@ -0,0 +1,71 @@ +package com.mapbefine.mapbefine.common.interceptor; + +import com.mapbefine.mapbefine.auth.application.AuthService; +import com.mapbefine.mapbefine.auth.dto.AuthInfo; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; +import com.mapbefine.mapbefine.auth.exception.AuthException; +import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; +import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.util.Objects; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; + +@Component +public class AdminAuthInterceptor implements HandlerInterceptor { + + private final AuthorizationExtractor authorizationExtractor; + private final AuthService authService; + private final JwtTokenProvider jwtTokenProvider; + + public AdminAuthInterceptor( + AuthorizationExtractor authorizationExtractor, + AuthService authService, + JwtTokenProvider jwtTokenProvider + ) { + this.authorizationExtractor = authorizationExtractor; + this.authService = authService; + this.jwtTokenProvider = jwtTokenProvider; + } + + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) throws Exception { + if (!(handler instanceof HandlerMethod)) { + return true; + } + + Long memberId = extractMemberIdFromToken(request); + + validateAdmin(memberId); + request.setAttribute("memberId", memberId); + + return true; + } + + private Long extractMemberIdFromToken(HttpServletRequest request) { + AuthInfo authInfo = authorizationExtractor.extract(request); + if (Objects.isNull(authInfo)) { + return null; + } + String accessToken = authInfo.accessToken(); + if (jwtTokenProvider.validateToken(accessToken)) { + return Long.parseLong(jwtTokenProvider.getPayload(accessToken)); + } + throw new AuthException.AuthUnauthorizedException(AuthErrorCode.ILLEGAL_TOKEN); + } + + private void validateAdmin(Long memberId) { + if (authService.isAdmin(memberId)) { + return; + } + + throw new AuthException.AuthForbiddenException(AuthErrorCode.FORBIDDEN_ADMIN_ACCESS); + } + +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java index 32a52d38..01c98c73 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/common/interceptor/AuthInterceptor.java @@ -3,10 +3,11 @@ import com.mapbefine.mapbefine.auth.application.AuthService; import com.mapbefine.mapbefine.auth.domain.AuthMember; import com.mapbefine.mapbefine.auth.dto.AuthInfo; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; +import com.mapbefine.mapbefine.auth.exception.AuthException; +import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; import com.mapbefine.mapbefine.auth.infrastructure.AuthorizationExtractor; import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; -import com.mapbefine.mapbefine.common.exception.ErrorCode; -import com.mapbefine.mapbefine.common.exception.UnauthorizedException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.util.Arrays; @@ -18,10 +19,6 @@ @Component public class AuthInterceptor implements HandlerInterceptor { - private static final String UNAUTHORIZED_ERROR_MESSAGE = "로그인에 실패하였습니다."; - private static final ErrorCode ILLEGAL_MEMBER_ID = new ErrorCode("03100", UNAUTHORIZED_ERROR_MESSAGE); - private static final ErrorCode ILLEGAL_TOKEN = new ErrorCode("03101", UNAUTHORIZED_ERROR_MESSAGE); - private final AuthorizationExtractor authorizationExtractor; private final AuthService authService; private final JwtTokenProvider jwtTokenProvider; @@ -65,7 +62,7 @@ private void validateMember(Long memberId) { return; } - throw new UnauthorizedException(ILLEGAL_MEMBER_ID); + throw new AuthUnauthorizedException(AuthErrorCode.ILLEGAL_MEMBER_ID); } private boolean isAuthMemberNotRequired(HandlerMethod handlerMethod) { @@ -86,7 +83,7 @@ private Long extractMemberIdFromToken(HttpServletRequest request) { } String accessToken = authInfo.accessToken(); if (!jwtTokenProvider.validateToken(accessToken)) { - throw new UnauthorizedException(ILLEGAL_TOKEN); + throw new AuthException.AuthUnauthorizedException(AuthErrorCode.ILLEGAL_TOKEN); } return Long.parseLong(jwtTokenProvider.getPayload(accessToken)); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java index c032cf36..df5296f9 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/application/MemberQueryService.java @@ -37,6 +37,7 @@ private Member findMemberById(Long id) { .orElseThrow(() -> new MemberNotFoundException(MemberErrorCode.MEMBER_NOT_FOUND, id)); } + // TODO: 2023/09/13 차단된 or 탈퇴한 사용자 필터링 필요 public List findAll() { return memberRepository.findAll() .stream() diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java index 71de6abf..1d64325e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Member.java @@ -64,13 +64,15 @@ public static Member of( String email, String imageUrl, Role role, + Status status, OauthId oauthId ) { MemberInfo memberInfo = MemberInfo.of( nickName, email, imageUrl, - role + role, + status ); return new Member(memberInfo, oauthId); @@ -85,7 +87,7 @@ public static Member ofRandomNickname( ) { String nickName = createNickname(nickname); - return Member.of(nickName, email, imageUrl, role, oauthId); + return Member.of(nickName, email, imageUrl, role, Status.NORMAL, oauthId); } private static String createNickname(String nickname) { @@ -111,7 +113,8 @@ public void update( nickName, email, imageUrl, - memberInfo.getRole() + memberInfo.getRole(), + memberInfo.getStatus() ); } @@ -149,4 +152,17 @@ public List getTopicsWithPermissions() { .toList(); } + public boolean isNormalStatus() { + return memberInfo.getStatus() == Status.NORMAL; + } + + public void updateStatus(Status status) { + memberInfo = MemberInfo.of( + memberInfo.getNickName(), + memberInfo.getEmail(), + memberInfo.getImageUrl(), + memberInfo.getRole(), + status + ); + } } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java index c15054c3..3cc5dfa4 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberInfo.java @@ -16,6 +16,7 @@ import java.util.Objects; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.ColumnDefault; @Embeddable @NoArgsConstructor(access = PROTECTED) @@ -38,33 +39,43 @@ public class MemberInfo { @Column(nullable = false) private Role role; + @Enumerated(EnumType.STRING) + @ColumnDefault(value = "NORMAL") + @Column(nullable = false) + private Status status; + private MemberInfo( String nickName, String email, Image imageUrl, - Role role + Role role, + Status status ) { this.nickName = nickName; this.email = email; this.imageUrl = imageUrl; this.role = role; + this.status = status; } public static MemberInfo of( String nickName, String email, String imageUrl, - Role role + Role role, + Status status ) { validateNickName(nickName); validateEmail(email); validateRole(role); + validateStatus(status); return new MemberInfo( nickName, email, Image.from(imageUrl), - role + role, + status ); } @@ -93,6 +104,12 @@ private static void validateRole(Role role) { } } + private static void validateStatus(Status status) { + if (Objects.isNull(status)) { + throw new IllegalArgumentException("validateStatus; member status is null;"); + } + } + public String getImageUrl() { return imageUrl.getImageUrl(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java index 16b57c05..65125aa1 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/MemberRepository.java @@ -1,5 +1,6 @@ package com.mapbefine.mapbefine.member.domain; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -13,4 +14,6 @@ public interface MemberRepository extends JpaRepository { Optional findByOauthId(OauthId oauthId); + List findAllByMemberInfoRole(Role role); + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java new file mode 100644 index 00000000..bf594c09 --- /dev/null +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/domain/Status.java @@ -0,0 +1,15 @@ +package com.mapbefine.mapbefine.member.domain; + +public enum Status { + NORMAL("STATUS_NORMAL", "정상 사용자"), + DELETE("STATAUS_DELETE", "탈퇴한 사용자"), + BLOCKED("STATUS_BLOCKED", "차단된 사용자"); + + private final String key; + private final String title; + + Status(String key, String title) { + this.key = key; + this.title = title; + } +} diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java index c02d2ab1..b0c5d034 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberErrorCode.java @@ -9,6 +9,7 @@ public enum MemberErrorCode { ILLEGAL_NICKNAME_LENGTH("05001", "닉네임 길이는 최소 1 자에서 20자여야 합니다."), ILLEGAL_EMAIL_NULL("05002", "이메일은 필수로 입력해야합니다."), ILLEGAL_EMAIL_PATTERN("05003", "올바르지 않은 이메일 형식입니다."), + FORBIDDEN_MEMBER_STATUS("05100", "탈퇴 혹은 차단된 회원입니다."), MEMBER_NOT_FOUND("05400", "존재하지 않는 회원입니다."), ; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java index ec98e9d0..89a68948 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/member/exception/MemberException.java @@ -2,6 +2,7 @@ import com.mapbefine.mapbefine.common.exception.BadRequestException; import com.mapbefine.mapbefine.common.exception.ErrorCode; +import com.mapbefine.mapbefine.common.exception.ForbiddenException; import com.mapbefine.mapbefine.common.exception.NotFoundException; public class MemberException { @@ -18,5 +19,11 @@ public MemberNotFoundException(MemberErrorCode errorCode, Long id) { } } + public static class MemberForbiddenException extends ForbiddenException { + public MemberForbiddenException(MemberErrorCode errorCode, Long id) { + super(new ErrorCode<>(errorCode.getCode(), errorCode.getMessage(), id)); + } + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java index d75526a0..50681719 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/application/OauthService.java @@ -1,5 +1,7 @@ package com.mapbefine.mapbefine.oauth.application; +import com.mapbefine.mapbefine.auth.exception.AuthErrorCode; +import com.mapbefine.mapbefine.auth.exception.AuthException.AuthUnauthorizedException; import com.mapbefine.mapbefine.auth.infrastructure.JwtTokenProvider; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; @@ -39,6 +41,8 @@ public LoginInfoResponse login(OauthServerType oauthServerType, String code) { Member savedMember = memberRepository.findByOauthId(oauthMember.getOauthId()) .orElseGet(() -> register(oauthMember)); + validateMemberStatus(savedMember); + String accessToken = jwtTokenProvider.createToken(String.valueOf(savedMember.getId())); return LoginInfoResponse.of(accessToken, savedMember); @@ -49,4 +53,12 @@ private Member register(OauthMember oauthMember) { return memberRepository.save(oauthMember.toRegisterMember()); } + private void validateMemberStatus(Member member) { + if (member.isNormalStatus()) { + return; + } + + throw new AuthUnauthorizedException(AuthErrorCode.BLOCKING_MEMBER_ACCESS); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java index a2a3c518..2ad2295d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/AuthCodeRequestUrlProviderComposite.java @@ -4,7 +4,7 @@ import static java.util.function.UnaryOperator.identity; import static java.util.stream.Collectors.toMap; -import com.mapbefine.mapbefine.oauth.exception.OathException.OauthNotFoundException; +import com.mapbefine.mapbefine.oauth.exception.OauthException.OauthNotFoundException; import java.util.Map; import java.util.Optional; import java.util.Set; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java index f5fb9648..eba1172d 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/domain/OauthMemberClientComposite.java @@ -4,7 +4,7 @@ import static java.util.function.UnaryOperator.identity; import static java.util.stream.Collectors.toMap; -import com.mapbefine.mapbefine.oauth.exception.OathException.OauthNotFoundException; +import com.mapbefine.mapbefine.oauth.exception.OauthException.OauthNotFoundException; import java.util.Map; import java.util.Optional; import java.util.Set; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java b/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java similarity index 94% rename from backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java rename to backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java index a18d234c..3dc67e35 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OathException.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/oauth/exception/OauthException.java @@ -4,7 +4,7 @@ import com.mapbefine.mapbefine.common.exception.NotFoundException; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; -public class OathException { +public class OauthException { public static class OauthNotFoundException extends NotFoundException { public OauthNotFoundException(OauthErrorCode errorCode, OauthServerType oauthServerType) { diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java index da8dca9b..7d14c061 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/domain/PermissionRepository.java @@ -9,4 +9,5 @@ public interface PermissionRepository extends JpaRepository { boolean existsByTopicIdAndMemberId(Long topicId, Long memberId); + void deleteAllByMemberId(Long memberId); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java b/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java index 5a0f6aa8..c3f3f699 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/permission/exception/PermissionErrorCode.java @@ -9,7 +9,8 @@ public enum PermissionErrorCode { ILLEGAL_PERMISSION_ID("07001", "유효하지 않은 권한 정보입니다."), FORBIDDEN_ADD_PERMISSION_GUEST("07300", "로그인하지 않은 사용자는 권한을 줄 수 없습니다."), FORBIDDEN_ADD_PERMISSION("07301", "지도를 생성한 사용자가 아니면 권한을 줄 수 없습니다."), - PERMISSION_NOT_FOUND("07400", "존재하지 않는 권한 정보입니다.") + PERMISSION_NOT_FOUND("07400", "존재하지 않는 권한 정보입니다."), + PERMISSION_FORBIDDEN_BY_NOT_ADMIN("07401", "어드민 계정만 접근 가능합니다."), ; private final String code; diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java index 6956b55d..0e2aa2bf 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/application/PinQueryService.java @@ -52,7 +52,7 @@ private void validateReadAuth(AuthMember member, Topic topic) { } public List findAllPinsByMemberId(AuthMember authMember, Long memberId) { - return pinRepository.findByCreatorId(memberId) + return pinRepository.findAllByCreatorId(memberId) .stream() .filter(pin -> authMember.canRead(pin.getTopic())) .map(PinResponse::from) diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java index 36f53800..6e25d0af 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinImageRepository.java @@ -18,6 +18,9 @@ public interface PinImageRepository extends JpaRepository { @Query("update PinImage p set p.isDeleted = true where p.id = :id") void deleteById(@Param("id") Long id); + @Modifying(clearAutomatically = true) + @Query("update PinImage p set p.isDeleted = true where p.pin.id in :pinIds") + void deleteAllByPinIds(@Param("pinIds") List pinIds); + List findAllByPinId(Long pinId); - } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java index 11a2d95d..60d29dc9 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/pin/domain/PinRepository.java @@ -18,9 +18,13 @@ public interface PinRepository extends JpaRepository { @Query("update Pin p set p.isDeleted = true where p.id = :pinId") void deleteById(@Param("pinId") Long pinId); + @Modifying(clearAutomatically = true) + @Query("update Pin p set p.isDeleted = true where p.creator.id = :memberId") + void deleteAllByMemberId(@Param("memberId") Long memberId); + List findAllByTopicId(Long topicId); - List findByCreatorId(Long creatorId); + List findAllByCreatorId(Long creatorId); List findAllByOrderByUpdatedAtDesc(); } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java index aa19d281..de0efdda 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/application/TopicQueryService.java @@ -186,7 +186,7 @@ private void validateReadableTopics(AuthMember member, List topics) { public List findAllTopicsByMemberId(AuthMember authMember, Long memberId) { if (Objects.isNull(authMember.getMemberId())) { - return topicRepository.findByCreatorId(memberId) + return topicRepository.findAllByCreatorId(memberId) .stream() .filter(authMember::canRead) .map(TopicResponse::fromGuestQuery) @@ -198,7 +198,7 @@ public List findAllTopicsByMemberId(AuthMember authMember, Long m List topicsInAtlas = findTopicsInAtlas(member); List topicsInBookMark = findBookMarkedTopics(member); - return topicRepository.findByCreatorId(memberId) + return topicRepository.findAllByCreatorId(memberId) .stream() .filter(authMember::canRead) .map(topic -> TopicResponse.from( diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java index a41c807e..d5128022 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/Topic.java @@ -114,4 +114,8 @@ public int countBookmarks() { return bookmarks.size(); } + public void removeImage() { + this.topicInfo = topicInfo.removeImage(); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java index 3f5ffd59..dff5da7e 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicInfo.java @@ -88,4 +88,13 @@ private static Image createImage(String imageUrl) { public String getImageUrl() { return image.getImageUrl(); } + + public TopicInfo removeImage() { + return new TopicInfo( + this.name, + this.description, + DEFAULT_IMAGE + ); + } + } diff --git a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java index 1ef9fb6f..e6eb95db 100644 --- a/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java +++ b/backend/src/main/java/com/mapbefine/mapbefine/topic/domain/TopicRepository.java @@ -16,8 +16,12 @@ public interface TopicRepository extends JpaRepository { @Query("update Topic t set t.isDeleted = true where t.id = :topicId") void deleteById(@Param("topicId") Long topicId); + @Modifying(clearAutomatically = true) + @Query("update Topic t set t.isDeleted = true where t.creator.id = :memberId") + void deleteAllByMemberId(@Param("memberId") Long memberId); + boolean existsById(Long id); - List findByCreatorId(Long creatorId); + List findAllByCreatorId(Long creatorId); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java new file mode 100644 index 00000000..c7d7c26f --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/AdminIntegrationTest.java @@ -0,0 +1,249 @@ +package com.mapbefine.mapbefine.admin; + +import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.http.HttpHeaders.AUTHORIZATION; + +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.common.IntegrationTest; +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.location.domain.LocationRepository; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.PinImageFixture; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinImage; +import com.mapbefine.mapbefine.pin.domain.PinImageRepository; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import io.restassured.common.mapper.TypeRef; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; + +class AdminIntegrationTest extends IntegrationTest { + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private PinRepository pinRepository; + + @Autowired + private LocationRepository locationRepository; + + @Autowired + private PinImageRepository pinImageRepository; + + private Location location; + private Member admin; + private Member member; + private Topic topic; + private Pin pin; + private PinImage pinImage; + + @BeforeEach + void setup() { + admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); + member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); + topic = topicRepository.save(TopicFixture.createByName("topic", member)); + location = locationRepository.save(LocationFixture.create()); + pin = pinRepository.save(PinFixture.create(location, topic, member)); + pinImage = pinImageRepository.save(PinImageFixture.create(pin)); + } + + @Test + @DisplayName("어드민일 경우, 회원을 전체 조회할 수 있다.") + void findAllMembers_Success() { + //given + Member member1 = MemberFixture.create("member1", "member1@gmail.com", Role.USER); + Member member2 = MemberFixture.create("member2", "member2@gmail.com", Role.USER); + memberRepository.save(member1); + memberRepository.save(member2); + + //when + List response = given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members") + .then().log().all() + .extract() + .as(new TypeRef<>() { + }); + + //then + List expected = List.of( + AdminMemberResponse.from(member), + AdminMemberResponse.from(member1), + AdminMemberResponse.from(member2) + ); + + assertThat(response).usingRecursiveComparison() + .ignoringFields("updatedAt") + .isEqualTo(expected); + } + + @Test + @DisplayName("어드민이 아닐 경우, 회원을 전체 조회할 수 없다.") + void findAllMembers_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 회원의 상세 정보를 조회할 수 있다.") + void findMemberDetail_Success() { + //given when + AdminMemberDetailResponse response = given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members/" + member.getId()) + .then().log().all() + .extract() + .as(new TypeRef<>() { + }); + + //then + + AdminMemberDetailResponse expected = AdminMemberDetailResponse.of( + member, + member.getCreatedTopics(), + member.getCreatedPins() + ); + + assertThat(response).usingRecursiveComparison() + .ignoringFields("updatedAt") + .ignoringFields("topics.updatedAt") + .isEqualTo(expected); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 회원의 상세 정보를 조회할 수 없다.") + void findMemberDetail_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .accept(MediaType.APPLICATION_JSON_VALUE) + .when().get("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 회원을 삭제(차단)할 수 있다.") + void deleteMember_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 회원을 삭제(차단)할 수 없다.") + void deleteMember_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/members/" + member.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 토픽을 삭제할 수 있다.") + void deleteTopic_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/topics/" + topic.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 토픽을 삭제할 수 없다.") + void deleteTopic_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/topics/" + topic.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 토픽 이미지를 삭제할 수 있다.") + void deleteTopicImage_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/topics/" + topic.getId() + "/images") + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 토픽 이미지를 삭제할 수 없다.") + void deleteTopicImage_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/topics/" + topic.getId() + "/images") + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 핀을 삭제할 수 있다.") + void deletePin_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/pins/" + pin.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 핀을 삭제할 수 없다.") + void deletePin_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/pins/" + pin.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + + @Test + @DisplayName("어드민일 경우, 특정 핀 이미지를 삭제할 수 있다.") + void deletePinImage_Success() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(admin)) + .when().delete("/admin/pins/images/" + pinImage.getId()) + .then().log().all() + .statusCode(HttpStatus.NO_CONTENT.value()); + } + + @Test + @DisplayName("어드민이 아닐 경우, 특정 핀 이미지를 삭제할 수 없다.") + void deletePinImage_Fail() { + given().log().all() + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeader(member)) + .when().delete("/admin/pins/images/" + pinImage.getId()) + .then().log().all() + .statusCode(HttpStatus.FORBIDDEN.value()); + } + +} diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java new file mode 100644 index 00000000..12e49f77 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminCommandServiceTest.java @@ -0,0 +1,280 @@ +package com.mapbefine.mapbefine.admin.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; + +import com.mapbefine.mapbefine.atlas.domain.Atlas; +import com.mapbefine.mapbefine.atlas.domain.AtlasRepository; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.bookmark.domain.Bookmark; +import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.location.domain.LocationRepository; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.member.domain.Status; +import com.mapbefine.mapbefine.permission.domain.Permission; +import com.mapbefine.mapbefine.permission.domain.PermissionRepository; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.PinImageFixture; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinImage; +import com.mapbefine.mapbefine.pin.domain.PinImageRepository; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicInfo; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +class AdminCommandServiceTest { + + @Autowired + private AdminCommandService adminCommandService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private PinRepository pinRepository; + + @Autowired + private LocationRepository locationRepository; + + @Autowired + private PinImageRepository pinImageRepository; + + @Autowired + private AtlasRepository atlasRepository; + + @Autowired + private PermissionRepository permissionRepository; + + @Autowired + private BookmarkRepository bookmarkRepository; + + private Location location; + private Member admin; + private Member member; + private Topic topic; + private Pin pin; + private PinImage pinImage; + + @BeforeEach + void setup() { + admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); + member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); + topic = topicRepository.save(TopicFixture.createByName("topic", member)); + location = locationRepository.save(LocationFixture.create()); + pin = pinRepository.save(PinFixture.create(location, topic, member)); + pinImage = pinImageRepository.save(PinImageFixture.create(pin)); + } + + @DisplayName("Member를 차단(탈퇴시킬)할 경우, Member가 생성한 토픽, 핀, 핀 이미지가 soft-deleting 된다.") + @Test + void blockMember_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + Bookmark bookmark = Bookmark.createWithAssociatedTopicAndMember(topic, member); + Atlas atlas = Atlas.createWithAssociatedMember(topic, member); + Permission permission = Permission.createPermissionAssociatedWithTopicAndMember(topic, member); + + bookmarkRepository.save(bookmark); + atlasRepository.save(atlas); + permissionRepository.save(permission); + + assertAll(() -> { + assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.NORMAL); + assertThat(topic.isDeleted()).isFalse(); + assertThat(pin.isDeleted()).isFalse(); + assertThat(pinImage.isDeleted()).isFalse(); + assertThat(bookmarkRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(); + assertThat(atlasRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isTrue(); + assertThat(permissionRepository.existsByTopicIdAndMemberId(topic.getId(), member.getId())).isTrue(); + }); + + //when + adminCommandService.blockMember(adminAuthMember, member.getId()); + + //then + Topic deletedTopic = topicRepository.findById(topic.getId()).get(); + Pin deletedPin = pinRepository.findById(pin.getId()).get(); + PinImage deletedPinImage = pinImageRepository.findById(pinImage.getId()).get(); + + assertAll(() -> { + assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.BLOCKED); + assertThat(deletedTopic.isDeleted()).isTrue(); + assertThat(deletedPin.isDeleted()).isTrue(); + assertThat(deletedPinImage.isDeleted()).isTrue(); + assertThat(bookmarkRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isFalse(); + assertThat(atlasRepository.existsByMemberIdAndTopicId(member.getId(), topic.getId())).isFalse(); + assertThat(permissionRepository.existsByTopicIdAndMemberId(topic.getId(), member.getId())).isFalse(); + }); + } + + @DisplayName("Admin이 아닐 경우, Member를 차단(탈퇴시킬)할 수 없다.") + @Test + void blockMember_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + Member otherMember = MemberFixture.create("otherMember", "otherMember@email.com", Role.USER); + memberRepository.save(otherMember); + + assertAll(() -> { + assertThat(member.getMemberInfo().getStatus()).isEqualTo(Status.NORMAL); + assertThat(topic.isDeleted()).isFalse(); + assertThat(pin.isDeleted()).isFalse(); + assertThat(pinImage.isDeleted()).isFalse(); + }); + + //when then + Long otherMemberId = otherMember.getId(); + assertThatThrownBy(() -> adminCommandService.blockMember(userAuthMember, otherMemberId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin은 토픽을 삭제시킬 수 있다.") + @Test + void deleteTopic_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + assertThat(topic.isDeleted()).isFalse(); + + //when + adminCommandService.deleteTopic(adminAuthMember, topic.getId()); + + //then + Topic deletedTopic = topicRepository.findById(topic.getId()).get(); + + assertThat(deletedTopic.isDeleted()).isTrue(); + } + + @DisplayName("Admin이 아닐 경우, 토픽을 삭제시킬 수 없다.") + @Test + void deleteTopic_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + assertThat(topic.isDeleted()).isFalse(); + + //when then + Long topicId = topic.getId(); + assertThatThrownBy(() -> adminCommandService.deleteTopic(userAuthMember, topicId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin은 토픽 이미지를 삭제할 수 있다.") + @Test + void deleteTopicImage_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + TopicInfo topicInfo = topic.getTopicInfo(); + + topic.updateTopicInfo(topicInfo.getName(), topicInfo.getDescription(), "https://imageUrl.png"); + + assertThat(topic.getTopicInfo().getImageUrl()).isEqualTo("https://imageUrl.png"); + + //when + adminCommandService.deleteTopicImage(adminAuthMember, topic.getId()); + + //then + Topic imageDeletedTopic = topicRepository.findById(topic.getId()).get(); + + assertThat(imageDeletedTopic.getTopicInfo().getImageUrl()) + .isEqualTo("https://map-befine-official.github.io/favicon.png"); + } + + @DisplayName("Admin이 아닐 경우, 이미지를 삭제할 수 없다.") + @Test + void deleteTopicImage_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + TopicInfo topicInfo = topic.getTopicInfo(); + + topic.updateTopicInfo(topicInfo.getName(), topicInfo.getDescription(), "https://imageUrl.png"); + + assertThat(topic.getTopicInfo().getImageUrl()).isEqualTo("https://imageUrl.png"); + + //when then + Long topicId = topic.getId(); + assertThatThrownBy(() -> adminCommandService.deleteTopicImage(userAuthMember, topicId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin은 핀을 삭제할 수 있다.") + @Test + void deletePin_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + assertThat(pin.isDeleted()).isFalse(); + + //when + adminCommandService.deletePin(adminAuthMember, pin.getId()); + + //then + Pin deletedPin = pinRepository.findById(pin.getId()).get(); + + assertThat(deletedPin.isDeleted()).isTrue(); + } + + @DisplayName("Admin이 아닐 경우, 핀을 삭제할 수 없다.") + @Test + void deletePin_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + assertThat(pin.isDeleted()).isFalse(); + + //when then + Long pinId = pin.getId(); + assertThatThrownBy(() -> adminCommandService.deletePin(userAuthMember, pinId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @DisplayName("Admin인 경우, 핀 이미지를 삭제할 수 있다.") + @Test + void deletePinImage_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + assertThat(pinImage.isDeleted()).isFalse(); + + //when + adminCommandService.deletePinImage(adminAuthMember, pinImage.getId()); + + //then + PinImage deletedPinImage = pinImageRepository.findById(pinImage.getId()).get(); + + assertThat(deletedPinImage.isDeleted()).isTrue(); + } + + @DisplayName("Admin이 아닐 경우, 핀 이미지를 삭제할 수 없다.") + @Test + void deletePinImage_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + assertThat(pinImage.isDeleted()).isFalse(); + + //when then + Long pinImageId = pinImage.getId(); + assertThatThrownBy(() -> adminCommandService.deletePinImage(userAuthMember, pinImageId)) + .isInstanceOf(PermissionForbiddenException.class); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java new file mode 100644 index 00000000..33599110 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/application/AdminQueryServiceTest.java @@ -0,0 +1,135 @@ +package com.mapbefine.mapbefine.admin.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.common.annotation.ServiceTest; +import com.mapbefine.mapbefine.location.LocationFixture; +import com.mapbefine.mapbefine.location.domain.Location; +import com.mapbefine.mapbefine.location.domain.LocationRepository; +import com.mapbefine.mapbefine.member.MemberFixture; +import com.mapbefine.mapbefine.member.domain.Member; +import com.mapbefine.mapbefine.member.domain.MemberRepository; +import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.permission.exception.PermissionException.PermissionForbiddenException; +import com.mapbefine.mapbefine.pin.PinFixture; +import com.mapbefine.mapbefine.pin.domain.Pin; +import com.mapbefine.mapbefine.pin.domain.PinRepository; +import com.mapbefine.mapbefine.topic.TopicFixture; +import com.mapbefine.mapbefine.topic.domain.Topic; +import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +@ServiceTest +class AdminQueryServiceTest { + + @Autowired + private AdminQueryService adminQueryService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private TopicRepository topicRepository; + + @Autowired + private PinRepository pinRepository; + + @Autowired + private LocationRepository locationRepository; + + private Location location; + private Member admin; + private Member member; + private Topic topic; + private Pin pin; + + @BeforeEach + void setup() { + admin = memberRepository.save(MemberFixture.create("Admin", "admin@naver.com", Role.ADMIN)); + member = memberRepository.save(MemberFixture.create("member", "member@gmail.com", Role.USER)); + topic = topicRepository.save(TopicFixture.createByName("topic", member)); + location = locationRepository.save(LocationFixture.create()); + pin = pinRepository.save(PinFixture.create(location, topic, member)); + } + + @Test + @DisplayName("사용자와 관련된 세부(민감한 정보 포함) 정보를 모두 조회할 수 있다.") + void findMemberDetail_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + //when + AdminMemberDetailResponse response = adminQueryService.findMemberDetail(adminAuthMember, member.getId()); + + //then + assertThat(response).usingRecursiveComparison() + .ignoringFields("updatedAt") + .isEqualTo(AdminMemberDetailResponse.of(member, List.of(topic), List.of(pin))); + } + + @Test + @DisplayName("Admin이 아닌 경우, 사용자와 관련된 세부(민감한 정보 포함) 정보를 모두 조회할 수 없다.") + void findMemberDetail_Fail() { + //given + AuthMember userAuthMember = MemberFixture.createUser(member); + + //when //then + Long memberId = member.getId(); + assertThatThrownBy(() -> adminQueryService.findMemberDetail(userAuthMember, memberId)) + .isInstanceOf(PermissionForbiddenException.class); + } + + @Test + @DisplayName("모든 사용자와 관련된 세부 정보를 모두 조회할 수 있다.") + void findAllMemberDetails_Success() { + //given + AuthMember adminAuthMember = MemberFixture.createUser(admin); + + ArrayList members = new ArrayList<>(); + members.add(member); + for (int i = 0; i < 10; i++) { + Member saveMember = MemberFixture.create("member" + i, "member" + i + "@email.com", Role.USER); + members.add(memberRepository.save(saveMember)); + } + + //when + List responses = adminQueryService.findAllMemberDetails(adminAuthMember); + //then + List expected = members.stream() + .map(AdminMemberResponse::from) + .toList(); + + assertThat(responses).usingRecursiveComparison() + .ignoringFields("updatedAt") + .ignoringCollectionOrderInFields() + .isEqualTo(expected); + } + + @Test + @DisplayName("Admin이 아닌 경우 모든 사용자와 관련된 세부 정보를 모두 조회할 수 없다.") + void findAllMemberDetails_Fail() { + //given + AuthMember memberAuthMember = MemberFixture.createUser(member); + + ArrayList members = new ArrayList<>(); + members.add(member); + for (int i = 0; i < 10; i++) { + Member saveMember = MemberFixture.create("member" + i, "member" + i + "@email.com", Role.USER); + members.add(memberRepository.save(saveMember)); + } + + //when //then + assertThatThrownBy(() -> adminQueryService.findAllMemberDetails(memberAuthMember)) + .isInstanceOf(PermissionForbiddenException.class); + } + +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java b/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java new file mode 100644 index 00000000..edceb5d3 --- /dev/null +++ b/backend/src/test/java/com/mapbefine/mapbefine/admin/presentation/AdminControllerTest.java @@ -0,0 +1,172 @@ +package com.mapbefine.mapbefine.admin.presentation; + +import static org.apache.http.HttpHeaders.AUTHORIZATION; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doNothing; + +import com.mapbefine.mapbefine.admin.application.AdminCommandService; +import com.mapbefine.mapbefine.admin.application.AdminQueryService; +import com.mapbefine.mapbefine.admin.dto.AdminMemberDetailResponse; +import com.mapbefine.mapbefine.admin.dto.AdminMemberResponse; +import com.mapbefine.mapbefine.common.RestDocsIntegration; +import com.mapbefine.mapbefine.common.interceptor.AdminAuthInterceptor; +import com.mapbefine.mapbefine.pin.dto.response.PinResponse; +import com.mapbefine.mapbefine.topic.dto.response.TopicResponse; +import java.time.LocalDateTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +class AdminControllerTest extends RestDocsIntegration { + + private static final List TOPIC_RESPONSES = List.of(new TopicResponse( + 1L, + "준팍의 또 토픽", + "https://map-befine-official.github.io/favicon.png", + "준팍", + 3, + Boolean.FALSE, + 5, + Boolean.FALSE, + LocalDateTime.now() + ), new TopicResponse( + 2L, + "준팍의 두번째 토픽", + "https://map-befine-official.github.io/favicon.png", + "준팍", + 5, + Boolean.FALSE, + 3, + Boolean.FALSE, + LocalDateTime.now() + )); + private static final List PIN_RESPONSES = List.of( + new PinResponse( + 1L, + "매튜의 산스장", + "지번 주소", + "매튜가 사랑하는 산스장", + "매튜", + 37, + 127 + ), new PinResponse( + 2L, + "매튜의 안갈집", + "지번 주소", + "매튜가 두번은 안 갈 집", + "매튜", + 37, + 127 + ) + ); + + @MockBean + private AdminCommandService adminCommandService; + + @MockBean + private AdminQueryService adminQueryService; + + @MockBean + private AdminAuthInterceptor adminAuthInterceptor; + + @BeforeEach + void setAll() throws Exception { + given(adminAuthInterceptor.preHandle(any(), any(), any())).willReturn(true); + } + + @DisplayName("멤버 목록 조회") + @Test + void findAllMemberDetails() throws Exception { + List response = List.of( + new AdminMemberResponse(1L, "쥬니", "zuny@naver.com", "https://zuny.png", LocalDateTime.now()), + new AdminMemberResponse(2L, "세인", "semin@naver.com", "https://semin.png", LocalDateTime.now()) + ); + + given(adminQueryService.findAllMemberDetails(any())).willReturn(response); + + mockMvc.perform( + MockMvcRequestBuilders.get("/admin/members") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("멤버 상세 조회") + @Test + void findMember() throws Exception { + AdminMemberDetailResponse response = new AdminMemberDetailResponse( + 1L, + "쥬니", + "zuny@naver.com", + "https://image.png", + TOPIC_RESPONSES, + PIN_RESPONSES, + LocalDateTime.now() + ); + + given(adminQueryService.findMemberDetail(any(), any())).willReturn(response); + + mockMvc.perform( + MockMvcRequestBuilders.get("/admin/members/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("멤버 차단(블랙리스트)") + @Test + void deleteMember() throws Exception { + doNothing().when(adminCommandService).blockMember(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/members/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("토픽 삭제") + @Test + void deleteTopic() throws Exception { + doNothing().when(adminCommandService).deleteTopic(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/topics/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("토픽 이미지 삭제") + @Test + void deleteTopicImage() throws Exception { + doNothing().when(adminCommandService).deleteTopicImage(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/topics/1/images") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("핀 삭제") + @Test + void deletePin() throws Exception { + doNothing().when(adminCommandService).deletePin(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/pins/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } + + @DisplayName("토픽 이미지 삭제") + @Test + void deletePinImage() throws Exception { + doNothing().when(adminCommandService).deletePinImage(any(), any()); + + mockMvc.perform( + MockMvcRequestBuilders.delete("/admin/pins/images/1") + .header(AUTHORIZATION, testAuthHeaderProvider.createAuthHeaderById(1L)) + ).andDo(restDocs.document()); + } +} \ No newline at end of file diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java index d6943a99..6fd7bc14 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/MemberFixture.java @@ -1,10 +1,12 @@ package com.mapbefine.mapbefine.member; import com.mapbefine.mapbefine.auth.domain.AuthMember; +import com.mapbefine.mapbefine.auth.domain.member.Admin; import com.mapbefine.mapbefine.auth.domain.member.User; import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.OauthId; import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.member.domain.Status; import com.mapbefine.mapbefine.oauth.domain.OauthServerType; import com.mapbefine.mapbefine.topic.domain.Topic; @@ -16,6 +18,7 @@ public static Member create(String name, String email, Role role) { email, "https://map-befine-official.github.io/favicon.png", role, + Status.NORMAL, new OauthId(1L, OauthServerType.KAKAO) ); } @@ -26,11 +29,16 @@ public static Member createWithOauthId(String name, String email, Role role, Oau email, "https://map-befine-official.github.io/favicon.png", role, + Status.NORMAL, oauthId) ; } public static AuthMember createUser(Member member) { + if (member.isAdmin()) { + return new Admin(member.getId()); + } + return new User( member.getId(), member.getCreatedTopics().stream().map(Topic::getId).toList(), diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java index 7c4d4a20..1ed6e737 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberInfoTest.java @@ -22,6 +22,7 @@ class Validate { private final String VALID_EMAIL = "member@naver.com"; private final String VALID_IMAGE_URL = "https://map-befine-official.github.io/favicon.png"; private final Role VALID_ROLE = Role.ADMIN; + private final Status VALID_STATUS = Status.NORMAL; @Test @DisplayName("정확한 값을 입력하면 객체가 생성된다") @@ -31,7 +32,8 @@ void success() { VALID_NICK_NAME, VALID_EMAIL, VALID_IMAGE_URL, - VALID_ROLE + VALID_ROLE, + VALID_STATUS ); //then @@ -52,7 +54,8 @@ void whenNameIsInvalid_thenFail(String invalidNickName) { invalidNickName, VALID_EMAIL, VALID_IMAGE_URL, - VALID_ROLE + VALID_ROLE, + VALID_STATUS )).isInstanceOf(MemberBadRequestException.class); } @@ -67,7 +70,8 @@ void whenEmailIsInvalid_thenFail(String invalidEmail) { VALID_NICK_NAME, invalidEmail, VALID_IMAGE_URL, - VALID_ROLE + VALID_ROLE, + VALID_STATUS )).isInstanceOf(MemberBadRequestException.class); } @@ -81,7 +85,8 @@ void whenImageUrlIsInvalid_thenFail() { VALID_NICK_NAME, VALID_EMAIL, invalidImageUrl, - VALID_ROLE + VALID_ROLE, + VALID_STATUS )).isInstanceOf(ImageBadRequestException.class); } @@ -94,6 +99,7 @@ void whenRoleIsInvalid_thenFail() { VALID_EMAIL, VALID_IMAGE_URL, null + ,VALID_STATUS )).isInstanceOf(IllegalArgumentException.class); } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java index 0ee9b697..7fcb4b10 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/member/domain/MemberTest.java @@ -16,6 +16,7 @@ void createMember_success() { String email = "member@naver.com"; String imageUrl = "https://map-befine-official.github.io/favicon.png"; Role role = Role.ADMIN; + Status status = Status.NORMAL; // when Member member = Member.of( @@ -23,7 +24,9 @@ void createMember_success() { email, imageUrl, role, - new OauthId(1L, OauthServerType.KAKAO)); + status, + new OauthId(1L, OauthServerType.KAKAO) + ); // then assertThat(member).isNotNull(); diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java index ad36bb0d..a8604271 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinImageRepositoryTest.java @@ -14,6 +14,7 @@ import com.mapbefine.mapbefine.topic.TopicFixture; import com.mapbefine.mapbefine.topic.domain.Topic; import com.mapbefine.mapbefine.topic.domain.TopicRepository; +import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; @@ -87,4 +88,30 @@ void deleteAllByPinId_Success() { .extractingResultOf("isDeleted") .containsOnly(true); } + + @Test + @DisplayName("여러 핀을 Id로 삭제하면, 핀 이미지들도 soft-deleting 된다.") + void deleteAllByMemberId_Success() { + //given + Pin otherPin = pinRepository.save(PinFixture.create(pin.getLocation(), topic, member)); + + PinImage pinImage1 = PinImageFixture.create(pin); + PinImage pinImage2 = PinImageFixture.create(otherPin); + pinImageRepository.save(pinImage1); + pinImageRepository.save(pinImage2); + + //when + assertThat(pinImage1.isDeleted()).isFalse(); + assertThat(pinImage2.isDeleted()).isFalse(); + pinImageRepository.deleteAllByPinIds(List.of(pin.getId(),otherPin.getId())); + + //then + assertThat(pinImageRepository.findAllByPinId(pin.getId())) + .extractingResultOf("isDeleted") + .containsOnly(true); + assertThat(pinImageRepository.findAllByPinId(otherPin.getId())) + .extractingResultOf("isDeleted") + .containsOnly(true); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java index 508d938e..24bf7cde 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/pin/domain/PinRepositoryTest.java @@ -3,7 +3,6 @@ import static org.assertj.core.api.Assertions.assertThat; import com.mapbefine.mapbefine.location.LocationFixture; -import com.mapbefine.mapbefine.location.domain.Coordinate; import com.mapbefine.mapbefine.location.domain.Location; import com.mapbefine.mapbefine.location.domain.LocationRepository; import com.mapbefine.mapbefine.member.MemberFixture; @@ -24,10 +23,6 @@ @DataJpaTest class PinRepositoryTest { - private static final Coordinate DEFAULT_COORDINATE = Coordinate.of( - 37.5152933, - 127.1029866 - ); @Autowired private TopicRepository topicRepository; @@ -88,4 +83,49 @@ void deleteAllByTopicId_Success() { .containsOnly(true); } + + @Test + @DisplayName("Member ID로 모든 핀을 soft-delete 할 수 있다.") + void deleteAllByMemberId_Success() { + //given + for (int i = 0; i < 10; i++) { + pinRepository.save(PinFixture.create(location, topic, member)); + } + + //when + assertThat(member.getCreatedPins()).hasSize(10) + .extractingResultOf("isDeleted") + .containsOnly(false); + pinRepository.deleteAllByMemberId(member.getId()); + + //then + List deletedPins = pinRepository.findAllByCreatorId(member.getId()); + assertThat(deletedPins).extractingResultOf("isDeleted") + .containsOnly(true); + } + + @Test + @DisplayName("다른 토픽에 존재하는 핀들이여도, Member ID로 모든 핀을 soft-delete 할 수 있다.") + void deleteAllByMemberIdInOtherTopics_Success() { + //given + Topic otherTopic = TopicFixture.createByName("otherTopic", member); + topicRepository.save(otherTopic); + + for (int i = 0; i < 10; i++) { + pinRepository.save(PinFixture.create(location, topic, member)); + pinRepository.save(PinFixture.create(location, otherTopic, member)); + } + + //when + assertThat(member.getCreatedPins()).hasSize(20) + .extractingResultOf("isDeleted") + .containsOnly(false); + pinRepository.deleteAllByMemberId(member.getId()); + + //then + List deletedPins = pinRepository.findAllByCreatorId(member.getId()); + assertThat(deletedPins).extractingResultOf("isDeleted") + .containsOnly(true); + } + } diff --git a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java index 3280df30..2a783b70 100644 --- a/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java +++ b/backend/src/test/java/com/mapbefine/mapbefine/topic/domain/TopicRepositoryTest.java @@ -6,6 +6,8 @@ import com.mapbefine.mapbefine.member.domain.Member; import com.mapbefine.mapbefine.member.domain.MemberRepository; import com.mapbefine.mapbefine.member.domain.Role; +import com.mapbefine.mapbefine.topic.TopicFixture; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; @@ -25,24 +27,14 @@ class TopicRepositoryTest { @BeforeEach void setUp() { - member = MemberFixture.create("member", "member@naver.com", Role.USER); - memberRepository.save(member); + member = memberRepository.save(MemberFixture.create("member", "member@naver.com", Role.USER)); } @Test @DisplayName("토픽을 삭제하면, soft-deleting 된다.") void deleteById_Success() { //given - Topic topic = Topic.createTopicAssociatedWithCreator( - "토픽", - "토픽설명", - "https://example.com/image.jpg", - Publicity.PUBLIC, - PermissionType.ALL_MEMBERS, - member - ); - - topicRepository.save(topic); + Topic topic = topicRepository.save(TopicFixture.createByName("Topic", member)); assertThat(topic.isDeleted()).isFalse(); @@ -54,4 +46,24 @@ void deleteById_Success() { assertThat(deletedTopic.isDeleted()).isTrue(); } + @Test + @DisplayName("Member Id로 모든 토픽을 삭제하면, soft-deleting 된다.") + void deleteAllByMemberId_Success() { + //given + for (int i = 0; i < 10; i++) { + topicRepository.save(TopicFixture.createByName("topic" + i, member)); + } + assertThat(member.getCreatedTopics()).hasSize(10) + .extractingResultOf("isDeleted") + .containsOnly(false); + + //when + topicRepository.deleteAllByMemberId(member.getId()); + + //then + List deletedTopics = topicRepository.findAllByCreatorId(member.getId()); + assertThat(deletedTopics).hasSize(10) + .extractingResultOf("isDeleted") + .containsOnly(true); + } }