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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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) {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,6 +39,7 @@
@RestController
@AllArgsConstructor
@Slf4j
@TrackActivity
public class FeeController {
private final UserService userService;
private final FeeService feeService;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package school.hei.haapi.endpoint;
package school.hei.haapi.http.filter;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/school/hei/haapi/model/TrackActivity.java
Original file line number Diff line number Diff line change
@@ -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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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))
Expand All @@ -75,50 +74,48 @@ 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))
.until(() -> userActivityRepository.count() > before);

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))
Expand Down
Loading