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
2 changes: 1 addition & 1 deletion config
2 changes: 2 additions & 0 deletions src/main/java/com/meetup/server/ServerApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication(exclude = { RateLimiterAutoConfiguration.class })
@EnableCaching
@EnableScheduling
@EnableAsync
public class ServerApplication {

public static void main(String[] args) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,18 @@

import com.meetup.server.admin.dto.request.AdminRegisterRequest;
import com.meetup.server.admin.dto.response.AdminEventResponse;
import com.meetup.server.admin.dto.response.DailyStatsResponse;
import com.meetup.server.admin.dto.response.AdminUserResponse;
import com.meetup.server.admin.dto.response.DailyEventStatsResponse;
import com.meetup.server.admin.dto.response.DailyUserStatsResponse;
import com.meetup.server.admin.implement.AdminValidator;
import com.meetup.server.admin.implement.AdminWriter;
import com.meetup.server.event.domain.Event;
import com.meetup.server.event.implement.EventReader;
import com.meetup.server.startpoint.implement.StartPointReader;
import com.meetup.server.startpoint.infrastructure.querydsl.projection.ParticipantCount;
import com.meetup.server.user.domain.User;
import com.meetup.server.user.implement.LogUserLoginReader;
import com.meetup.server.user.implement.UserReader;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
Expand All @@ -31,6 +36,8 @@ public class AdminService {
private final AdminWriter adminWriter;
private final EventReader eventReader;
private final StartPointReader startPointReader;
private final LogUserLoginReader logUserLoginReader;
private final UserReader userReader;

@Transactional
public void register(AdminRegisterRequest request) {
Expand Down Expand Up @@ -67,11 +74,33 @@ public Page<AdminEventResponse> getAllEvents(Pageable pageable) {
);
}

public DailyStatsResponse getDailyStats() {
public Page<AdminUserResponse> getAllUsers(Pageable pageable) {
Page<User> users = userReader.readAll(pageable);

List<AdminUserResponse> adminUserResponses = users.getContent().stream()
.map(AdminUserResponse::from)
.toList();

return new PageImpl<>(
adminUserResponses,
pageable,
users.getTotalElements()
);
}

public DailyEventStatsResponse getDailyEventStats() {
LocalDate todayDate = LocalDate.now();
long dailyEventCount = eventReader.readDailyEventCount(todayDate);
long dailyParticipantCount = startPointReader.readDailyParticipantCount(todayDate);

return DailyStatsResponse.of(dailyEventCount, dailyParticipantCount);
return DailyEventStatsResponse.of(dailyEventCount, dailyParticipantCount);
}

public DailyUserStatsResponse getDailyUserStats() {
LocalDate todayDate = LocalDate.now();
long dailyLoginUserCount = logUserLoginReader.readDailyLoginUserCount(todayDate);
long dailyRegisterUserCount = userReader.readDailyRegisterUserCount(todayDate);

return DailyUserStatsResponse.of(dailyLoginUserCount, dailyRegisterUserCount);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.meetup.server.admin.dto.response;

import com.meetup.server.user.domain.User;

import java.time.LocalDateTime;

public record AdminUserResponse(
Long userId,
String nickname,
String email,
boolean isPersonalInfoAgreement,
boolean isMarketingAgreement,
LocalDateTime createdDateTime,
LocalDateTime deletedDateTime
) {
public static AdminUserResponse from(User user) {
return new AdminUserResponse(
user.getUserId(),
user.getNickname(),
user.getEmail(),
user.isPersonalInfoAgreement(),
user.isMarketingAgreement(),
user.getCreatedAt(),
user.getDeletedAt()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.meetup.server.admin.dto.response;

public record DailyEventStatsResponse(
long dailyEventCount,
long dailyParticipantCount
) {
public static DailyEventStatsResponse of(long eventCount, long participantCount) {
return new DailyEventStatsResponse(eventCount, participantCount);
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.meetup.server.admin.dto.response;

public record DailyUserStatsResponse(
long dailyLoginUserCount,
long dailyRegisterUserCount
) {
public static DailyUserStatsResponse of(long loginCount, long registerCount) {
return new DailyUserStatsResponse(loginCount, registerCount);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@
import com.meetup.server.admin.dto.request.AdminLoginRequest;
import com.meetup.server.admin.dto.request.AdminRegisterRequest;
import com.meetup.server.admin.dto.response.AdminEventResponse;
import com.meetup.server.admin.dto.response.DailyStatsResponse;
import com.meetup.server.admin.dto.response.AdminUserResponse;
import com.meetup.server.admin.dto.response.DailyEventStatsResponse;
import com.meetup.server.admin.dto.response.DailyUserStatsResponse;
import com.meetup.server.admin.exception.AdminErrorType;
import com.meetup.server.admin.exception.AdminException;
import jakarta.validation.Valid;
Expand Down Expand Up @@ -72,11 +74,24 @@ public String getAllEvents(
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable
) {
Page<AdminEventResponse> eventPages = adminService.getAllEvents(pageable);
DailyEventStatsResponse dailyEventStats = adminService.getDailyEventStats();

model.addAttribute("eventPages", eventPages);
model.addAttribute("clientUrl", clientUrl);

DailyStatsResponse dailyStats = adminService.getDailyStats();
model.addAttribute("dailyStats", dailyStats);
model.addAttribute("dailyEventStats", dailyEventStats);
return "admin/events";
}

@GetMapping("/users")
public String getAllUsers(
Model model,
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC) Pageable pageable
) {
Page<AdminUserResponse> userPages = adminService.getAllUsers(pageable);
DailyUserStatsResponse dailyUserStats = adminService.getDailyUserStats();

model.addAttribute("userPages", userPages);
model.addAttribute("dailyUserStats", dailyUserStats);
return "admin/users";
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
package com.meetup.server.auth.support.handler;

import com.meetup.server.user.domain.type.LoginStatus;
import com.meetup.server.user.implement.LogUserLoginWriter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
Expand All @@ -17,6 +20,8 @@
@Component
public class OAuth2LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

private final LogUserLoginWriter logUserLoginWriter;

@Value("${app.oauth2.failureRedirectUri}")
private String redirectUri;

Expand All @@ -27,6 +32,10 @@ public void onAuthenticationFailure(

log.info("OAuth2 login failed: {}", exception.getMessage());

String ipAddress = request.getHeader("X-Real-IP");
String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
logUserLoginWriter.save(null, LoginStatus.FAILURE, ipAddress, userAgent, exception.getMessage());

String targetUrl = UriComponentsBuilder.fromUriString(redirectUri)
.queryParam("error", exception.getLocalizedMessage())
.build().toUriString();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
import com.meetup.server.auth.dto.CustomOAuth2User;
import com.meetup.server.auth.support.CookieUtil;
import com.meetup.server.global.support.jwt.JwtTokenProvider;
import com.meetup.server.user.domain.type.LoginStatus;
import com.meetup.server.user.implement.LogUserLoginWriter;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpHeaders;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
Expand All @@ -22,6 +25,7 @@ public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHan

private final JwtTokenProvider tokenProvider;
private final CookieUtil cookieUtil;
private final LogUserLoginWriter logUserLoginWriter;

@Value("${app.oauth2.successRedirectUri}")
private String successRedirectUri;
Expand All @@ -33,6 +37,10 @@ public void onAuthenticationSuccess(

CustomOAuth2User oAuth2User = (CustomOAuth2User) authentication.getPrincipal();

String ipAddress = request.getHeader("X-Real-IP");
String userAgent = request.getHeader(HttpHeaders.USER_AGENT);
logUserLoginWriter.save(oAuth2User.getUserId(), LoginStatus.SUCCESS, ipAddress, userAgent, null);

String accessToken = tokenProvider.createAccessToken(oAuth2User);
String refreshToken = tokenProvider.createRefreshToken(oAuth2User);

Expand Down
47 changes: 47 additions & 0 deletions src/main/java/com/meetup/server/user/domain/LogUserLogin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.meetup.server.user.domain;

import com.meetup.server.global.domain.BaseEntity;
import com.meetup.server.global.util.StringUtil;
import com.meetup.server.user.domain.type.LoginStatus;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Entity
@Table(name = "log_user_login")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class LogUserLogin extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "log_user_login_id")
private Long id;

@Column(name = "user_id")
private Long userId;

@Enumerated(EnumType.STRING)
@Column(name = "login_status", nullable = false)
private LoginStatus loginStatus;

@Column(name = "ip_address", length = 45)
private String ipAddress;

@Column(name = "user_agent")
private String userAgent;

@Column(name = "fail_reason")
private String failReason;

@Builder
public LogUserLogin(Long userId, LoginStatus loginStatus, String ipAddress, String userAgent, String failReason) {
this.userId = userId;
this.loginStatus = loginStatus;
this.ipAddress = ipAddress;
this.userAgent = StringUtil.truncate(userAgent, 255);
this.failReason = StringUtil.truncate(failReason, 255);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.meetup.server.user.domain.type;

public enum LoginStatus {
SUCCESS,
FAILURE,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.meetup.server.user.implement;

import com.meetup.server.user.infrastructure.jpa.LogUserLoginRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

@Component
@RequiredArgsConstructor
public class LogUserLoginReader {

private final LogUserLoginRepository logUserLoginRepository;

public long readDailyLoginUserCount(LocalDate todayDate) {
LocalDateTime startDateTime = todayDate.atStartOfDay();
LocalDateTime endDateTime = todayDate.atTime(LocalTime.MAX);
return logUserLoginRepository.countUniqueLoginUsers(startDateTime, endDateTime);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.meetup.server.user.implement;

import com.meetup.server.user.domain.LogUserLogin;
import com.meetup.server.user.domain.type.LoginStatus;
import com.meetup.server.user.infrastructure.jpa.LogUserLoginRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

import java.util.concurrent.CompletableFuture;

@Slf4j
@Component
@RequiredArgsConstructor
public class LogUserLoginWriter {

private final LogUserLoginRepository logUserLoginRepository;

@Async
public CompletableFuture<Void> save(Long userId, LoginStatus loginStatus, String ipAddress, String userAgent, String failReason) {
try {
logUserLoginRepository.save(
LogUserLogin.builder()
.userId(userId)
.loginStatus(loginStatus)
.ipAddress(ipAddress)
.userAgent(userAgent)
.failReason(failReason)
.build()
);
} catch (Exception e) {
log.error("[LogUserLoginWriter]: 유저 로그인 로그 저장에 실패했습니다. userId: {}", userId, e);
}
return CompletableFuture.completedFuture(null);
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/meetup/server/user/implement/UserReader.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
import com.meetup.server.user.exception.UserException;
import com.meetup.server.user.infrastructure.jpa.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Optional;

@Component
Expand All @@ -24,4 +29,14 @@ public User read(Long userId) {
return readUserIfExists(userId)
.orElseThrow(() -> new UserException(UserErrorType.USER_NOT_FOUND));
}

public Page<User> readAll(Pageable pageable) {
return userRepository.findAll(pageable);
}

public long readDailyRegisterUserCount(LocalDate todayDate) {
LocalDateTime startDateTime = todayDate.atStartOfDay();
LocalDateTime endDateTime = todayDate.atTime(LocalTime.MAX);
return userRepository.countByCreatedAtBetween(startDateTime, endDateTime);
}
}
Loading