Merge remote-tracking branch 'origin/main' into eh4pzj-codex/add-whitelist-invitation-registration-mode

This commit is contained in:
tim
2025-07-14 22:05:27 +08:00
15 changed files with 334 additions and 12 deletions

View File

@@ -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() {
@@ -96,6 +98,7 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.GET, "/api/tags/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/search/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/users/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/reaction-types").permitAll()
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
.requestMatchers(HttpMethod.POST, "/api/tags/**").authenticated()
.requestMatchers(HttpMethod.DELETE, "/api/categories/**").hasAuthority("ADMIN")
@@ -103,7 +106,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();
}
@@ -123,7 +127,8 @@ public class SecurityConfig {
boolean publicGet = "GET".equalsIgnoreCase(request.getMethod()) &&
(uri.startsWith("/api/posts") || uri.startsWith("/api/comments") ||
uri.startsWith("/api/categories") || uri.startsWith("/api/tags") ||
uri.startsWith("/api/search") || uri.startsWith("/api/users"));
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
uri.startsWith("/api/reaction-types"));
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
@@ -150,4 +155,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);
}
};
}
}

View File

@@ -0,0 +1,41 @@
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.List;
import java.util.Map;
@RestController
@RequestMapping("/api/stats")
@RequiredArgsConstructor
public class StatController {
private final UserVisitService userVisitService;
@GetMapping("/dau")
public Map<String, Long> dau(@RequestParam(value = "date", required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
long count = userVisitService.countDau(date);
return Map.of("dau", count);
}
@GetMapping("/dau-range")
public List<Map<String, Object>> dauRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1;
LocalDate end = LocalDate.now();
LocalDate start = end.minusDays(days - 1L);
var data = userVisitService.countDauRange(start, end);
return data.entrySet().stream()
.map(e -> Map.<String,Object>of(
"date", e.getKey().toString(),
"value", e.getValue()
))
.toList();
}
}

View File

@@ -3,6 +3,8 @@ package com.openisle.repository;
import com.openisle.model.User;
import com.openisle.model.UserVisit;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDate;
import java.util.Optional;
@@ -10,4 +12,8 @@ import java.util.Optional;
public interface UserVisitRepository extends JpaRepository<UserVisit, Long> {
Optional<UserVisit> findByUserAndVisitDate(User user, LocalDate visitDate);
long countByUser(User user);
long countByVisitDate(LocalDate visitDate);
@Query("SELECT uv.visitDate AS d, COUNT(uv) AS c FROM UserVisit uv WHERE uv.visitDate BETWEEN :start AND :end GROUP BY uv.visitDate ORDER BY uv.visitDate")
java.util.List<Object[]> countRange(@Param("start") LocalDate start, @Param("end") LocalDate end);
}

View File

@@ -8,6 +8,8 @@ import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.LinkedHashMap;
import java.util.Map;
@Service
@RequiredArgsConstructor
@@ -32,4 +34,27 @@ 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);
}
public Map<LocalDate, Long> countDauRange(LocalDate start, LocalDate end) {
Map<LocalDate, Long> result = new LinkedHashMap<>();
if (start == null || end == null || start.isAfter(end)) {
return result;
}
var list = userVisitRepository.countRange(start, end);
for (var obj : list) {
LocalDate d = (LocalDate) obj[0];
Long c = (Long) obj[1];
result.put(d, c);
}
// fill zero counts for missing dates
for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) {
result.putIfAbsent(d, 0L);
}
return result;
}
}