Skip to content

Commit f14e576

Browse files
authored
Merge pull request #3 from nkcoder/refactor/api_layer
[Refactor]API layer: add CurrentUser annotation
2 parents a0b6dc3 + 423dfc9 commit f14e576

8 files changed

Lines changed: 150 additions & 55 deletions

File tree

CODE_REFACTOR_PLAN.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ all identified issues with an aggressive approach to modernization.
162162

163163
## Phase 4: API Layer - Modern REST Design (Days 8-9)
164164

165-
### 4.1 Custom Annotations & Argument Resolvers
165+
### 4.1 Custom Annotations & Argument Resolvers
166166

167167
**New Files**: `CurrentUser.java`, `CurrentUserArgumentResolver.java`, `WebConfig.java`
168168

@@ -175,7 +175,7 @@ all identified issues with an aggressive approach to modernization.
175175

176176
**Breaking Changes**: Controller method signatures change (BREAKING but cleaner)
177177

178-
### 4.2 Controller Refactoring
178+
### 4.2 Controller Refactoring
179179

180180
**Files**: `AuthController.java`, `UserController.java`
181181

@@ -190,7 +190,7 @@ all identified issues with an aggressive approach to modernization.
190190

191191
**Breaking Changes**: None (internal improvements)
192192

193-
### 4.3 Global Exception Handler Enhancement
193+
### 4.3 Global Exception Handler Enhancement
194194

195195
**File**: `GlobalExceptionHandler.java`
196196

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.nkcoder.annotation;
2+
3+
import java.lang.annotation.Documented;
4+
import java.lang.annotation.ElementType;
5+
import java.lang.annotation.Retention;
6+
import java.lang.annotation.RetentionPolicy;
7+
import java.lang.annotation.Target;
8+
9+
/**
10+
* Annotation to inject the current authenticated user's ID into controller method parameters.
11+
*
12+
* <p>Usage: {@code public ResponseEntity<?> getMe(@CurrentUser UUID userId)}
13+
*
14+
* <p>The userId is extracted from the request attributes set by JwtAuthenticationFilter. If no
15+
* authenticated user is found, the resolver returns null (let Spring Security handle unauthorized
16+
* access via security configuration).
17+
*/
18+
@Target(ElementType.PARAMETER)
19+
@Retention(RetentionPolicy.RUNTIME)
20+
@Documented
21+
public @interface CurrentUser {}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.nkcoder.config;
2+
3+
import java.util.List;
4+
import org.nkcoder.resolver.CurrentUserArgumentResolver;
5+
import org.springframework.context.annotation.Configuration;
6+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
7+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
8+
9+
@Configuration
10+
public class WebConfig implements WebMvcConfigurer {
11+
private final CurrentUserArgumentResolver currentUserArgumentResolver;
12+
13+
public WebConfig(CurrentUserArgumentResolver currentUserArgumentResolver) {
14+
this.currentUserArgumentResolver = currentUserArgumentResolver;
15+
}
16+
17+
@Override
18+
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
19+
resolvers.add(currentUserArgumentResolver);
20+
}
21+
}

