diff --git a/src/main/java/com/openisle/config/SecurityConfig.java b/src/main/java/com/openisle/config/SecurityConfig.java index 98f9ad25b..2dd18b7ce 100644 --- a/src/main/java/com/openisle/config/SecurityConfig.java +++ b/src/main/java/com/openisle/config/SecurityConfig.java @@ -1,6 +1,7 @@ package com.openisle.config; import com.openisle.service.JwtService; +import com.openisle.service.UserVisitService; import com.openisle.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; @@ -38,6 +39,7 @@ public class SecurityConfig { private final JwtService jwtService; private final UserRepository userRepository; private final AccessDeniedHandler customAccessDeniedHandler; + private final UserVisitService userVisitService; @Bean public PasswordEncoder passwordEncoder() { @@ -103,7 +105,8 @@ public class SecurityConfig { .requestMatchers("/api/admin/**").hasAuthority("ADMIN") .anyRequest().authenticated() ) - .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); + .addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class) + .addFilterAfter(userVisitFilter(), UsernamePasswordAuthenticationFilter.class); return http.build(); } @@ -150,4 +153,18 @@ public class SecurityConfig { } }; } + + @Bean + public OncePerRequestFilter userVisitFilter() { + return new OncePerRequestFilter() { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication(); + if (auth != null && auth.isAuthenticated() && !(auth instanceof org.springframework.security.authentication.AnonymousAuthenticationToken)) { + userVisitService.recordVisit(auth.getName()); + } + filterChain.doFilter(request, response); + } + }; + } } diff --git a/src/main/java/com/openisle/controller/StatController.java b/src/main/java/com/openisle/controller/StatController.java new file mode 100644 index 000000000..0e05ca686 --- /dev/null +++ b/src/main/java/com/openisle/controller/StatController.java @@ -0,0 +1,26 @@ +package com.openisle.controller; + +import com.openisle.service.UserVisitService; +import lombok.RequiredArgsConstructor; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.time.LocalDate; +import java.util.Map; + +@RestController +@RequestMapping("/api/stats") +@RequiredArgsConstructor +public class StatController { + private final UserVisitService userVisitService; + + @GetMapping("/dau") + public Map dau(@RequestParam(value = "date", required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { + long count = userVisitService.countDau(date); + return Map.of("dau", count); + } +} diff --git a/src/main/java/com/openisle/repository/UserVisitRepository.java b/src/main/java/com/openisle/repository/UserVisitRepository.java index bf5870fbc..9f05ecbeb 100644 --- a/src/main/java/com/openisle/repository/UserVisitRepository.java +++ b/src/main/java/com/openisle/repository/UserVisitRepository.java @@ -10,4 +10,5 @@ import java.util.Optional; public interface UserVisitRepository extends JpaRepository { Optional findByUserAndVisitDate(User user, LocalDate visitDate); long countByUser(User user); + long countByVisitDate(LocalDate visitDate); } diff --git a/src/main/java/com/openisle/service/UserVisitService.java b/src/main/java/com/openisle/service/UserVisitService.java index 7ae912851..29c5cdefe 100644 --- a/src/main/java/com/openisle/service/UserVisitService.java +++ b/src/main/java/com/openisle/service/UserVisitService.java @@ -32,4 +32,9 @@ public class UserVisitService { .orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found")); return userVisitRepository.countByUser(user); } + + public long countDau(LocalDate date) { + LocalDate d = date != null ? date : LocalDate.now(); + return userVisitRepository.countByVisitDate(d); + } } diff --git a/src/test/java/com/openisle/controller/AdminControllerTest.java b/src/test/java/com/openisle/controller/AdminControllerTest.java index 587f50f41..8b068e415 100644 --- a/src/test/java/com/openisle/controller/AdminControllerTest.java +++ b/src/test/java/com/openisle/controller/AdminControllerTest.java @@ -11,6 +11,7 @@ import com.openisle.config.CustomAccessDeniedHandler; import com.openisle.config.SecurityConfig; import com.openisle.service.JwtService; import com.openisle.repository.UserRepository; +import com.openisle.service.UserVisitService; import com.openisle.model.Role; import com.openisle.model.User; import java.util.Optional; @@ -31,6 +32,8 @@ class AdminControllerTest { private JwtService jwtService; @MockBean private UserRepository userRepository; + @MockBean + private UserVisitService userVisitService; @Test void adminHelloReturnsMessage() throws Exception { diff --git a/src/test/java/com/openisle/controller/HelloControllerTest.java b/src/test/java/com/openisle/controller/HelloControllerTest.java index 65ab22779..d8837feba 100644 --- a/src/test/java/com/openisle/controller/HelloControllerTest.java +++ b/src/test/java/com/openisle/controller/HelloControllerTest.java @@ -11,6 +11,7 @@ import com.openisle.config.CustomAccessDeniedHandler; import com.openisle.config.SecurityConfig; import com.openisle.service.JwtService; import com.openisle.repository.UserRepository; +import com.openisle.service.UserVisitService; import com.openisle.model.Role; import com.openisle.model.User; import java.util.Optional; @@ -31,6 +32,8 @@ class HelloControllerTest { private JwtService jwtService; @MockBean private UserRepository userRepository; + @MockBean + private UserVisitService userVisitService; @Test void helloReturnsMessage() throws Exception { diff --git a/src/test/java/com/openisle/controller/StatControllerTest.java b/src/test/java/com/openisle/controller/StatControllerTest.java new file mode 100644 index 000000000..a44df193f --- /dev/null +++ b/src/test/java/com/openisle/controller/StatControllerTest.java @@ -0,0 +1,54 @@ +package com.openisle.controller; + +import com.openisle.config.CustomAccessDeniedHandler; +import com.openisle.config.SecurityConfig; +import com.openisle.service.JwtService; +import com.openisle.repository.UserRepository; +import com.openisle.service.UserVisitService; +import com.openisle.model.Role; +import com.openisle.model.User; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.test.web.servlet.MockMvc; + +import java.util.Optional; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(StatController.class) +@AutoConfigureMockMvc +@Import({SecurityConfig.class, CustomAccessDeniedHandler.class}) +class StatControllerTest { + @Autowired + private MockMvc mockMvc; + + @MockBean + private JwtService jwtService; + @MockBean + private UserRepository userRepository; + @MockBean + private UserVisitService userVisitService; + + @Test + void dauReturnsCount() throws Exception { + Mockito.when(jwtService.validateAndGetSubject("token")).thenReturn("user"); + User user = new User(); + user.setUsername("user"); + user.setPassword("p"); + user.setEmail("u@example.com"); + user.setRole(Role.USER); + Mockito.when(userRepository.findByUsername("user")).thenReturn(Optional.of(user)); + Mockito.when(userVisitService.countDau(Mockito.any())).thenReturn(3L); + + mockMvc.perform(get("/api/stats/dau").header("Authorization", "Bearer token")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.dau").value(3)); + } +}