From 1aab2709d6d7b1455248141c66b655408e529023 Mon Sep 17 00:00:00 2001 From: manitra Date: Wed, 8 Jul 2026 12:05:52 +0300 Subject: [PATCH 1/3] feat: add TrackActivity annotator --- .../endpoint/UserActivityInterceptor.java | 16 +++++-- .../rest/annotation/TrackActivity.java | 10 ++++ .../rest/controller/FeeController.java | 2 + .../UserActivityInterceptorIT.java | 48 +++++++++---------- 4 files changed, 47 insertions(+), 29 deletions(-) create mode 100644 src/main/java/school/hei/haapi/endpoint/rest/annotation/TrackActivity.java diff --git a/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptor.java b/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptor.java index 556fb6951..cab47df46 100644 --- a/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptor.java +++ b/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptor.java @@ -6,8 +6,10 @@ import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.util.ContentCachingRequestWrapper; +import school.hei.haapi.endpoint.rest.annotation.TrackActivity; import school.hei.haapi.endpoint.rest.security.AuthProvider; import school.hei.haapi.endpoint.rest.security.model.Principal; import school.hei.haapi.service.UserActivityService; @@ -27,10 +29,12 @@ public boolean preHandle( @Override public void afterCompletion( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { + if (!shouldTrack(handler)) { + return; + } try { String userId = null; String userEmail = null; - try { Principal principal = AuthProvider.getPrincipal(); if (principal != null && principal.getUser() != null) { @@ -38,8 +42,6 @@ public void afterCompletion( userEmail = principal.getUser().getEmail(); } } catch (Exception e) { - // getPrincipal() can throw ClassCastException ("anonymousUser" string) - // or other exceptions when user is not authenticated log.debug("Anonymous request, no principal"); } String body = null; @@ -56,4 +58,12 @@ public void afterCompletion( log.error("Failed to persist user activity", e); } } + + private boolean shouldTrack(Object handler) { + if (!(handler instanceof HandlerMethod handlerMethod)) { + return false; + } + return handlerMethod.hasMethodAnnotation(TrackActivity.class) + || handlerMethod.getBeanType().isAnnotationPresent(TrackActivity.class); + } } diff --git a/src/main/java/school/hei/haapi/endpoint/rest/annotation/TrackActivity.java b/src/main/java/school/hei/haapi/endpoint/rest/annotation/TrackActivity.java new file mode 100644 index 000000000..2f8de51bb --- /dev/null +++ b/src/main/java/school/hei/haapi/endpoint/rest/annotation/TrackActivity.java @@ -0,0 +1,10 @@ +package school.hei.haapi.endpoint.rest.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface TrackActivity {} diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java index ecaa43fcc..4b5164a68 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java @@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import school.hei.haapi.endpoint.rest.annotation.TrackActivity; import school.hei.haapi.endpoint.rest.mapper.FeeMapper; import school.hei.haapi.endpoint.rest.mapper.FeeTemplateMapper; import school.hei.haapi.endpoint.rest.model.*; @@ -38,6 +39,7 @@ @RestController @AllArgsConstructor @Slf4j +@TrackActivity public class FeeController { private final UserService userService; private final FeeService feeService; diff --git a/src/test/java/school/hei/haapi/integration/UserActivityInterceptorIT.java b/src/test/java/school/hei/haapi/integration/UserActivityInterceptorIT.java index a6bd8734d..bf957ba3d 100644 --- a/src/test/java/school/hei/haapi/integration/UserActivityInterceptorIT.java +++ b/src/test/java/school/hei/haapi/integration/UserActivityInterceptorIT.java @@ -13,7 +13,9 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.testcontainers.junit.jupiter.Testcontainers; import school.hei.haapi.endpoint.rest.api.EventsApi; +import school.hei.haapi.endpoint.rest.api.PayingApi; import school.hei.haapi.endpoint.rest.client.ApiClient; +import school.hei.haapi.endpoint.rest.client.ApiException; import school.hei.haapi.integration.conf.FacadeITMockedThirdParties; import school.hei.haapi.integration.conf.TestUtils; import school.hei.haapi.model.UserActivity; @@ -44,24 +46,20 @@ void setUp() { } @Test - void request_always_passes_and_activity_is_saved() throws Exception { - var api = new EventsApi(anApiClient(null)); + void request_on_untracked_controller_saves_no_activity() throws Exception { + var api = new EventsApi(anApiClient(MANAGER1_TOKEN)); var before = userActivityRepository.count(); - api.getEvents(1, 15, null, null, null, null, null, null, null); - - await() - .atMost(Duration.ofSeconds(5)) - .pollInterval(Duration.ofMillis(100)) - .until(() -> userActivityRepository.count() > before); + Thread.sleep(500); + assertEquals(before, userActivityRepository.count()); } @Test - void get_request_with_auth_saves_activity() throws Exception { - var api = new EventsApi(anApiClient(MANAGER1_TOKEN)); + void get_request_with_auth_saves_activity() throws ApiException { + var api = new PayingApi(anApiClient(STUDENT1_TOKEN)); var before = userActivityRepository.count(); - api.getEventById(EVENT1_ID); + api.getStudentFeeById(STUDENT1_ID, FEE1_ID); await() .atMost(Duration.ofSeconds(5)) @@ -75,27 +73,26 @@ void get_request_with_auth_saves_activity() throws Exception { } @Test - void post_request_saves_activity_with_request_body() throws Exception { - var api = new EventsApi(anApiClient(MANAGER1_TOKEN)); + void post_request_saves_activity_with_request_body() throws ApiException { + var api = new PayingApi(anApiClient(MANAGER1_TOKEN)); var before = userActivityRepository.count(); - api.crupdateEvents(List.of(createEventCourse1()), null, null, null, null); - + api.createStudentFees(STUDENT1_ID, List.of(createFeeForTest())); await() .atMost(Duration.ofSeconds(5)) .pollInterval(Duration.ofMillis(100)) .until(() -> userActivityRepository.count() > before); var last = getLastActivity(); - assertEquals("PUT", last.getHttpMethod()); + assertEquals("POST", last.getHttpMethod()); assertNotNull(last.getRequestBody()); assertFalse(last.getRequestBody().isBlank()); } @Test - void activity_saves_correct_endpoint_and_http_method() throws Exception { - var api = new EventsApi(anApiClient(MANAGER1_TOKEN)); + void activity_saves_correct_endpoint_and_http_method() throws ApiException { + var api = new PayingApi(anApiClient(MANAGER1_TOKEN)); var before = userActivityRepository.count(); - api.getEventById(EVENT1_ID); + api.getStudentFeeById(STUDENT1_ID, FEE1_ID); await() .atMost(Duration.ofSeconds(5)) .pollInterval(Duration.ofMillis(100)) @@ -103,22 +100,21 @@ void activity_saves_correct_endpoint_and_http_method() throws Exception { var last = getLastActivity(); assertTrue( - last.getEndpoint().contains("/events/" + EVENT1_ID), - "Endpoint should contain /events/" + EVENT1_ID); + last.getEndpoint().contains("/students/" + STUDENT1_ID + "/fees/" + FEE1_ID), + "Endpoint should contain /students/" + STUDENT1_ID + "/fees/" + FEE1_ID); assertEquals("GET", last.getHttpMethod()); } @Test - void delete_request_saves_activity() throws Exception { - var api = new EventsApi(anApiClient(MANAGER1_TOKEN)); - var created = - api.crupdateEvents(List.of(createEventCourse1()), null, null, null, null).getFirst(); + void delete_request_saves_activity() throws ApiException { + var api = new PayingApi(anApiClient(MANAGER1_TOKEN)); + var created = api.createStudentFees(STUDENT1_ID, List.of(createFeeForTest())).getFirst(); await() .atMost(Duration.ofSeconds(5)) .pollInterval(Duration.ofMillis(100)) .until(() -> userActivityRepository.count() > 0); var before = userActivityRepository.count(); - api.deleteEventById(created.getId()); + api.deleteStudentFeeById(created.getId(), STUDENT1_ID); await() .atMost(Duration.ofSeconds(5)) .pollInterval(Duration.ofMillis(100)) From c9f81f3a3a6bcbb964069d8076f58d6d943a6475 Mon Sep 17 00:00:00 2001 From: manitra Date: Thu, 9 Jul 2026 13:47:21 +0300 Subject: [PATCH 2/3] feat: add TrackActivity annotator and refactor related code --- .../endpoint/UserActivityInterceptor.java | 69 ---------------- .../UserActivityInterceptorConfigurer.java | 80 ++++++++++++++++++- .../rest/controller/FeeController.java | 2 +- .../filter}/RequestWrappingFilter.java | 2 +- .../annotation => model}/TrackActivity.java | 2 +- .../UserActivityInterceptorIT.java | 11 +-- 6 files changed, 87 insertions(+), 79 deletions(-) delete mode 100644 src/main/java/school/hei/haapi/endpoint/UserActivityInterceptor.java rename src/main/java/school/hei/haapi/{endpoint => http/filter}/RequestWrappingFilter.java (97%) rename src/main/java/school/hei/haapi/{endpoint/rest/annotation => model}/TrackActivity.java (84%) diff --git a/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptor.java b/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptor.java deleted file mode 100644 index cab47df46..000000000 --- a/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptor.java +++ /dev/null @@ -1,69 +0,0 @@ -package school.hei.haapi.endpoint; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import java.nio.charset.StandardCharsets; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.method.HandlerMethod; -import org.springframework.web.servlet.HandlerInterceptor; -import org.springframework.web.util.ContentCachingRequestWrapper; -import school.hei.haapi.endpoint.rest.annotation.TrackActivity; -import school.hei.haapi.endpoint.rest.security.AuthProvider; -import school.hei.haapi.endpoint.rest.security.model.Principal; -import school.hei.haapi.service.UserActivityService; - -@Slf4j -@Component -@AllArgsConstructor -public class UserActivityInterceptor implements HandlerInterceptor { - private final UserActivityService userActivityService; - - @Override - public boolean preHandle( - HttpServletRequest request, HttpServletResponse response, Object handler) { - return true; - } - - @Override - public void afterCompletion( - HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { - if (!shouldTrack(handler)) { - return; - } - try { - String userId = null; - String userEmail = null; - try { - Principal principal = AuthProvider.getPrincipal(); - if (principal != null && principal.getUser() != null) { - userId = principal.getUser().getId(); - userEmail = principal.getUser().getEmail(); - } - } catch (Exception e) { - log.debug("Anonymous request, no principal"); - } - String body = null; - var attr = request.getAttribute("cachedRequestWrapper"); - if (attr instanceof ContentCachingRequestWrapper wrapper) { - var buf = wrapper.getContentAsByteArray(); - if (buf.length > 0) { - body = new String(buf, StandardCharsets.UTF_8); - } - } - userActivityService.save( - userId, userEmail, request.getRequestURI(), request.getMethod(), body); - } catch (Exception e) { - log.error("Failed to persist user activity", e); - } - } - - private boolean shouldTrack(Object handler) { - if (!(handler instanceof HandlerMethod handlerMethod)) { - return false; - } - return handlerMethod.hasMethodAnnotation(TrackActivity.class) - || handlerMethod.getBeanType().isAnnotationPresent(TrackActivity.class); - } -} diff --git a/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptorConfigurer.java b/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptorConfigurer.java index 7befc7945..9a4d6f834 100644 --- a/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptorConfigurer.java +++ b/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptorConfigurer.java @@ -1,17 +1,93 @@ package school.hei.haapi.endpoint; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.nio.charset.StandardCharsets; import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Configuration; +import org.springframework.lang.Nullable; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.util.ContentCachingRequestWrapper; +import school.hei.haapi.model.TrackActivity; +import school.hei.haapi.endpoint.rest.security.AuthProvider; +import school.hei.haapi.service.UserActivityService; @Configuration @AllArgsConstructor public class UserActivityInterceptorConfigurer implements WebMvcConfigurer { - private UserActivityInterceptor userActivityInterceptor; + private final UserActivityService userActivityService; @Override public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(userActivityInterceptor); + registry.addInterceptor(new UserActivityInterceptor(userActivityService)); + } + + @AllArgsConstructor + @Slf4j + private static class UserActivityInterceptor implements HandlerInterceptor { + private static final String CACHED_REQUEST_WRAPPER_ATTR = "cachedRequestWrapper"; + private final UserActivityService userActivityService; + + @Override + public boolean preHandle( + HttpServletRequest request, HttpServletResponse response, Object handler) { + return true; + } + + @Override + public void afterCompletion( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + @Nullable Exception ex) { + if (!shouldTrack(handler)) { + return; + } + try { + UserInfo userInfo = extractUserInfo(); + String body = extractBody(request); + userActivityService.save( + userInfo.id(), userInfo.email(), request.getRequestURI(), request.getMethod(), body); + } catch (Exception e) { + log.error("Failed to persist user activity", e); + } + } + + private boolean shouldTrack(Object handler) { + if (!(handler instanceof HandlerMethod handlerMethod)) { + return false; + } + return handlerMethod.hasMethodAnnotation(TrackActivity.class) + || handlerMethod.getBeanType().isAnnotationPresent(TrackActivity.class); + } + + private UserInfo extractUserInfo() { + try { + var principal = AuthProvider.getPrincipal(); + if (principal != null && principal.getUser() != null) { + return new UserInfo(principal.getUser().getId(), principal.getUser().getEmail()); + } + } catch (Exception e) { + log.debug("Anonymous request, no principal"); + } + return new UserInfo(null, null); + } + + private String extractBody(HttpServletRequest request) { + var attr = request.getAttribute(CACHED_REQUEST_WRAPPER_ATTR); + if (attr instanceof ContentCachingRequestWrapper wrapper) { + var buf = wrapper.getContentAsByteArray(); + if (buf.length > 0) { + return new String(buf, StandardCharsets.UTF_8); + } + } + return null; + } + + private record UserInfo(String id, String email) {} } } diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java index 4b5164a68..a39d4719d 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java @@ -20,7 +20,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import school.hei.haapi.endpoint.rest.annotation.TrackActivity; +import school.hei.haapi.model.TrackActivity; import school.hei.haapi.endpoint.rest.mapper.FeeMapper; import school.hei.haapi.endpoint.rest.mapper.FeeTemplateMapper; import school.hei.haapi.endpoint.rest.model.*; diff --git a/src/main/java/school/hei/haapi/endpoint/RequestWrappingFilter.java b/src/main/java/school/hei/haapi/http/filter/RequestWrappingFilter.java similarity index 97% rename from src/main/java/school/hei/haapi/endpoint/RequestWrappingFilter.java rename to src/main/java/school/hei/haapi/http/filter/RequestWrappingFilter.java index 2fb038f06..37dd5ac31 100644 --- a/src/main/java/school/hei/haapi/endpoint/RequestWrappingFilter.java +++ b/src/main/java/school/hei/haapi/http/filter/RequestWrappingFilter.java @@ -1,4 +1,4 @@ -package school.hei.haapi.endpoint; +package school.hei.haapi.http.filter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/src/main/java/school/hei/haapi/endpoint/rest/annotation/TrackActivity.java b/src/main/java/school/hei/haapi/model/TrackActivity.java similarity index 84% rename from src/main/java/school/hei/haapi/endpoint/rest/annotation/TrackActivity.java rename to src/main/java/school/hei/haapi/model/TrackActivity.java index 2f8de51bb..1bcaa8ebf 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/annotation/TrackActivity.java +++ b/src/main/java/school/hei/haapi/model/TrackActivity.java @@ -1,4 +1,4 @@ -package school.hei.haapi.endpoint.rest.annotation; +package school.hei.haapi.model; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/src/test/java/school/hei/haapi/integration/UserActivityInterceptorIT.java b/src/test/java/school/hei/haapi/integration/UserActivityInterceptorIT.java index bf957ba3d..34844f646 100644 --- a/src/test/java/school/hei/haapi/integration/UserActivityInterceptorIT.java +++ b/src/test/java/school/hei/haapi/integration/UserActivityInterceptorIT.java @@ -46,21 +46,22 @@ void setUp() { } @Test - void request_on_untracked_controller_saves_no_activity() throws Exception { + void request_on_untracked_controller_saves_no_activity() throws ApiException { var api = new EventsApi(anApiClient(MANAGER1_TOKEN)); var before = userActivityRepository.count(); api.getEvents(1, 15, null, null, null, null, null, null, null); - Thread.sleep(500); - assertEquals(before, userActivityRepository.count()); + await() + .during(Duration.ofMillis(500)) + .atMost(Duration.ofSeconds(2)) + .pollInterval(Duration.ofMillis(50)) + .untilAsserted(() -> assertEquals(before, userActivityRepository.count())); } @Test void get_request_with_auth_saves_activity() throws ApiException { var api = new PayingApi(anApiClient(STUDENT1_TOKEN)); var before = userActivityRepository.count(); - api.getStudentFeeById(STUDENT1_ID, FEE1_ID); - await() .atMost(Duration.ofSeconds(5)) .pollInterval(Duration.ofMillis(100)) From 0146eafe673cdde279988d62c28d6b36cbe900b6 Mon Sep 17 00:00:00 2001 From: manitra Date: Thu, 9 Jul 2026 13:49:56 +0300 Subject: [PATCH 3/3] fix: format --- .../hei/haapi/endpoint/UserActivityInterceptorConfigurer.java | 2 +- .../hei/haapi/endpoint/rest/controller/FeeController.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptorConfigurer.java b/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptorConfigurer.java index 9a4d6f834..92822dcb1 100644 --- a/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptorConfigurer.java +++ b/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptorConfigurer.java @@ -12,8 +12,8 @@ import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.util.ContentCachingRequestWrapper; -import school.hei.haapi.model.TrackActivity; import school.hei.haapi.endpoint.rest.security.AuthProvider; +import school.hei.haapi.model.TrackActivity; import school.hei.haapi.service.UserActivityService; @Configuration diff --git a/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java b/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java index a39d4719d..3b5c4de4b 100644 --- a/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java +++ b/src/main/java/school/hei/haapi/endpoint/rest/controller/FeeController.java @@ -20,12 +20,12 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import school.hei.haapi.model.TrackActivity; import school.hei.haapi.endpoint.rest.mapper.FeeMapper; import school.hei.haapi.endpoint.rest.mapper.FeeTemplateMapper; import school.hei.haapi.endpoint.rest.model.*; import school.hei.haapi.model.BoundedPageSize; import school.hei.haapi.model.PageFromOne; +import school.hei.haapi.model.TrackActivity; import school.hei.haapi.model.statistics.AdvancedFeeStats; import school.hei.haapi.model.statistics.AdvancedFeeStats.AdvancedFeeStatsCountType; import school.hei.haapi.model.validator.UpdateFeeValidator;