src/main/java/org/nkcoder/controller/AuthController.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public AuthController(AuthService authService) {
3333
@PostMapping("/register")
3434
public ResponseEntity<ApiResponse<AuthResponse>> register(
3535
@Valid @RequestBody RegisterRequest request) {
36-
logger.info("Registration request for email: {}", request.email());
36+
logger.debug("Registration request received.");
3737

3838
AuthResponse authResponse = authService.register(request);
3939

@@ -43,7 +43,7 @@ public ResponseEntity<ApiResponse<AuthResponse>> register(
4343

4444
@PostMapping("/login")
4545
public ResponseEntity<ApiResponse<AuthResponse>> login(@Valid @RequestBody LoginRequest request) {
46-
logger.info("Login request for email: {}", request.email());
46+
logger.debug("Login request received.");
4747

4848
AuthResponse authResponse = authService.login(request);
4949

src/main/java/org/nkcoder/controller/UserController.java

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package org.nkcoder.controller;
22

3-
import jakarta.servlet.http.HttpServletRequest;
43
import jakarta.validation.Valid;
54
import java.util.UUID;
5+
import org.nkcoder.annotation.CurrentUser;
66
import org.nkcoder.dto.common.ApiResponse;
77
import org.nkcoder.dto.user.ChangePasswordRequest;
88
import org.nkcoder.dto.user.UpdateProfileRequest;
@@ -27,11 +27,8 @@ public UserController(UserService userService) {
2727
}
2828

2929
@GetMapping("/me")
30-
public ResponseEntity<ApiResponse<UserResponse>> getMe(HttpServletRequest request) {
31-
UUID userId = (UUID) request.getAttribute("userId");
32-
String email = (String) request.getAttribute("email");
33-
34-
logger.info("Get profile request for user: {}", email);
30+
public ResponseEntity<ApiResponse<UserResponse>> getMe(@CurrentUser UUID userId) {
31+
logger.info("Get profile request for userId: {}", userId);
3532

3633
UserResponse userResponse = userService.findById(userId);
3734

@@ -41,12 +38,9 @@ public ResponseEntity<ApiResponse<UserResponse>> getMe(HttpServletRequest reques
4138

4239
@PatchMapping("/me")
4340
public ResponseEntity<ApiResponse<UserResponse>> updateMe(
44-
@Valid @RequestBody UpdateProfileRequest request, HttpServletRequest httpRequest) {
45-
46-
UUID userId = (UUID) httpRequest.getAttribute("userId");
47-
String email = (String) httpRequest.getAttribute("email");
41+
@CurrentUser UUID userId, @Valid @RequestBody UpdateProfileRequest request) {
4842

49-
logger.info("Update profile request for user: {}", email);
43+
logger.info("Update profile request for userId: {}", userId);
5044

5145
UserResponse userResponse = userService.updateProfile(userId, request);
5246

@@ -55,12 +49,9 @@ public ResponseEntity<ApiResponse<UserResponse>> updateMe(
5549

5650
@PatchMapping("/me/password")
5751
public ResponseEntity<ApiResponse<Void>> changeMyPassword(
58-
@Valid @RequestBody ChangePasswordRequest request, HttpServletRequest httpRequest) {
59-
60-
UUID userId = (UUID) httpRequest.getAttribute("userId");
61-
String email = (String) httpRequest.getAttribute("email");
52+
@CurrentUser UUID userId, @Valid @RequestBody ChangePasswordRequest request) {
6253

63-
logger.info("Change password request for user: {}", email);
54+
logger.info("Change password request for userId: {}", userId);
6455

6556
userService.changePassword(userId, request);
6657

@@ -71,7 +62,7 @@ public ResponseEntity<ApiResponse<Void>> changeMyPassword(
7162
@GetMapping("/{userId}")
7263
@PreAuthorize("hasRole('ADMIN')")
7364
public ResponseEntity<ApiResponse<UserResponse>> getUser(@PathVariable UUID userId) {
74-
logger.info("Admin get user request for user: {}", userId);
65+
logger.info("Admin get user request for userId: {}", userId);
7566

7667
UserResponse userResponse = userService.findById(userId);
7768

@@ -83,7 +74,7 @@ public ResponseEntity<ApiResponse<UserResponse>> getUser(@PathVariable UUID user
8374
public ResponseEntity<ApiResponse<UserResponse>> updateUser(
8475
@PathVariable UUID userId, @Valid @RequestBody UpdateProfileRequest request) {
8576

86-
logger.info("Admin update user request for user: {}", userId);
77+
logger.info("Admin update user request for userId: {}", userId);
8778

8879
UserResponse userResponse = userService.updateProfile(userId, request);
8980

@@ -93,9 +84,9 @@ public ResponseEntity<ApiResponse<UserResponse>> updateUser(
9384
@PatchMapping("/{userId}/password")
9485
@PreAuthorize("hasRole('ADMIN')")
9586
public ResponseEntity<ApiResponse<Void>> changeUserPassword(
96-
@PathVariable UUID userId, @RequestBody ChangePasswordRequest request) {
87+
@PathVariable UUID userId, @Valid @RequestBody ChangePasswordRequest request) {
9788

98-
logger.info("Admin change password request for user: {}", userId);
89+
logger.info("Admin change password request for userId: {}", userId);
9990

10091
// For admin, we only use the newPassword field
10192
userService.changeUserPassword(userId, request.newPassword());

src/main/java/org/nkcoder/exception/GlobalExceptionHandler.java

Lines changed: 42 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package org.nkcoder.exception;
22

33
import com.fasterxml.jackson.core.JsonParseException;
4-
import java.util.HashMap;
4+
import java.time.LocalDateTime;
55
import java.util.Map;
6+
import java.util.stream.Collectors;
67
import org.nkcoder.dto.common.ApiResponse;
78
import org.slf4j.Logger;
89
import org.slf4j.LoggerFactory;
@@ -51,20 +52,21 @@ public ResponseEntity<ApiResponse<Object>> handleAccessDeniedException(AccessDen
5152
@ExceptionHandler(MethodArgumentNotValidException.class)
5253
public ResponseEntity<ApiResponse<Object>> handleValidationExceptions(
5354
MethodArgumentNotValidException e) {
54-
logger.error("Validation (invalid method argument) error: {}", e.getMessage());
55-
56-
Map<String, String> errors = new HashMap<>();
57-
e.getBindingResult()
58-
.getAllErrors()
59-
.forEach(
60-
(error) -> {
61-
String fieldName = ((FieldError) error).getField();
62-
String errorMessage = error.getDefaultMessage();
63-
errors.put(fieldName, errorMessage);
64-
});
55+
logger.debug("Validation error: {} field(s) failed", e.getBindingResult().getErrorCount());
56+
57+
Map<String, String> errors =
58+
e.getBindingResult().getFieldErrors().stream()
59+
.collect(
60+
Collectors.toMap(
61+
FieldError::getField,
62+
fieldError ->
63+
fieldError.getDefaultMessage() != null
64+
? fieldError.getDefaultMessage()
65+
: "Invalid value",
66+
(existing, replacement) -> existing));
6567

6668
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
67-
.body(ApiResponse.error("Validation failed"));
69+
.body(new ApiResponse<>("Validation failed", errors, LocalDateTime.now()));
6870
}
6971

7072
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
@@ -75,26 +77,37 @@ public ResponseEntity<ApiResponse<Object>> handleRequestMethodNotSupportedExcept
7577
.body(ApiResponse.error("Method not allowed: " + e.getMessage()));
7678
}
7779

80+
@ExceptionHandler(HttpMessageNotReadableException.class)
81+
public ResponseEntity<ApiResponse<Object>> handleHttpMessageNotReadableException(
82+
HttpMessageNotReadableException e) {
83+
logger.debug("Message not readable: {}", e.getMostSpecificCause().getMessage());
84+
85+
String message = "Malformed JSON request";
86+
Throwable cause = e.getCause();
87+
if (cause instanceof JsonParseException) {
88+
message = "Invalid JSON format: " + cause.getMessage();
89+
}
90+
91+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ApiResponse.error(message));
92+
}
93+
94+
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
95+
public ResponseEntity<ApiResponse<Object>> handleHttpMediaTypeNotSupportedException(
96+
HttpMediaTypeNotSupportedException e) {
97+
logger.debug("Unsupported media type: {}", e.getContentType());
98+
99+
String message =
100+
String.format(
101+
"Content type '%s' is not supported. Use 'application/json'", e.getContentType());
102+
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
103+
.body(ApiResponse.error(message));
104+
}
105+
78106
@ExceptionHandler(Exception.class)
79107
public ResponseEntity<ApiResponse<Object>> handleGenericException(Exception e) {
80108
logger.error("Unexpected error: {}", e.getMessage(), e);
81-
if (e instanceof JsonParseException) {
82-
logger.error("JSON parsing error: {}", e.getMessage());
83-
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
84-
.body(ApiResponse.error("Invalid JSON format"));
85-
}
86-
if (e instanceof HttpMediaTypeNotSupportedException) {
87-
logger.error("Unsupported media type: {}", e.getMessage());
88-
return ResponseEntity.status(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
89-
.body(ApiResponse.error("Unsupported media type"));
90-
}
91-
if (e instanceof HttpMessageNotReadableException) {
92-
logger.error("Message not readable: {}", e.getMessage());
93-
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
94-
.body(ApiResponse.error("Message not readable"));
95-
}
96109

97110
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
98-
.body(ApiResponse.error("Internal server error"));
111+
.body(ApiResponse.error("An unexpected error occurred. Please try again later."));
99112
}
100113
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.nkcoder.resolver;
2+
3+
import jakarta.servlet.http.HttpServletRequest;
4+
import java.util.UUID;
5+
import org.jetbrains.annotations.NotNull;
6+
import org.nkcoder.annotation.CurrentUser;
7+
import org.springframework.core.MethodParameter;
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.web.bind.support.WebDataBinderFactory;
10+
import org.springframework.web.context.request.NativeWebRequest;
11+
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
12+
import org.springframework.web.method.support.ModelAndViewContainer;
13+
14+
/**
15+
* Resolves method parameters annotated with {@link CurrentUser} by extracting the user ID from
16+
* request attributes set by {@link org.nkcoder.security.JwtAuthenticationFilter}
17+
*/
18+
@Component
19+
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
20+
21+
private static final String USER_ID_ATTRIBUTE = "userId";
22+
23+
@Override
24+
public boolean supportsParameter(MethodParameter parameter) {
25+
return parameter.hasParameterAnnotation(CurrentUser.class)
26+
&& parameter.getParameterType().equals(UUID.class);
27+
}
28+
29+
@Override
30+
public Object resolveArgument(
31+
@NotNull MethodParameter parameter,
32+
ModelAndViewContainer mavContainer,
33+
NativeWebRequest webRequest,
34+
WebDataBinderFactory binderFactory) {
35+
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
36+
if (request == null) {
37+
return null;
38+
}
39+
40+
Object userId = request.getAttribute(USER_ID_ATTRIBUTE);
41+
if (userId instanceof UUID) {
42+
return userId;
43+
}
44+
45+
return null;
46+
}
47+
}

src/test/java/org/nkcoder/controller/UserControllerTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,9 @@ void shouldChangeUserPasswordAsAdmin() throws Exception {
235235
// Given
236236
ChangePasswordRequest request =
237237
new ChangePasswordRequest(
238-
"", "NewAdminPassword123!", ""); // Only newPassword is used for admin
238+
"OldPassword123!",
239+
"NewAdminPassword123!",
240+
"NewAdminPassword123!"); // Only newPassword is used for admin
239241
doNothing().when(userService).changeUserPassword(testUserId, "NewAdminPassword123!");
240242

241243
// When & Then

0 commit comments

Comments
 (0)