Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions module-app/src/main/resources/logback-spring.xml
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- @formatter:off -->
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<conversionRule conversionWord="sanitizedMsg"
converterClass="co.kr.pinhouse.app.logging.SanitizedMessageConverter"/>
converterClass="co.kr.pinhouse.common.logging.SanitizedMessageConverter"/>

<!-- Console Appender - 로컬/컨테이너 모두 stdout만 사용 -->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!-- Loki/Grafana가 로그 레벨을 안정적으로 인식할 수 있도록 level 키를 앞쪽에 둔다 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} level=%-5level [%thread] [%X{traceId:-},%X{spanId:-}] %logger{36} -
%sanitizedMsg%n
</pattern>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} level=%-5level [%thread] [%X{traceId:-},%X{spanId:-}] %logger{36} - %sanitizedMsg%n</pattern>
</encoder>
</appender>

Expand All @@ -29,3 +28,4 @@
<logger name="org.springframework" level="WARN"/>
<logger name="org.hibernate" level="WARN"/>
</configuration>
<!-- @formatter:on -->
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public enum UserErrorCode implements ErrorCode {
// ========================
// 403 Forbidden
// ========================
FORBIDDEN_SELF_ROLE_CHANGE(403_200, HttpStatus.FORBIDDEN, "자신의 권한은 변경할 수 없습니다."),

// ========================
// 404 Not Found
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

public enum AdminAuditTargetType {
NOTICE,
USER,
CS_INQUIRY,
ADVERTISEMENT
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package co.kr.pinhouse.domain.admin.user.application.dto.request;

import co.kr.pinhouse.domain.user.domain.entity.Role;
import jakarta.validation.constraints.NotNull;

public record UpdateAdminUserRoleRequest(
@NotNull Role role
) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,20 @@
import co.kr.pinhouse.common.response.CustomException;
import co.kr.pinhouse.common.response.pageable.SliceRequest;
import co.kr.pinhouse.common.response.pageable.SliceResponse;
import co.kr.pinhouse.domain.admin.application.usecase.AdminSessionUseCase;
import co.kr.pinhouse.domain.admin.audit.application.usecase.AdminAuditLogUseCase;
import co.kr.pinhouse.domain.admin.audit.domain.entity.AdminAuditActionType;
import co.kr.pinhouse.domain.admin.audit.domain.entity.AdminAuditTargetType;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserDetailResponse;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserSummaryResponse;
import co.kr.pinhouse.domain.admin.user.application.usecase.AdminUserUseCase;
import co.kr.pinhouse.domain.diagnostic.diagnosis.domain.repository.DiagnosisJpaRepository;
import co.kr.pinhouse.domain.like.domain.LikeJpaRepository;
import co.kr.pinhouse.domain.pinpoint.domain.repository.PinPointMongoRepository;
import co.kr.pinhouse.domain.user.domain.entity.Role;
import co.kr.pinhouse.domain.user.domain.entity.User;
import co.kr.pinhouse.domain.user.domain.repository.UserJpaRepository;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;

@Service
Expand All @@ -29,6 +35,8 @@ public class AdminUserService implements AdminUserUseCase {
private final LikeJpaRepository likeRepository;
private final PinPointMongoRepository pinPointRepository;
private final DiagnosisJpaRepository diagnosisRepository;
private final AdminSessionUseCase adminSessionService;
private final AdminAuditLogUseCase adminAuditLogService;

// =================
// 퍼블릭 로직
Expand Down Expand Up @@ -57,23 +65,72 @@ public SliceResponse<AdminUserSummaryResponse> getUsers(String keyword, SliceReq
@Transactional(readOnly = true)
@Override
public AdminUserDetailResponse getUser(UUID userId) {
User user = userRepository.findWithFacilityTypesById(userId)
.orElseThrow(() -> new CustomException(UserErrorCode.NOT_FOUND_USER));
User user = loadUserWithFacilityTypes(userId);

return toAdminUserDetailResponse(user);
}

/// 관리자 유저 권한 변경
@Transactional
@Override
public AdminUserDetailResponse updateUserRole(
UUID userId,
Role role,
UUID adminId,
HttpServletRequest httpServletRequest
) {
adminSessionService.loadAdmin(adminId);

if (adminId.equals(userId)) {
throw new CustomException(UserErrorCode.FORBIDDEN_SELF_ROLE_CHANGE);
}

User user = loadUserWithFacilityTypes(userId);
AdminUserDetailResponse before = toAdminUserDetailResponse(user);

if (user.getRole() == role) {
return before;
}

user.changeRole(role);
AdminUserDetailResponse after = toAdminUserDetailResponse(user);

adminAuditLogService.log(
adminId,
AdminAuditActionType.UPDATE,
AdminAuditTargetType.USER,
userId.toString(),
"유저 권한 변경",
before,
after,
httpServletRequest
);

return after;
}

// =================
// 내부 로직
// =================

/// 관리자 유저 상세 응답 변환
private AdminUserDetailResponse toAdminUserDetailResponse(User user) {
return AdminUserDetailResponse.of(
user,
maskName(user.getName()),
maskEmail(user.getEmail()),
maskPhone(user.getPhoneNumber()),
likeRepository.countByUser_Id(userId),
pinPointRepository.countByUserId(userId.toString()),
diagnosisRepository.countByUser_Id(userId)
likeRepository.countByUser_Id(user.getId()),
pinPointRepository.countByUserId(user.getId().toString()),
diagnosisRepository.countByUser_Id(user.getId())
);
}

// =================
// 내부 로직
// =================
/// 유저 상세 조회
private User loadUserWithFacilityTypes(UUID userId) {
return userRepository.findWithFacilityTypesById(userId)
.orElseThrow(() -> new CustomException(UserErrorCode.NOT_FOUND_USER));
}

/// 이름 마스킹
private String maskName(String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import co.kr.pinhouse.common.response.pageable.SliceResponse;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserDetailResponse;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserSummaryResponse;
import co.kr.pinhouse.domain.user.domain.entity.Role;
import jakarta.servlet.http.HttpServletRequest;

public interface AdminUserUseCase {

Expand All @@ -18,4 +20,7 @@ public interface AdminUserUseCase {

/// 관리자 유저 상세 조회
AdminUserDetailResponse getUser(UUID userId);

/// 관리자 유저 권한 변경
AdminUserDetailResponse updateUserRole(UUID userId, Role role, UUID adminId, HttpServletRequest httpServletRequest);
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ public enum Role {
/// JSON에서 생성
@JsonCreator
public static Role fromLabel(String label) {
if (label == null || label.isBlank()) {
throw new CustomException(UserErrorCode.BAD_REQUEST_ROLE);
}

String normalizedLabel = label.trim();
for (Role userRole : values()) {
if (userRole.label.equals(label)) {
if (userRole.label.equals(normalizedLabel) || userRole.name().equalsIgnoreCase(normalizedLabel)) {
return userRole;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,11 @@ public void updateFacilityTypes(List<FacilityType> facilityTypes) {
this.facilityTypes = new ArrayList<>(facilityTypes);
}
}

/// 권한 업데이트
public void changeRole(Role role) {
if (role != null) {
this.role = role;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package co.kr.pinhouse.app.logging;
package co.kr.pinhouse.common.logging;

import ch.qos.logback.classic.pattern.MessageConverter;
import ch.qos.logback.classic.spi.ILoggingEvent;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,25 @@
import java.util.UUID;

import org.springframework.web.bind.annotation.GetMapping;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import co.kr.pinhouse.common.auth.CurrentUserId;
import co.kr.pinhouse.common.response.ApiResponse;
import co.kr.pinhouse.common.response.pageable.SliceRequest;
import co.kr.pinhouse.common.response.pageable.SliceResponse;
import co.kr.pinhouse.domain.admin.user.application.dto.request.UpdateAdminUserRoleRequest;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserDetailResponse;
import co.kr.pinhouse.domain.admin.user.application.dto.response.AdminUserSummaryResponse;
import co.kr.pinhouse.domain.admin.user.application.usecase.AdminUserUseCase;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;

@RestController
Expand All @@ -42,4 +48,16 @@ public ApiResponse<SliceResponse<AdminUserSummaryResponse>> getUsers(
public ApiResponse<AdminUserDetailResponse> getUser(@PathVariable UUID userId) {
return ApiResponse.ok(adminUserService.getUser(userId));
}

/// 관리자 유저 권한 변경
@PatchMapping("/{userId}/role")
@Operation(summary = "관리자 유저 권한 변경", description = "유저의 권한을 변경합니다.")
public ApiResponse<AdminUserDetailResponse> updateUserRole(
@PathVariable UUID userId,
@RequestBody @Valid UpdateAdminUserRoleRequest request,
@CurrentUserId(required = true) UUID adminId,
HttpServletRequest httpServletRequest
) {
return ApiResponse.ok(adminUserService.updateUserRole(userId, request.role(), adminId, httpServletRequest));
}
}
Original file line number Diff line number Diff line change
@@ -1,39 +1,48 @@
package co.kr.pinhouse.security.auth.application.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Builder;

@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "[응답][인증] Exchange Code 교환 응답", description = "Exchange Code 해석 결과를 반환합니다.")
@Builder
public record AuthExchangeResponse(
@Schema(description = "Exchange Code 처리 결과", example = "TOKEN_ISSUED")
AuthExchangeResultType result,

@Schema(description = "발급된 액세스 토큰", example = "eyJhbGciOiJIUzI1NiJ9...")
String accessToken,

@Schema(description = "발급된 리프레쉬 토큰", example = "eyJhbGciOiJIUzI1NiJ9...")
String refreshToken,

@Schema(description = "회원가입이 필요한 경우 사용할 임시 키", example = "OAUTH2_TEMP_USER:1234...")
String tempKey
) {

public static AuthExchangeResponse tokenIssued(String accessToken, String refreshToken) {
return AuthExchangeResponse.builder()
.result(AuthExchangeResultType.TOKEN_ISSUED)
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();

@Schema(
name = "[응답][인증] Exchange Code 교환 응답",
description = "Exchange Code 해석 결과를 반환합니다.",
oneOf = {
TokenIssuedAuthExchangeResponse.class,
SignupRequiredAuthExchangeResponse.class
}
)
public interface AuthExchangeResponse {

AuthExchangeResultType result();

default String accessToken() {
return null;
}

default String refreshToken() {
return null;
}

default String pinpointId() {
return null;
}

default String tempKey() {
return null;
}

static AuthExchangeResponse tokenIssued(String accessToken, String refreshToken, String pinpointId) {
return new TokenIssuedAuthExchangeResponse(
AuthExchangeResultType.TOKEN_ISSUED,
accessToken,
refreshToken,
pinpointId
);
}

public static AuthExchangeResponse signupRequired(String tempKey) {
return AuthExchangeResponse.builder()
.result(AuthExchangeResultType.SIGNUP_REQUIRED)
.tempKey(tempKey)
.build();
static AuthExchangeResponse signupRequired(String tempKey) {
return new SignupRequiredAuthExchangeResponse(
AuthExchangeResultType.SIGNUP_REQUIRED,
tempKey
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package co.kr.pinhouse.security.auth.application.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;

import io.swagger.v3.oas.annotations.media.Schema;

@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "[응답][인증] 회원가입 필요 응답", description = "회원가입이 필요한 상태를 반환합니다.")
public record SignupRequiredAuthExchangeResponse(
@Schema(description = "Exchange Code 처리 결과", example = "SIGNUP_REQUIRED")
AuthExchangeResultType result,

@Schema(description = "회원가입이 필요한 경우 사용할 임시 키", example = "OAUTH2_TEMP_USER:1234...")
String tempKey
) implements AuthExchangeResponse {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package co.kr.pinhouse.security.auth.application.dto.response;

import com.fasterxml.jackson.annotation.JsonInclude;

import io.swagger.v3.oas.annotations.media.Schema;

@JsonInclude(JsonInclude.Include.NON_NULL)
@Schema(name = "[응답][인증] 토큰 발급 응답", description = "토큰 발급 완료 상태를 반환합니다.")
public record TokenIssuedAuthExchangeResponse(
@Schema(description = "Exchange Code 처리 결과", example = "TOKEN_ISSUED")
AuthExchangeResultType result,

@Schema(description = "발급된 액세스 토큰", example = "eyJhbGciOiJIUzI1NiJ9...")
String accessToken,

@Schema(description = "발급된 리프레쉬 토큰", example = "eyJhbGciOiJIUzI1NiJ9...")
String refreshToken,

@Schema(description = "로그인 완료 시 대표 PinPoint ID", example = "f47ac10b-58cc-4372-a567-0e02b2c3d479")
@JsonInclude(JsonInclude.Include.ALWAYS)
String pinpointId
) implements AuthExchangeResponse {
}
Loading
Loading