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
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Slf4j
@Component
Expand All @@ -13,6 +15,7 @@ public class KmcTxFailureHandler implements
TxFailureHandler<KmcIssueTxEvent> {

@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handle(KmcIssueTxEvent event) {
KmcContext ctx = event.getContext();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@ public ProfileJpaEntity(
}

public void edit(final EditProfileRequest request) {
this.userName = request.userName();
this.gender = request.validatedGender();
this.birth = request.birth();
this.phoneNumber = request.phoneNumber();
this.email = request.email();
this.education = request.education();
this.schoolInfo = request.schoolInfo().toEntity();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package life.mosu.mosuserver.domain.user;

public enum UserRole {
ROLE_USER, ROLE_ADMIN, PENDING
ROLE_USER, ROLE_ADMIN, ROLE_PENDING
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
Expand Down Expand Up @@ -54,6 +58,21 @@ public class SecurityConfig {

private final AuthorizationRequestRedirectResolver authorizationRequestRedirectResolver;

@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER \n ROLE_USER > PENDING");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The role hierarchy definition uses PENDING, but the UserRole enum was updated in this pull request to use ROLE_PENDING. This inconsistency will cause the role hierarchy to not function as expected for users with the pending role, as Spring Security will not recognize the relationship.

To ensure the hierarchy works correctly, update the string to use the correct ROLE_PENDING name.

Suggested change
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER \n ROLE_USER > PENDING");
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER \n ROLE_USER > ROLE_PENDING");

return hierarchy;
}
Comment on lines +61 to +66
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix role hierarchy inconsistency.

The role hierarchy defines "PENDING" but according to the AI summary, this should be "ROLE_PENDING" to maintain consistency with the enum constant renaming mentioned in the PR.

-        hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER \n ROLE_USER > PENDING");
+        hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER \n ROLE_USER > ROLE_PENDING");
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER \n ROLE_USER > PENDING");
return hierarchy;
}
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER \n ROLE_USER > ROLE_PENDING");
return hierarchy;
}
🤖 Prompt for AI Agents
In src/main/java/life/mosu/mosuserver/global/config/SecurityConfig.java around
lines 61 to 66, the role hierarchy string uses "PENDING" instead of the
consistent enum naming "ROLE_PENDING". Update the hierarchy string to replace
"PENDING" with "ROLE_PENDING" to align with the enum constant naming convention.


@Bean
public MethodSecurityExpressionHandler methodSecurityExpressionHandler(
RoleHierarchy roleHierarchy) {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setRoleHierarchy(roleHierarchy);
return expressionHandler;
}

@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
Expand All @@ -74,7 +93,6 @@ public WebSecurityCustomizer webSecurityCustomizer() {
@Bean
public SecurityFilterChain securityFilterChain(final HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
// .cors(Customizer.withDefaults())
.cors(Customizer.withDefaults())
.httpBasic(AbstractHttpConfigurer::disable)
.headers(c -> c.frameOptions(FrameOptionsConfig::disable))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,17 @@
import jakarta.persistence.EntityNotFoundException;
import java.util.LinkedHashMap;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {

Expand All @@ -31,6 +34,7 @@ public ResponseEntity<Map<String, Object>> handleMethodArgumentNotValidException
errors.put(error.getField(), error.getDefaultMessage());
});
response.put("status", HttpStatus.BAD_REQUEST.value());
response.put("message", "유효성 검사에 실패했습니다.");
response.put("errors", errors);

return ResponseEntity.badRequest().body(response);
Expand All @@ -46,6 +50,7 @@ public ResponseEntity<Map<String, Object>> handleIllegalArgumentException(
IllegalArgumentException ex) {
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", HttpStatus.BAD_REQUEST.value());
response.put("errors", "잘못된 요청입니다.");
response.put("message", ex.getMessage());
Comment on lines +53 to 54

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

In this handler, the user-friendly message ("잘못된 요청입니다.") is assigned to the errors field, while the technical detail from the exception is in the message field. In other handlers (like handleEntityNotFoundException), this is reversed. The content of these fields should be consistent across all handlers.

To improve consistency, swap the values assigned here to align with the pattern used in other handlers.

Suggested change
response.put("errors", "잘못된 요청입니다.");
response.put("message", ex.getMessage());
response.put("message", ex.getMessage());
response.put("errors", "잘못된 요청입니다.");


return ResponseEntity.badRequest().body(response);
Expand All @@ -61,7 +66,9 @@ public ResponseEntity<Map<String, Object>> handleEntityNotFoundException(
EntityNotFoundException ex) {
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", HttpStatus.NOT_FOUND.value());
response.put("message", ex.getMessage());
response.put("message", "요청한 리소스가 존재하지 않습니다.");
response.put("errors", ex.getMessage());

return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}

Expand All @@ -75,7 +82,7 @@ public ResponseEntity<Map<String, Object>> handleAuthenticationException(
AuthenticationException ex) {
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", HttpStatus.UNAUTHORIZED.value());
response.put("message", "AUTHENTICATION_ERROR");
response.put("message", "인증에 실패했습니다");
response.put("errors", ex.getMessage());

return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
Expand All @@ -91,12 +98,27 @@ public ResponseEntity<Map<String, Object>> handleAccessDeniedException(
AccessDeniedException ex) {
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", HttpStatus.FORBIDDEN.value());
response.put("message", "ACCESS_DENIED");
response.put("message", "인가를 실패 했습니다");
response.put("errors", ex.getMessage());

return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
}

/**
* @return 409 Bad Request
* @RequestBody JSON 파싱 실패 (필드명 불일치, 데이터 타입 불일치, JSON 형식 오류 등)
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, Object>> handleHttpMessageNotReadableException(
HttpMessageNotReadableException ex) {
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", HttpStatus.CONFLICT.value());
response.put("message", "필드명 또는 데이터 타입이 일치하지 않습니다.");
response.put("errors", ex.getMessage());

return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}
Comment on lines +111 to +120

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The Javadoc at the top says @return 409 Bad Request, which is a contradiction. It should be either "409 Conflict" or "400 Bad Request".
HttpStatus.CONFLICT (409) is returned for an HttpMessageNotReadableException. This exception is typically thrown for client-side errors like malformed JSON. The more conventional and semantically appropriate HTTP status for such errors is HttpStatus.BAD_REQUEST (400). Aligning with this standard will make your API more predictable for developers.

Change the status code to BAD_REQUEST and correct the Javadoc.

    /**
     * @return 400 Bad Request
     * @RequestBody JSON 파싱 실패 (필드명 불일치, 데이터 타입 불일치, JSON 형식 오류 등)
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResponseEntity<Map<String, Object>> handleHttpMessageNotReadableException(
            HttpMessageNotReadableException ex) {
        Map<String, Object> response = new LinkedHashMap<>();
        response.put("status", HttpStatus.BAD_REQUEST.value());
        response.put("message", "필드명 또는 데이터 타입이 일치하지 않습니다.");
        response.put("errors", ex.getMessage());

        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
    }

Comment on lines +107 to +120
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Useful addition but consider using HTTP 400 status.

The new handler for JSON parsing errors fills an important gap in error handling coverage. However, HTTP 409 Conflict is typically reserved for resource state conflicts rather than malformed request data.

Consider using HTTP 400 Bad Request for JSON parsing errors:

-        response.put("status", HttpStatus.CONFLICT.value());
+        response.put("status", HttpStatus.BAD_REQUEST.value());
-        return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
+        return ResponseEntity.badRequest().body(response);

HTTP 400 is more semantically appropriate for malformed request data, while HTTP 409 is better suited for business logic conflicts.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* @return 409 Bad Request
* @RequestBody JSON 파싱 실패 (필드명 불일치, 데이터 타입 불일치, JSON 형식 오류 )
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, Object>> handleHttpMessageNotReadableException(
HttpMessageNotReadableException ex) {
Map<String, Object> response = new LinkedHashMap<>();
response.put("status", HttpStatus.CONFLICT.value());
response.put("message", "필드명 또는 데이터 타입이 일치하지 않습니다.");
response.put("errors", ex.getMessage());
return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
}
/**
* @return 409 Bad Request
* @RequestBody JSON 파싱 실패 (필드명 불일치, 데이터 타입 불일치, JSON 형식 오류 )
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<Map<String, Object>> handleHttpMessageNotReadableException(
HttpMessageNotReadableException ex) {
Map<String, Object> response = new LinkedHashMap<>();
- response.put("status", HttpStatus.CONFLICT.value());
+ response.put("status", HttpStatus.BAD_REQUEST.value());
response.put("message", "필드명 또는 데이터 타입이 일치하지 않습니다.");
response.put("errors", ex.getMessage());
-
- return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
+ return ResponseEntity.badRequest().body(response);
}
🤖 Prompt for AI Agents
In
src/main/java/life/mosu/mosuserver/global/exception/GlobalExceptionHandler.java
around lines 107 to 120, the handler for HttpMessageNotReadableException
currently returns HTTP 409 Conflict, which is not appropriate for JSON parsing
errors. Change the HTTP status code from HttpStatus.CONFLICT (409) to
HttpStatus.BAD_REQUEST (400) in both the response map and the ResponseEntity
status to correctly indicate a malformed request error.


@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGeneralException(Exception ex) {
System.out.println("Exception: " + ex.getMessage());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

public record LoginResponse(
Boolean isProfileRegistered,
@JsonInclude(Include.NON_NULL) LoginUserResponse oauthUser
@JsonInclude(Include.NON_NULL) LoginUserResponse userInfo
) {

public static LoginResponse from(final OAuthUser user) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public UserJpaEntity toAuthEntity(PasswordEncoder passwordEncoder) {
.agreedToMarketing(serviceTermRequest.agreedToMarketing())
.gender(Gender.PENDING)
.provider(AuthProvider.MOSU)
.userRole(UserRole.PENDING)
.userRole(UserRole.ROLE_PENDING)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,28 @@ public class ProfileController implements ProfileControllerDocs {
@PostMapping
@PreAuthorize("isAuthenticated()")
public ResponseEntity<ApiResponseWrapper<Void>> create(
@UserId final Long userId,
@UserId Long userId,
@Valid @RequestBody ProfileRequest request
) {
log.info("userId: {}", userId);
profileService.registerProfile(userId, request);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.CREATED, "프로필 등록 성공"));
}

@PutMapping
@PreAuthorize("isAuthenticated()")
public ResponseEntity<ApiResponseWrapper<Void>> update(
@UserId final Long userId,
@UserId Long userId,
@Valid @RequestBody EditProfileRequest request
) {
log.info("userId: {}", userId);
profileService.editProfile(userId, request);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "프로필 수정 성공"));
}

@GetMapping
@PreAuthorize("isAuthenticated()")
public ResponseEntity<ApiResponseWrapper<ProfileDetailResponse>> getProfile(
//@AuthenticationPrincipal final PrincipalDetails principalDetails
@UserId final Long userId
@UserId Long userId
) {
//Long userId = principalDetails.getId();
log.info("userId: {}", userId);
ProfileDetailResponse response = profileService.getProfile(userId);
return ResponseEntity.ok(ApiResponseWrapper.success(HttpStatus.OK, "프로필 조회 성공", response));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,36 +1,11 @@
package life.mosu.mosuserver.presentation.profile.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDate;
import life.mosu.mosuserver.domain.profile.Education;
import life.mosu.mosuserver.domain.profile.Gender;
import life.mosu.mosuserver.domain.profile.Grade;
import life.mosu.mosuserver.global.annotation.PhoneNumberPattern;
import life.mosu.mosuserver.global.exception.CustomRuntimeException;
import life.mosu.mosuserver.global.exception.ErrorCode;

@Schema(description = "프로필 수정 요청 DTO")
public record EditProfileRequest(

@Schema(description = "사용자 이름", example = "홍길동", required = true)
@NotBlank(message = "이름은 필수입니다.")
String userName,

@Schema(description = "생년월일", example = "2005-05-10", required = true)
@NotNull(message = "생년월일은 필수입니다.")
LocalDate birth,

@Schema(description = "성별 (MALE 또는 FEMALE)", example = "MALE", required = true)
@NotBlank(message = "성별은 필수입니다.")
String gender,

@Schema(description = "휴대폰 번호", example = "01012345678", required = true)
@NotBlank(message = "휴대폰 번호는 필수입니다.")
@PhoneNumberPattern
String phoneNumber,

@Schema(description = "이메일 주소", example = "hong@example.com")
String email,

Expand All @@ -42,14 +17,6 @@ public record EditProfileRequest(

@Schema(description = "학년 (Enum: FIRST, SECOND, THIRD 등)", example = "SECOND")
Grade grade

) {

public Gender validatedGender() {
try {
return Gender.valueOf(gender.toUpperCase());
} catch (IllegalArgumentException | NullPointerException e) {
throw new CustomRuntimeException(ErrorCode.INVALID_GENDER);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ class 카카오_로그인_응답_테스트 {

// then
assertTrue(response.isProfileRegistered());
assertNull(response.oauthUser());
assertNull(response.userInfo());

String jsonResponse = objectMapper.writeValueAsString(response);
log.info("카카오_로그인_사용자가_기존의_회원인_경우_응답 JSON : {}", jsonResponse);
Expand All @@ -99,7 +99,7 @@ class 카카오_로그인_응답_테스트 {

// then
assertFalse(response.isProfileRegistered());
assertNotNull(response.oauthUser());
assertNotNull(response.userInfo());

String jsonResponse = objectMapper.writeValueAsString(response);
log.info("카카오_로그인_사용자가_기존의_회원이_아닌_경우_응답 JSON : {}", jsonResponse);
Expand All @@ -118,7 +118,7 @@ class 모수_로그인_응답_테스트 {

// then
assertTrue(response.isProfileRegistered());
assertNull(response.oauthUser());
assertNull(response.userInfo());

String jsonResponse = objectMapper.writeValueAsString(response);
log.info("모수_로그인_사용자가_기존의_회원인_경우_응답 : {}", jsonResponse);
Expand All @@ -132,7 +132,7 @@ class 모수_로그인_응답_테스트 {

// then
assertFalse(response.isProfileRegistered());
assertNotNull(response.oauthUser());
assertNotNull(response.userInfo());

String jsonResponse = objectMapper.writeValueAsString(response);
log.info("모수_로그인_사용자가_기존의_회원이_아닌_경우_응답 : {}", jsonResponse);
Expand Down