Skip to content

Commit

Permalink
feat: API를 통한 사용자 권한 관리 및 회원 정보 제공 완료 (#465)
Browse files Browse the repository at this point in the history
Co-authored-by: GwanSik Kim <[email protected]>
  • Loading branch information
limehee and gwansikk authored Aug 17, 2024
1 parent c2bcc0b commit 8868240
Show file tree
Hide file tree
Showing 17 changed files with 312 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package page.clab.api.domain.memberManagement.member.adapter.in.web;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import page.clab.api.domain.memberManagement.member.application.dto.response.MemberRoleInfoResponseDto;
import page.clab.api.domain.memberManagement.member.application.port.in.RetrieveMemberRoleInfoUseCase;
import page.clab.api.domain.memberManagement.member.domain.Role;
import page.clab.api.global.common.dto.ApiResponse;
import page.clab.api.global.exception.InvalidColumnException;
import page.clab.api.global.exception.SortingArgumentException;
import page.clab.api.global.util.PageableUtils;

import java.util.List;

@RestController
@RequestMapping("/api/v1/members")
@RequiredArgsConstructor
@Tag(name = "Member Management - Member", description = "멤버")
public class MemberRoleInfoRetrievalController {

private final RetrieveMemberRoleInfoUseCase retrieveMemberRoleInfoUseCase;
private final PageableUtils pageableUtils;

@Operation(summary = "[A] 멤버 권한 조회", description = "ROLE_ADMIN 이상의 권한이 필요함<br>" +
"3개의 파라미터를 자유롭게 조합하여 필터링 가능<br>" +
"멤버 ID, 멤버 이름, 권한 중 하나라도 입력하지 않으면 전체 조회됨<br>" +
"DTO의 필드명을 기준으로 정렬 가능하며, 정렬 방향은 오름차순(asc)과 내림차순(desc)이 가능함")
@PreAuthorize("hasRole('ADMIN')")
@GetMapping("/roles")
public ApiResponse<List<MemberRoleInfoResponseDto>> retrieveMemberRoleInfo(
@RequestParam(required = false) String memberId,
@RequestParam(required = false) String memberName,
@RequestParam(required = false) Role role,
@RequestParam(name = "page", defaultValue = "0") int page,
@RequestParam(name = "size", defaultValue = "20") int size,
@RequestParam(name = "sortBy", defaultValue = "id") List<String> sortBy,
@RequestParam(name = "sortDirection", defaultValue = "asc") List<String> sortDirection
) throws InvalidColumnException, SortingArgumentException {
Pageable pageable = pageableUtils.createPageable(page, size, sortBy, sortDirection, MemberRoleInfoResponseDto.class);
List<MemberRoleInfoResponseDto> memberRoles = retrieveMemberRoleInfoUseCase.retrieveMemberRoleInfo(memberId, memberName, role, pageable);
return ApiResponse.success(memberRoles);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package page.clab.api.domain.memberManagement.member.adapter.in.web;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import page.clab.api.domain.memberManagement.member.application.dto.request.ChangeMemberRoleRequest;
import page.clab.api.domain.memberManagement.member.application.port.in.ManageMemberRoleUseCase;
import page.clab.api.global.common.dto.ApiResponse;

@RestController
@RequestMapping("/api/v1/members")
@RequiredArgsConstructor
@Tag(name = "Member Management - Member", description = "멤버")
public class MemberRoleManagementController {

private final ManageMemberRoleUseCase manageMemberRoleUseCase;

@Operation(summary = "[S] 멤버 권한 변경", description = "ROLE_SUPER 이상의 권한이 필요함<br>" +
"권한 계층: SUPER > ADMIN > USER > GUEST<br>" +
"GUEST 권한은 변경 불가능함")
@PreAuthorize("hasRole('SUPER')")
@PatchMapping("/{memberId}/roles")
public ApiResponse<String> changeMemberRole(
HttpServletRequest httpServletRequest,
@PathVariable(name = "memberId") String memberId,
@RequestBody ChangeMemberRoleRequest request
) {
String id = manageMemberRoleUseCase.changeMemberRole(httpServletRequest, memberId, request);
return ApiResponse.success(id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,16 @@ public Member findByIdOrThrow(String memberId) {
@Override
public List<Member> findAll() {
List<MemberJpaEntity> jpaEntities = memberRepository.findAll();
return jpaEntities.stream().map(memberMapper::toDomainEntity).toList();
return jpaEntities.stream()
.map(memberMapper::toDomainEntity)
.toList();
}

public List<Member> findMemberRoleInfoByConditions(String memberId, String memberName, Role role, Pageable pageable) {
Page<MemberJpaEntity> jpaEntities = memberRepository.findMemberRoleInfoByConditions(memberId, memberName, role, pageable);
return jpaEntities.stream()
.map(memberMapper::toDomainEntity)
.toList();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@

import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import page.clab.api.domain.memberManagement.member.domain.Role;

public interface MemberRepositoryCustom {

Page<MemberJpaEntity> findByConditions(String id, String name, Pageable pageable);

Page<MemberJpaEntity> findBirthdaysThisMonth(int month, Pageable pageable);

Page<MemberJpaEntity> findMemberRoleInfoByConditions(String memberId, String memberName, Role role, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Repository;
import page.clab.api.domain.memberManagement.member.domain.Role;
import page.clab.api.global.util.OrderSpecifierUtil;

import java.util.List;
Expand Down Expand Up @@ -61,6 +62,30 @@ public Page<MemberJpaEntity> findBirthdaysThisMonth(int month, Pageable pageable
return new PageImpl<>(members, pageable, total);
}

@Override
public Page<MemberJpaEntity> findMemberRoleInfoByConditions(String memberId, String memberName, Role role, Pageable pageable) {
QMemberJpaEntity member = QMemberJpaEntity.memberJpaEntity;
BooleanBuilder builder = new BooleanBuilder();

if (memberId != null) builder.and(member.id.eq(memberId));
if (memberName != null) builder.and(member.name.eq(memberName));
if (role != null) builder.and(member.role.eq(role));

List<MemberJpaEntity> members = queryFactory
.selectFrom(member)
.where(builder)
.orderBy(OrderSpecifierUtil.getOrderSpecifiers(pageable, member))
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

long totalCount = queryFactory.selectFrom(member)
.where(builder)
.fetchCount();

return new PageImpl<>(members, pageable, totalCount);
}

private BooleanExpression birthdayInMonth(int month) {
return QMemberJpaEntity.memberJpaEntity.birth.month().eq(month);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package page.clab.api.domain.memberManagement.member.application.dto.request;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import lombok.Setter;
import page.clab.api.domain.memberManagement.member.domain.Role;

@Getter
@Setter
public class ChangeMemberRoleRequest {

@Schema(description = "변경할 멤버 권한", example = "USER")
private Role role;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package page.clab.api.domain.memberManagement.member.application.dto.response;

import lombok.Builder;
import lombok.Getter;
import page.clab.api.domain.memberManagement.member.domain.Member;
import page.clab.api.domain.memberManagement.member.domain.Role;

import java.util.List;

@Getter
@Builder
public class MemberRoleInfoResponseDto {

private String id;
private String name;
private Role role;

public static List<MemberRoleInfoResponseDto> toDto(List<Member> members) {
return members.stream()
.map(MemberRoleInfoResponseDto::toDto)
.toList();
}

public static MemberRoleInfoResponseDto toDto(Member member) {
return MemberRoleInfoResponseDto.builder()
.id(member.getId())
.name(member.getName())
.role(member.getRole())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package page.clab.api.domain.memberManagement.member.application.exception;

public class InvalidRoleChangeException extends RuntimeException {

public InvalidRoleChangeException(String s) {
super(s);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package page.clab.api.domain.memberManagement.member.application.port.in;

import jakarta.servlet.http.HttpServletRequest;
import page.clab.api.domain.memberManagement.member.application.dto.request.ChangeMemberRoleRequest;

public interface ManageMemberRoleUseCase {
String changeMemberRole(HttpServletRequest httpServletRequest, String memberId, ChangeMemberRoleRequest request);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package page.clab.api.domain.memberManagement.member.application.port.in;

import org.springframework.data.domain.Pageable;
import page.clab.api.domain.memberManagement.member.application.dto.response.MemberRoleInfoResponseDto;
import page.clab.api.domain.memberManagement.member.domain.Role;

import java.util.List;

public interface RetrieveMemberRoleInfoUseCase {
List<MemberRoleInfoResponseDto> retrieveMemberRoleInfo(String memberId, String memberName, Role role, Pageable pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public interface RetrieveMemberPort {

List<Member> findAll();

List<Member> findMemberRoleInfoByConditions(String memberId, String memberName, Role role, Pageable pageable);

Page<Member> findAllByOrderByCreatedAtDesc(Pageable pageable);

Member findByEmailOrThrow(String email);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package page.clab.api.domain.memberManagement.member.application.service;

import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.memberManagement.member.application.dto.response.MemberRoleInfoResponseDto;
import page.clab.api.domain.memberManagement.member.application.port.in.RetrieveMemberRoleInfoUseCase;
import page.clab.api.domain.memberManagement.member.application.port.out.RetrieveMemberPort;
import page.clab.api.domain.memberManagement.member.domain.Member;
import page.clab.api.domain.memberManagement.member.domain.Role;

import java.util.List;

@Service
@RequiredArgsConstructor
public class MemberRoleInfoRetrievalService implements RetrieveMemberRoleInfoUseCase {

private final RetrieveMemberPort retrieveMemberPort;

@Transactional(readOnly = true)
@Override
public List<MemberRoleInfoResponseDto> retrieveMemberRoleInfo(String memberId, String memberName, Role role, Pageable pageable) {
List<Member> members = retrieveMemberPort.findMemberRoleInfoByConditions(memberId, memberName, role, pageable);
return MemberRoleInfoResponseDto.toDto(members);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package page.clab.api.domain.memberManagement.member.application.service;

import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import page.clab.api.domain.memberManagement.member.application.dto.request.ChangeMemberRoleRequest;
import page.clab.api.domain.memberManagement.member.application.exception.InvalidRoleChangeException;
import page.clab.api.domain.memberManagement.member.application.port.in.ManageMemberRoleUseCase;
import page.clab.api.domain.memberManagement.member.application.port.out.RetrieveMemberPort;
import page.clab.api.domain.memberManagement.member.application.port.out.UpdateMemberPort;
import page.clab.api.domain.memberManagement.member.domain.Member;
import page.clab.api.domain.memberManagement.member.domain.Role;
import page.clab.api.global.common.slack.application.SlackService;
import page.clab.api.global.common.slack.domain.SecurityAlertType;

@Service
@RequiredArgsConstructor
public class MemberRoleManagementService implements ManageMemberRoleUseCase {

private final RetrieveMemberPort retrieveMemberPort;
private final UpdateMemberPort updateMemberPort;
private final SlackService slackService;

@Transactional
@Override
public String changeMemberRole(HttpServletRequest httpServletRequest, String memberId, ChangeMemberRoleRequest request) {
Member member = retrieveMemberPort.findByIdOrThrow(memberId);

Role oldRole = member.getRole();
Role newRole = request.getRole();

validateRoleChange(member, oldRole, newRole);
member.changeRole(newRole);

updateMemberPort.update(member);
slackService.sendSecurityAlertNotification(httpServletRequest, SecurityAlertType.MEMBER_ROLE_CHANGED,
String.format("[%s] %s: %s -> %s",
member.getId(), member.getName(), oldRole, newRole));
return memberId;
}

private void validateRoleChange(Member member, Role oldRole, Role newRole) {
// 기존 권한과 새 권한이 동일한지 확인
if (oldRole.equals(newRole)) {
throw new InvalidRoleChangeException("기존 권한과 변경할 권한이 같습니다.");
}
// 멤버의 변경될 권한이 GUEST 인지 또는 변경되는 권한이 GUEST 인지 확인
if (member.isGuestRole() || newRole.isGuestRole()) {
throw new InvalidRoleChangeException("GUEST 권한은 변경 불가능합니다.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,23 @@ public void delete() {
this.isDeleted = true;
}

public boolean isGuestRole() {
return role.isGuestRole();
}

public boolean isAdminRole() {
return role.equals(Role.ADMIN) || role.equals(Role.SUPER);
return role.isHigherThanOrEqual(Role.ADMIN);
}

public boolean isSuperAdminRole() {
return role.equals(Role.SUPER);
return role.isSuperRole();
}

public boolean isSameMember(Member member) {
return id.equals(member.getId());
return isSameMemberId(member.getId());
}

public boolean isSameMember(String memberId) {
public boolean isSameMemberId(String memberId) {
return id.equals(memberId);
}

Expand Down Expand Up @@ -150,6 +154,10 @@ public void updateLoanSuspensionDate(LocalDateTime loanSuspensionDate) {
this.loanSuspensionDate = loanSuspensionDate;
}

public void changeRole(Role role) {
this.role = role;
}

public void clearImageUrl() {
this.imageUrl = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,20 @@ public Long toRoleLevel() {
public boolean isHigherThan(Role role) {
return this.toRoleLevel() > role.toRoleLevel();
}

public boolean isHigherThanOrEqual(Role role) {
return this.toRoleLevel() >= role.toRoleLevel();
}

public boolean isGuestRole() {
return this.equals(GUEST);
}

public boolean isAdminRole() {
return this.equals(ADMIN);
}

public boolean isSuperRole() {
return this.equals(SUPER);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ public enum SecurityAlertType implements AlertType {
ABNORMAL_ACCESS_IP_BLOCKED("비정상적인 접근 IP 차단", "Abnormal access IP has been blocked."),
ABNORMAL_ACCESS_IP_DELETED("비정상적인 접근 IP 삭제", "Abnormal access IP has been deleted."),
MEMBER_BANNED("멤버 밴 등록", "Member has been banned."),
MEMBER_UNBANNED("멤버 밴 해제", "Member has been unbanned.");
MEMBER_UNBANNED("멤버 밴 해제", "Member has been unbanned."),
MEMBER_ROLE_CHANGED("멤버 권한 변경", "Member role has been changed.");

private final String title;
private final String defaultMessage;
Expand Down
Loading

0 comments on commit 8868240

Please sign in to comment.