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 556fb6951e..0000000000 --- a/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptor.java +++ /dev/null @@ -1,59 +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.servlet.HandlerInterceptor; -import org.springframework.web.util.ContentCachingRequestWrapper; -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) { - 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) { - // getPrincipal() can throw ClassCastException ("anonymousUser" string) - // or other exceptions when user is not authenticated - 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); - } - } -} diff --git a/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptorConfigurer.java b/src/main/java/school/hei/haapi/endpoint/UserActivityInterceptorConfigurer.java index 7befc79450..92822dcb1d 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.endpoint.rest.security.AuthProvider; +import school.hei.haapi.model.TrackActivity; +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 ecaa43fccb..3b5c4de4b3 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 @@ -25,6 +25,7 @@ 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; @@ -38,6 +39,7 @@ @RestController @AllArgsConstructor @Slf4j +@TrackActivity public class FeeController { private final UserService userService; private final FeeService feeService; 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 2fb038f06e..37dd5ac31d 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/model/TrackActivity.java b/src/main/java/school/hei/haapi/model/TrackActivity.java new file mode 100644 index 0000000000..1bcaa8ebf7 --- /dev/null +++ b/src/main/java/school/hei/haapi/model/TrackActivity.java @@ -0,0 +1,10 @@ +package school.hei.haapi.model; + +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/test/java/school/hei/haapi/integration/UserActivityInterceptorIT.java b/src/test/java/school/hei/haapi/integration/UserActivityInterceptorIT.java index a6bd8734d7..34844f6463 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,25 +46,22 @@ 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 ApiException { + 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); + .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 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)) .pollInterval(Duration.ofMillis(100)) @@ -75,27 +74,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 +101,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))