diff --git a/build.gradle b/build.gradle index f46f2163..8523b115 100644 --- a/build.gradle +++ b/build.gradle @@ -24,6 +24,7 @@ repositories { } dependencies { + implementation 'org.springframework.boot:spring-boot-starter' implementation 'org.springframework.boot:spring-boot-starter-web' //ElasticSearch diff --git a/src/main/java/clap/server/adapter/inbound/web/auth/AuthController.java b/src/main/java/clap/server/adapter/inbound/web/auth/AuthController.java index 79d26333..ae60c027 100644 --- a/src/main/java/clap/server/adapter/inbound/web/auth/AuthController.java +++ b/src/main/java/clap/server/adapter/inbound/web/auth/AuthController.java @@ -3,9 +3,11 @@ import clap.server.adapter.inbound.security.SecurityUserDetails; import clap.server.adapter.inbound.web.dto.auth.LoginRequest; import clap.server.adapter.inbound.web.dto.auth.LoginResponse; +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; import clap.server.application.port.inbound.auth.LoginUsecase; import clap.server.application.port.inbound.auth.LogoutUsecase; import clap.server.common.annotation.architecture.WebAdapter; +import clap.server.config.annotation.LogType; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -27,6 +29,7 @@ public class AuthController { private final LoginUsecase loginUsecase; private final LogoutUsecase logoutUsecase; + @LogType(LogStatus.LOGIN) @Operation(summary = "로그인 API") @PostMapping("/login") public ResponseEntity login(@RequestHeader(name = "sessionId") String sessionId, diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/common/PageResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/common/PageResponse.java new file mode 100644 index 00000000..7ecf8697 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/common/PageResponse.java @@ -0,0 +1,27 @@ +package clap.server.adapter.inbound.web.dto.common; + +import org.springframework.data.domain.Page; + +import java.util.List; + +public record PageResponse( + List content, + long totalElements, + int totalPages, + int pageNumber, + int pageSize, + boolean isFirst, + boolean isLast +) { + public static PageResponse from(Page page) { + return new PageResponse<>( + page.getContent(), + page.getTotalElements(), + page.getTotalPages(), + page.getNumber() + 1, + page.getSize(), + page.isFirst(), + page.isLast() + ); + } +} \ No newline at end of file diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/log/AnonymousLogResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/log/AnonymousLogResponse.java new file mode 100644 index 00000000..bc74da6b --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/log/AnonymousLogResponse.java @@ -0,0 +1,24 @@ +package clap.server.adapter.inbound.web.dto.log; + +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; + +public record AnonymousLogResponse( + @NotBlank + Long logId, + LogStatus logStatus, + @NotBlank + LocalDateTime requestAt, + @NotBlank + String nickName, + String clientIp, + @NotBlank + Integer statusCode, + @NotNull + String customStatusCode, + int failedAttempts +) { +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/log/FilterLogRequest.java b/src/main/java/clap/server/adapter/inbound/web/dto/log/FilterLogRequest.java new file mode 100644 index 00000000..65a9a70c --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/log/FilterLogRequest.java @@ -0,0 +1,19 @@ +package clap.server.adapter.inbound.web.dto.log; + +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotNull; + +import java.util.List; + +public record FilterLogRequest( + @Schema(description = "검색 기간 (단위: 시간)", example = "1, 24, 168, 730, 2190 (1시간, 24시간, 1주일, 1개월, 3개월)") + Integer term, + @NotNull + List logStatus, + @NotNull + String nickName, + @NotNull + String clientIp + ) { +} diff --git a/src/main/java/clap/server/adapter/inbound/web/dto/log/MemberLogResponse.java b/src/main/java/clap/server/adapter/inbound/web/dto/log/MemberLogResponse.java new file mode 100644 index 00000000..1c499ce7 --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/dto/log/MemberLogResponse.java @@ -0,0 +1,20 @@ +package clap.server.adapter.inbound.web.dto.log; + +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +import java.time.LocalDateTime; + +public record MemberLogResponse( + @NotBlank + Long logId, + LogStatus logStatus, + @NotBlank + LocalDateTime responseAt, + String nickName, + String clientIp, + @NotBlank + Integer statusCode +) { +} diff --git a/src/main/java/clap/server/adapter/inbound/web/log/LogController.java b/src/main/java/clap/server/adapter/inbound/web/log/LogController.java new file mode 100644 index 00000000..66ba981d --- /dev/null +++ b/src/main/java/clap/server/adapter/inbound/web/log/LogController.java @@ -0,0 +1,50 @@ +package clap.server.adapter.inbound.web.log; + +import clap.server.adapter.inbound.security.SecurityUserDetails; +import clap.server.adapter.inbound.web.dto.common.PageResponse; +import clap.server.adapter.inbound.web.dto.log.AnonymousLogResponse; +import clap.server.adapter.inbound.web.dto.log.FilterLogRequest; +import clap.server.adapter.inbound.web.dto.log.MemberLogResponse; +import clap.server.application.port.inbound.log.FindApiLogsUsecase; +import clap.server.common.annotation.architecture.WebAdapter; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.annotation.Secured; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@WebAdapter +@RestController +@RequestMapping("/api/logs") +@RequiredArgsConstructor +public class LogController { + + private final FindApiLogsUsecase findApiLogsUsecase; + + @Secured({"ROLE_ADMIN"}) + @GetMapping("/login") + public ResponseEntity> getLoginAttempts( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int pageSize, + @ModelAttribute FilterLogRequest anonymousLogRequest, + @AuthenticationPrincipal SecurityUserDetails userInfo) { + Pageable pageable = PageRequest.of(page, pageSize); + return ResponseEntity.ok(findApiLogsUsecase.filterAnonymousLogs(anonymousLogRequest, pageable)); + } + + @Secured({"ROLE_ADMIN"}) + @GetMapping("/general") + public ResponseEntity> getApiCalls( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "20") int pageSize, + @ModelAttribute FilterLogRequest memberLogRequest, + @AuthenticationPrincipal SecurityUserDetails userInfo) { + Pageable pageable = PageRequest.of(page, pageSize); + return ResponseEntity.ok(findApiLogsUsecase.filterMemberLogs(memberLogRequest, pageable)); + } +} diff --git a/src/main/java/clap/server/adapter/inbound/web/task/FindTaskHistoryController.java b/src/main/java/clap/server/adapter/inbound/web/task/FindTaskHistoryController.java index d4b71d92..9352bce9 100644 --- a/src/main/java/clap/server/adapter/inbound/web/task/FindTaskHistoryController.java +++ b/src/main/java/clap/server/adapter/inbound/web/task/FindTaskHistoryController.java @@ -3,8 +3,10 @@ import clap.server.adapter.inbound.security.SecurityUserDetails; import clap.server.adapter.inbound.web.dto.task.FindTaskDetailsForManagerResponse; import clap.server.adapter.inbound.web.dto.task.response.FindTaskHistoryResponse; +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; import clap.server.application.port.inbound.task.FindTaskHistoriesUsecase; import clap.server.common.annotation.architecture.WebAdapter; +import clap.server.config.annotation.LogType; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; @@ -26,6 +28,7 @@ public class FindTaskHistoryController { private final FindTaskHistoriesUsecase findTaskHistoriesUsecase; + @LogType(LogStatus.TASK_VIEWED) @Operation(summary = "작업 히스토리 조회") @Secured({"ROLE_MANAGER","ROLE_USER"}) @GetMapping("/{taskId}/histories") diff --git a/src/main/java/clap/server/adapter/outbound/persistense/ApiLogPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/ApiLogPersistenceAdapter.java new file mode 100644 index 00000000..dd3d9f25 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/ApiLogPersistenceAdapter.java @@ -0,0 +1,65 @@ +package clap.server.adapter.outbound.persistense; + +import clap.server.adapter.inbound.web.dto.log.AnonymousLogResponse; +import clap.server.adapter.inbound.web.dto.log.FilterLogRequest; + +import clap.server.adapter.inbound.web.dto.log.MemberLogResponse; +import clap.server.adapter.outbound.persistense.mapper.ApiLogPersistenceMapper; +import clap.server.adapter.outbound.persistense.mapper.MemberPersistenceMapper; +import clap.server.adapter.outbound.persistense.repository.log.AnonymousLogRepository; +import clap.server.adapter.outbound.persistense.repository.log.ApiLogRepository; +import clap.server.adapter.outbound.persistense.repository.log.MemberLogRepository; +import clap.server.application.mapper.response.LogMapper; +import clap.server.application.port.outbound.log.CommandLogPort; +import clap.server.application.port.outbound.log.LoadLogPort; +import clap.server.common.annotation.architecture.PersistenceAdapter; +import clap.server.domain.model.log.AnonymousLog; +import clap.server.domain.model.log.ApiLog; +import clap.server.domain.model.log.MemberLog; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +@PersistenceAdapter +@RequiredArgsConstructor +public class ApiLogPersistenceAdapter implements CommandLogPort, LoadLogPort { + + private final ApiLogRepository apiLogRepository; + private final AnonymousLogRepository anonymousLogRepository; + private final MemberLogRepository memberLogRepository; + private final ApiLogPersistenceMapper apiLogPersistenceMapper; + private final MemberPersistenceMapper memberPersistenceMapper; + + @Override + public void saveMemberLog(MemberLog memberLog) { + apiLogRepository.save(apiLogPersistenceMapper.mapMemberLogToEntity(memberLog, memberPersistenceMapper.toEntity(memberLog.getMember()))); + + } + + @Override + public void saveAnonymousLog(AnonymousLog anonymousLog) { + apiLogRepository.save(apiLogPersistenceMapper.mapAnonymousLogToEntity(anonymousLog, anonymousLog.getLoginNickname())); + } + + @Override + public List findAllLogs() { + return apiLogRepository.findAll().stream() + .map(apiLogPersistenceMapper::mapLogEntityToDomain) + .toList(); + } + + @Override + public Page filterMemberLogs(FilterLogRequest memberLogRequest, Pageable pageable) { + return memberLogRepository.filterMemberLogs(memberLogRequest, pageable) + .map(apiLogPersistenceMapper::mapMemberLogEntityToDomain); + } + + @Override + public Page filterAnonymousLogs(FilterLogRequest anonymousLogRequest, Pageable pageable) { + return anonymousLogRepository.filterAnonymousLogs(anonymousLogRequest, pageable) + .map(apiLogPersistenceMapper::mapAnonymousLogEntityToDomain); + } +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/CommentPersistenceAdapter.java b/src/main/java/clap/server/adapter/outbound/persistense/CommentPersistenceAdapter.java index b50ca262..aec21821 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/CommentPersistenceAdapter.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/CommentPersistenceAdapter.java @@ -29,10 +29,4 @@ public Comment saveComment(Comment comment) { CommentEntity commentEntity = commentRepository.save(commentPersistenceMapper.toEntity(comment)); return commentPersistenceMapper.toDomain(commentEntity); } - - @Override - public Comment saveComment(Comment comment) { - CommentEntity commentEntity = commentRepository.save(commentPersistenceMapper.toEntity(comment)); - return commentPersistenceMapper.toDomain(commentEntity); - } } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/entity/log/AnonymousLogEntity.java b/src/main/java/clap/server/adapter/outbound/persistense/entity/log/AnonymousLogEntity.java index 005fd4dc..efb24ac7 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/entity/log/AnonymousLogEntity.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/entity/log/AnonymousLogEntity.java @@ -13,7 +13,8 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @DiscriminatorValue("ANONYMOUS") @SuperBuilder -public class AnonymousLogEntity extends ApiLogEntity{ +public class AnonymousLogEntity extends ApiLogEntity { + @Column private String loginNickname; } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/entity/log/ApiLogEntity.java b/src/main/java/clap/server/adapter/outbound/persistense/entity/log/ApiLogEntity.java index ab8c7d0e..cbeda0fd 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/entity/log/ApiLogEntity.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/entity/log/ApiLogEntity.java @@ -2,6 +2,7 @@ import clap.server.adapter.outbound.persistense.entity.common.BaseTimeEntity; import clap.server.adapter.outbound.persistense.entity.log.constant.ApiHttpMethod; +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -16,15 +17,12 @@ @SuperBuilder @NoArgsConstructor(access = AccessLevel.PROTECTED) @Inheritance(strategy = InheritanceType.SINGLE_TABLE) -@DiscriminatorColumn(name = "DTYPE") -public class ApiLogEntity extends BaseTimeEntity { +@DiscriminatorColumn +public abstract class ApiLogEntity extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long logId; - @Column(nullable = false) - private String serverIp; - @Column(nullable = false) private String clientIp; @@ -42,15 +40,18 @@ public class ApiLogEntity extends BaseTimeEntity { private String customStatusCode; @Column(length = 4096, nullable = false) - private String request; + private String requestBody; @Column(length = 4096, nullable = false) - private String response; + private String responseBody; @Column(nullable = false) private LocalDateTime requestAt; @Column(nullable = false) - private LocalDateTime responseAt; + @Enumerated(EnumType.STRING) + private LogStatus logStatus; -} \ No newline at end of file + @Version + private Long version; +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/entity/log/MemberLogEntity.java b/src/main/java/clap/server/adapter/outbound/persistense/entity/log/MemberLogEntity.java index 0a761ceb..aaa0f0cd 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/entity/log/MemberLogEntity.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/entity/log/MemberLogEntity.java @@ -1,6 +1,7 @@ package clap.server.adapter.outbound.persistense.entity.log; import clap.server.adapter.outbound.persistense.entity.member.MemberEntity; +import clap.server.domain.model.log.ApiLog; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; @@ -12,8 +13,9 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @DiscriminatorValue("MEMBER") @SuperBuilder -public class MemberLogEntity extends ApiLogEntity{ - @ManyToOne(fetch = FetchType.LAZY) +public class MemberLogEntity extends ApiLogEntity { + + @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "member_id") private MemberEntity member; } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/entity/log/constant/ApiHttpMethod.java b/src/main/java/clap/server/adapter/outbound/persistense/entity/log/constant/ApiHttpMethod.java index f9014fa1..71233ab2 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/entity/log/constant/ApiHttpMethod.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/entity/log/constant/ApiHttpMethod.java @@ -10,5 +10,6 @@ public enum ApiHttpMethod { GET, PATCH, PUT, - DELETE; + DELETE, + UNKNOWN; // Logging } diff --git a/src/main/java/clap/server/adapter/outbound/persistense/entity/log/constant/LogStatus.java b/src/main/java/clap/server/adapter/outbound/persistense/entity/log/constant/LogStatus.java new file mode 100644 index 00000000..1824c085 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/entity/log/constant/LogStatus.java @@ -0,0 +1,23 @@ +package clap.server.adapter.outbound.persistense.entity.log.constant; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum LogStatus { + LOGIN("로그인"), + REQUEST_CREATED("요청 생성"), + REQUEST_UPDATED("요청 수정"), + REQUEST_CANCELLED("요청 취소"), + REQUEST_APPROVED("요청 승인"), + ASSIGNER_CHANGED("처리자 변경"), + COMMENT_ADDED("댓글 추가"), + COMMENT_UPDATED("댓글 수정"), + STATUS_CHANGED("작업 상태 변경"), + TASK_COMPLETED("작업 완료"), + TASK_FAILED("작업 실패"), + TASK_VIEWED("작업 조회"); + + private final String description; +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/mapper/ApiLogPersistenceMapper.java b/src/main/java/clap/server/adapter/outbound/persistense/mapper/ApiLogPersistenceMapper.java new file mode 100644 index 00000000..1b986eb1 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/mapper/ApiLogPersistenceMapper.java @@ -0,0 +1,96 @@ +package clap.server.adapter.outbound.persistense.mapper; + +import clap.server.adapter.outbound.persistense.entity.log.AnonymousLogEntity; +import clap.server.adapter.outbound.persistense.entity.log.ApiLogEntity; +import clap.server.adapter.outbound.persistense.entity.log.MemberLogEntity; +import clap.server.adapter.outbound.persistense.entity.log.constant.ApiHttpMethod; +import clap.server.adapter.outbound.persistense.entity.member.MemberEntity; +import clap.server.domain.model.log.AnonymousLog; +import clap.server.domain.model.log.ApiLog; +import clap.server.domain.model.log.MemberLog; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ApiLogPersistenceMapper { + private final MemberPersistenceMapper memberPersistenceMapper; + public AnonymousLogEntity mapAnonymousLogToEntity(ApiLog anonymousLog, String nickName) { + return AnonymousLogEntity.builder() + .clientIp(anonymousLog.getClientIp()) + .requestUrl(anonymousLog.getRequestUrl()) + .requestMethod(ApiHttpMethod.valueOf(anonymousLog.getRequestMethod())) + .statusCode(anonymousLog.getStatusCode()) + .customStatusCode(anonymousLog.getCustomStatusCode()) + .requestBody(anonymousLog.getRequestBody()) + .responseBody(anonymousLog.getResponseBody()) + .requestAt(anonymousLog.getRequestAt()) + .loginNickname(nickName != null ? nickName : "UNKNOWN") + .logStatus(anonymousLog.getLogStatus()) + .build(); + } + + public MemberLogEntity mapMemberLogToEntity(MemberLog memberLog, MemberEntity memberEntity) { + return MemberLogEntity.builder() + .member(memberEntity) + .clientIp(memberLog.getClientIp()) + .requestUrl(memberLog.getRequestUrl()) + .requestMethod(ApiHttpMethod.valueOf(memberLog.getRequestMethod())) + .statusCode(memberLog.getStatusCode()) + .customStatusCode(memberLog.getCustomStatusCode()) + .requestBody(memberLog.getRequestBody()) + .responseBody(memberLog.getResponseBody()) + .requestAt(memberLog.getRequestAt()) + .logStatus(memberLog.getLogStatus()) + .build(); + } + + public AnonymousLog mapAnonymousLogEntityToDomain(AnonymousLogEntity anonymousLogEntity) { + return AnonymousLog.builder() + .logId(anonymousLogEntity.getLogId()) + .clientIp(anonymousLogEntity.getClientIp()) + .requestUrl(anonymousLogEntity.getRequestUrl()) + .requestMethod(anonymousLogEntity.getRequestMethod().name()) + .statusCode(anonymousLogEntity.getStatusCode()) + .customStatusCode(anonymousLogEntity.getCustomStatusCode()) + .requestBody(anonymousLogEntity.getRequestBody()) + .responseBody(anonymousLogEntity.getResponseBody()) + .requestAt(anonymousLogEntity.getRequestAt()) + .logStatus(anonymousLogEntity.getLogStatus()) + .loginNickname(anonymousLogEntity.getLoginNickname()) + .build(); + } + + public MemberLog mapMemberLogEntityToDomain(MemberLogEntity memberLogEntity) { + return MemberLog.builder() + .logId(memberLogEntity.getLogId()) + .clientIp(memberLogEntity.getClientIp()) + .requestUrl(memberLogEntity.getRequestUrl()) + .requestMethod(memberLogEntity.getRequestMethod().name()) + .statusCode(memberLogEntity.getStatusCode()) + .customStatusCode(memberLogEntity.getCustomStatusCode()) + .requestBody(memberLogEntity.getRequestBody()) + .responseBody(memberLogEntity.getResponseBody()) + .requestAt(memberLogEntity.getRequestAt()) + .logStatus(memberLogEntity.getLogStatus()) + .member(memberLogEntity.getMember() != null + ? memberPersistenceMapper.toDomain(memberLogEntity.getMember()) + : null) + .build(); + } + + public ApiLog mapLogEntityToDomain(ApiLogEntity logEntity) { + return ApiLog.builder() + .logId(logEntity.getLogId()) + .clientIp(logEntity.getClientIp()) + .requestUrl(logEntity.getRequestUrl()) + .requestMethod(logEntity.getRequestMethod().name()) + .statusCode(logEntity.getStatusCode()) + .customStatusCode(logEntity.getCustomStatusCode()) + .requestBody(logEntity.getRequestBody()) + .responseBody(logEntity.getResponseBody()) + .requestAt(logEntity.getRequestAt()) + .logStatus(logEntity.getLogStatus()) + .build(); + } +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogCustomRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogCustomRepository.java new file mode 100644 index 00000000..580959ff --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogCustomRepository.java @@ -0,0 +1,10 @@ +package clap.server.adapter.outbound.persistense.repository.log; + +import clap.server.adapter.inbound.web.dto.log.FilterLogRequest; +import clap.server.adapter.outbound.persistense.entity.log.AnonymousLogEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface AnonymousLogCustomRepository { + Page filterAnonymousLogs(FilterLogRequest anonymousLogRequest, Pageable pageable); +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogCustomRepositoryImpl.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogCustomRepositoryImpl.java new file mode 100644 index 00000000..62058480 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogCustomRepositoryImpl.java @@ -0,0 +1,54 @@ +package clap.server.adapter.outbound.persistense.repository.log; + +import clap.server.adapter.inbound.web.dto.log.FilterLogRequest; +import clap.server.adapter.outbound.persistense.entity.log.AnonymousLogEntity; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +import static clap.server.adapter.outbound.persistense.entity.log.QAnonymousLogEntity.anonymousLogEntity; + +@Repository +@RequiredArgsConstructor +public class AnonymousLogCustomRepositoryImpl implements AnonymousLogCustomRepository{ + + private final JPAQueryFactory queryFactory; + @Override + public Page filterAnonymousLogs(FilterLogRequest request, Pageable pageable) { + BooleanBuilder builder = new BooleanBuilder(); + + if (request.term() != null) { + LocalDateTime fromDate = LocalDateTime.now().minusHours(request.term()); + builder.and(anonymousLogEntity.createdAt.after(fromDate)); + } + if (!request.logStatus().isEmpty()) { + builder.and(anonymousLogEntity.logStatus.in(request.logStatus())); + } + if (!request.nickName().isEmpty()) { + builder.and(anonymousLogEntity.loginNickname.contains(request.nickName())); + } + if (!request.clientIp().isEmpty()) { + builder.and(anonymousLogEntity.clientIp.eq(request.clientIp())); + } + + List result = queryFactory + .selectFrom(anonymousLogEntity) + .where(builder) + .orderBy(anonymousLogEntity.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + long total = queryFactory + .selectFrom(anonymousLogEntity) + .where(builder) + .fetch().size(); + return new PageImpl<>(result, pageable, total); + } +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogRepository.java index 6b373f3f..65b684c6 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/AnonymousLogRepository.java @@ -1,9 +1,16 @@ package clap.server.adapter.outbound.persistense.repository.log; +import aj.org.objectweb.asm.commons.Remapper; +import clap.server.adapter.inbound.web.dto.log.FilterLogRequest; import clap.server.adapter.outbound.persistense.entity.log.AnonymousLogEntity; +import clap.server.domain.model.log.AnonymousLog; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository -public interface AnonymousLogRepository extends JpaRepository { -} \ No newline at end of file +public interface AnonymousLogRepository extends JpaRepository, AnonymousLogCustomRepository{ +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/log/MemberLogCustomRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/MemberLogCustomRepository.java new file mode 100644 index 00000000..4ed6242f --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/MemberLogCustomRepository.java @@ -0,0 +1,10 @@ +package clap.server.adapter.outbound.persistense.repository.log; + +import clap.server.adapter.inbound.web.dto.log.FilterLogRequest; +import clap.server.adapter.outbound.persistense.entity.log.MemberLogEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface MemberLogCustomRepository { + Page filterMemberLogs(FilterLogRequest memberLogRequest, Pageable pageable); +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/log/MemberLogCustomRepositoryImpl.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/MemberLogCustomRepositoryImpl.java new file mode 100644 index 00000000..7eebfff9 --- /dev/null +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/MemberLogCustomRepositoryImpl.java @@ -0,0 +1,59 @@ +package clap.server.adapter.outbound.persistense.repository.log; + + +import clap.server.adapter.inbound.web.dto.log.FilterLogRequest; +import clap.server.adapter.outbound.persistense.entity.log.MemberLogEntity; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +import static clap.server.adapter.outbound.persistense.entity.log.QMemberLogEntity.memberLogEntity; +import static clap.server.adapter.outbound.persistense.entity.member.QMemberEntity.memberEntity; + + +@Repository +@RequiredArgsConstructor +public class MemberLogCustomRepositoryImpl implements MemberLogCustomRepository{ + + private final JPAQueryFactory queryFactory; + + @Override + public Page filterMemberLogs(FilterLogRequest request, Pageable pageable) { + BooleanBuilder builder = new BooleanBuilder(); + + if (request.term() != null) { + LocalDateTime fromDate = LocalDateTime.now().minusHours(request.term()); + builder.and(memberLogEntity.createdAt.after(fromDate)); + } + if (!request.logStatus().isEmpty()) { + builder.and(memberLogEntity.logStatus.in(request.logStatus())); + } + if (!request.nickName().isEmpty()) { + builder.and(memberEntity.nickname.contains(request.nickName())); + } + if (!request.clientIp().isEmpty()) { + builder.and(memberLogEntity.clientIp.eq(request.clientIp())); + } + + List result = queryFactory + .selectFrom(memberLogEntity) + .where(builder) + .leftJoin(memberLogEntity.member, memberEntity) + .orderBy(memberLogEntity.createdAt.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + long total = queryFactory + .selectFrom(memberLogEntity) + .where(builder) + .fetch().size(); + return new PageImpl<>(result, pageable, total); + } +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/log/MemberLogRepository.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/MemberLogRepository.java index e6d462f4..8f6500c9 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/log/MemberLogRepository.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/log/MemberLogRepository.java @@ -4,6 +4,8 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; +import java.util.List; + @Repository -public interface MemberLogRepository extends JpaRepository { -} \ No newline at end of file +public interface MemberLogRepository extends JpaRepository, MemberLogCustomRepository { +} diff --git a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java index 991efd1d..f02ddd55 100644 --- a/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java +++ b/src/main/java/clap/server/adapter/outbound/persistense/repository/task/TaskCustomRepositoryImpl.java @@ -9,8 +9,9 @@ import com.querydsl.core.types.dsl.DateTimePath; import com.querydsl.jpa.impl.JPAQueryFactory; import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; import java.time.LocalDateTime; @@ -19,7 +20,6 @@ import static clap.server.adapter.outbound.persistense.entity.task.QTaskEntity.taskEntity; import static com.querydsl.core.types.Order.*; -@Slf4j @Repository @RequiredArgsConstructor public class TaskCustomRepositoryImpl implements TaskCustomRepository { @@ -28,13 +28,13 @@ public class TaskCustomRepositoryImpl implements TaskCustomRepository { @Override public Page findTasksRequestedByUser(Long requesterId, Pageable pageable, FilterTaskListRequest filterTaskListRequest) { - BooleanBuilder whereClause = createFilter(filterTaskListRequest); + BooleanBuilder builder = createFilter(filterTaskListRequest); if (!filterTaskListRequest.nickName().isEmpty()) { - whereClause.and(taskEntity.processor.nickname.eq(filterTaskListRequest.nickName())); + builder.and(taskEntity.processor.nickname.eq(filterTaskListRequest.nickName())); } - whereClause.and(taskEntity.requester.memberId.eq(requesterId)); + builder.and(taskEntity.requester.memberId.eq(requesterId)); - return getTasksPage(pageable, whereClause, filterTaskListRequest.orderRequest().sortBy(), filterTaskListRequest.orderRequest().sortDirection()); + return getTasksPage(pageable, builder, filterTaskListRequest.orderRequest().sortBy(), filterTaskListRequest.orderRequest().sortDirection()); } @Override @@ -50,32 +50,32 @@ public Page findTasksAssignedByManager(Long processorId, Pageable pa @Override public Page findPendingApprovalTasks(Pageable pageable, FilterTaskListRequest filterTaskListRequest) { - BooleanBuilder whereClause = createFilter(filterTaskListRequest); + BooleanBuilder builder = createFilter(filterTaskListRequest); if (!filterTaskListRequest.nickName().isEmpty()) { - whereClause.and(taskEntity.requester.nickname.eq(filterTaskListRequest.nickName())); + builder.and(taskEntity.requester.nickname.eq(filterTaskListRequest.nickName())); } - whereClause.and(taskEntity.taskStatus.eq(TaskStatus.REQUESTED)); - return getTasksPage(pageable, whereClause, filterTaskListRequest.orderRequest().sortBy(), filterTaskListRequest.orderRequest().sortDirection()); + builder.and(taskEntity.taskStatus.eq(TaskStatus.REQUESTED)); + return getTasksPage(pageable, builder, filterTaskListRequest.orderRequest().sortBy(), filterTaskListRequest.orderRequest().sortDirection()); } @Override public Page findAllTasks(Pageable pageable, FilterTaskListRequest filterTaskListRequest) { - BooleanBuilder whereClause = createFilter(filterTaskListRequest); + BooleanBuilder builder = createFilter(filterTaskListRequest); if (!filterTaskListRequest.nickName().isEmpty()) { - whereClause.and( + builder.and( taskEntity.requester.nickname.eq(filterTaskListRequest.nickName()) .or(taskEntity.processor.nickname.eq(filterTaskListRequest.nickName())) ); } - return getTasksPage(pageable, whereClause, filterTaskListRequest.orderRequest().sortBy(), filterTaskListRequest.orderRequest().sortDirection()); + return getTasksPage(pageable, builder, filterTaskListRequest.orderRequest().sortBy(), filterTaskListRequest.orderRequest().sortDirection()); } @Override public List findTasksByFilter(Long processorId, List statuses, LocalDateTime untilDateTime, FilterTaskBoardRequest request, Pageable pageable) { - BooleanBuilder whereClause = createTaskBoardFilter(processorId, statuses, untilDateTime, request); + BooleanBuilder builder = createTaskBoardFilter(processorId, statuses, untilDateTime, request); return queryFactory .selectFrom(taskEntity) - .where(whereClause) + .where(builder) .orderBy(taskEntity.processorOrder.asc()) .limit(pageable.getPageSize() + 1) .offset(pageable.getOffset()) @@ -83,69 +83,67 @@ public List findTasksByFilter(Long processorId, List sta } private BooleanBuilder createTaskBoardFilter(Long processorId, List statuses, LocalDateTime untilDateTime, FilterTaskBoardRequest request) { - BooleanBuilder whereClause = new BooleanBuilder(); + BooleanBuilder builder = new BooleanBuilder(); - whereClause.and(taskEntity.processor.memberId.eq(processorId)); - whereClause.and(taskEntity.taskStatus.in(statuses)); - whereClause.and(taskEntity.finishedAt.isNull().or(taskEntity.finishedAt.loe(untilDateTime))); + builder.and(taskEntity.processor.memberId.eq(processorId)); + builder.and(taskEntity.taskStatus.in(statuses)); + builder.and(taskEntity.finishedAt.isNull().or(taskEntity.finishedAt.loe(untilDateTime))); if (request.labelId() != null) { - whereClause.and(taskEntity.label.labelId.eq(request.labelId())); + builder.and(taskEntity.label.labelId.eq(request.labelId())); } if (request.mainCategoryId() != null) { - whereClause.and(taskEntity.category.mainCategory.categoryId.eq(request.mainCategoryId())); + builder.and(taskEntity.category.mainCategory.categoryId.eq(request.mainCategoryId())); } if (request.subCategoryId() != null) { - whereClause.and(taskEntity.category.categoryId.eq(request.subCategoryId())); + builder.and(taskEntity.category.categoryId.eq(request.subCategoryId())); } if (request.title() != null && !request.title().isEmpty()) { String titleFilter = "%" + request.title() + "%"; - whereClause.and(taskEntity.title.like(titleFilter)); + builder.and(taskEntity.title.like(titleFilter)); } if (request.requesterNickname() != null && !request.requesterNickname().isEmpty()) { String nicknameFilter = "%" + request.requesterNickname().toLowerCase() + "%"; - whereClause.and(taskEntity.requester.nickname.lower().like(nicknameFilter)); + builder.and(taskEntity.requester.nickname.lower().like(nicknameFilter)); } - - return whereClause; + return builder; } private BooleanBuilder createFilter(FilterTaskListRequest request) { - BooleanBuilder whereClause = new BooleanBuilder(); + BooleanBuilder builder = new BooleanBuilder(); if (request.term() != null) { LocalDateTime fromDate = LocalDateTime.now().minusHours(request.term()); - whereClause.and(taskEntity.createdAt.after(fromDate)); + builder.and(taskEntity.createdAt.after(fromDate)); } if (!request.categoryIds().isEmpty()) { - whereClause.and(taskEntity.category.categoryId.in(request.categoryIds())); + builder.and(taskEntity.category.categoryId.in(request.categoryIds())); } if (!request.mainCategoryIds().isEmpty()) { - whereClause.and(taskEntity.category.mainCategory.categoryId.in(request.mainCategoryIds())); + builder.and(taskEntity.category.mainCategory.categoryId.in(request.mainCategoryIds())); } if (!request.title().isEmpty()) { - whereClause.and(taskEntity.title.containsIgnoreCase(request.title())); + builder.and(taskEntity.title.containsIgnoreCase(request.title())); } if (!request.taskStatus().isEmpty()) { - whereClause.and(taskEntity.taskStatus.in(request.taskStatus())); + builder.and(taskEntity.taskStatus.in(request.taskStatus())); } - return whereClause; + return builder; } - - private Page getTasksPage(Pageable pageable, BooleanBuilder whereClause, String sortBy, String sortDirection) { + private Page getTasksPage(Pageable pageable, BooleanBuilder builder, String sortBy, String sortDirection) { OrderSpecifier orderSpecifier = getOrderSpecifier(sortBy, sortDirection); List result = queryFactory .selectFrom(taskEntity) - .where(whereClause) + .where(builder) .orderBy(orderSpecifier) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) .fetch(); long total = queryFactory .selectFrom(taskEntity) - .where(whereClause) + .where(builder) .fetch().size(); return new PageImpl<>(result, pageable, total); } diff --git a/src/main/java/clap/server/application/mapper/response/LogMapper.java b/src/main/java/clap/server/application/mapper/response/LogMapper.java new file mode 100644 index 00000000..b5759cf4 --- /dev/null +++ b/src/main/java/clap/server/application/mapper/response/LogMapper.java @@ -0,0 +1,31 @@ +package clap.server.application.mapper.response; + +import clap.server.adapter.inbound.web.dto.log.AnonymousLogResponse; +import clap.server.adapter.inbound.web.dto.log.MemberLogResponse; +import clap.server.domain.model.log.AnonymousLog; +import clap.server.domain.model.log.MemberLog; + +public class LogMapper { + public static AnonymousLogResponse toAnonymousLogResponse(AnonymousLog anonymousLog, int failedAttempts) { + return new AnonymousLogResponse( + anonymousLog.getLogId(), + anonymousLog.getLogStatus(), + anonymousLog.getRequestAt(), + anonymousLog.getLoginNickname(), + anonymousLog.getClientIp(), + anonymousLog.getStatusCode(), + anonymousLog.getCustomStatusCode(), + failedAttempts + ); + } + public static MemberLogResponse toMemberLogResponse(MemberLog memberLog) { + return new MemberLogResponse( + memberLog.getLogId(), + memberLog.getLogStatus(), + memberLog.getRequestAt(), + memberLog.getMember().getNickname(), + memberLog.getClientIp(), + memberLog.getStatusCode() + ); + } +} diff --git a/src/main/java/clap/server/application/port/inbound/domain/LoginDomainService.java b/src/main/java/clap/server/application/port/inbound/domain/LoginDomainService.java new file mode 100644 index 00000000..d2362601 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/domain/LoginDomainService.java @@ -0,0 +1,12 @@ +package clap.server.application.port.inbound.domain; + +import org.springframework.stereotype.Service; + +@Service +public class LoginDomainService { + + public int getFailedAttemptCount(String loginNickname) { + //TODO: 로그인 실패 횟수 계산 로직 추가 + return 3; + } +} diff --git a/src/main/java/clap/server/application/port/inbound/log/CreateAnonymousLogsUsecase.java b/src/main/java/clap/server/application/port/inbound/log/CreateAnonymousLogsUsecase.java new file mode 100644 index 00000000..e21c6f5c --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/log/CreateAnonymousLogsUsecase.java @@ -0,0 +1,12 @@ +package clap.server.application.port.inbound.log; + +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.time.LocalDateTime; + +public interface CreateAnonymousLogsUsecase { + void createAnonymousLog(HttpServletRequest request, HttpServletResponse response, Object result, LogStatus logType, String customCode, String requestBody, String nicknameFromRequestBody); + +} diff --git a/src/main/java/clap/server/application/port/inbound/log/CreateMemberLogsUsecase.java b/src/main/java/clap/server/application/port/inbound/log/CreateMemberLogsUsecase.java new file mode 100644 index 00000000..b6870930 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/log/CreateMemberLogsUsecase.java @@ -0,0 +1,12 @@ +package clap.server.application.port.inbound.log; + +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.time.LocalDateTime; + +public interface CreateMemberLogsUsecase { + + void createMemberLog(HttpServletRequest request, HttpServletResponse response, Object result, LogStatus logType, String customCode, String body, Long userId); +} diff --git a/src/main/java/clap/server/application/port/inbound/log/FindApiLogsUsecase.java b/src/main/java/clap/server/application/port/inbound/log/FindApiLogsUsecase.java new file mode 100644 index 00000000..35c7f644 --- /dev/null +++ b/src/main/java/clap/server/application/port/inbound/log/FindApiLogsUsecase.java @@ -0,0 +1,18 @@ +package clap.server.application.port.inbound.log; + + +import clap.server.adapter.inbound.web.dto.common.PageResponse; +import clap.server.adapter.inbound.web.dto.log.AnonymousLogResponse; +import clap.server.adapter.inbound.web.dto.log.FilterLogRequest; +import clap.server.adapter.inbound.web.dto.log.MemberLogResponse; +import clap.server.domain.model.log.ApiLog; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface FindApiLogsUsecase { + PageResponse filterAnonymousLogs(FilterLogRequest anonymousLogsRequest, Pageable pageable); + PageResponse filterMemberLogs(FilterLogRequest memberLogRequest, Pageable pageable); + List getApiLogs(); +} \ No newline at end of file diff --git a/src/main/java/clap/server/application/port/outbound/log/CommandLogPort.java b/src/main/java/clap/server/application/port/outbound/log/CommandLogPort.java new file mode 100644 index 00000000..ed243aad --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/log/CommandLogPort.java @@ -0,0 +1,9 @@ +package clap.server.application.port.outbound.log; + +import clap.server.domain.model.log.AnonymousLog; +import clap.server.domain.model.log.MemberLog; + +public interface CommandLogPort { + void saveMemberLog(MemberLog memberLog); + void saveAnonymousLog(AnonymousLog anonymousLog); +} diff --git a/src/main/java/clap/server/application/port/outbound/log/LoadLogPort.java b/src/main/java/clap/server/application/port/outbound/log/LoadLogPort.java new file mode 100644 index 00000000..4ee39ae2 --- /dev/null +++ b/src/main/java/clap/server/application/port/outbound/log/LoadLogPort.java @@ -0,0 +1,19 @@ +package clap.server.application.port.outbound.log; + +import clap.server.adapter.inbound.web.dto.log.AnonymousLogResponse; +import clap.server.adapter.inbound.web.dto.log.FilterLogRequest; +import clap.server.adapter.inbound.web.dto.log.MemberLogResponse; +import clap.server.domain.model.log.AnonymousLog; +import clap.server.domain.model.log.ApiLog; +import clap.server.domain.model.log.MemberLog; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface LoadLogPort { + List findAllLogs(); + Page filterAnonymousLogs(FilterLogRequest anonymousLogRequest, Pageable pageable); + + Page filterMemberLogs(FilterLogRequest memberLogRequest, Pageable pageable); +} diff --git a/src/main/java/clap/server/application/service/log/CreateAnonymousLogsService.java b/src/main/java/clap/server/application/service/log/CreateAnonymousLogsService.java new file mode 100644 index 00000000..f2152d64 --- /dev/null +++ b/src/main/java/clap/server/application/service/log/CreateAnonymousLogsService.java @@ -0,0 +1,25 @@ +package clap.server.application.service.log; + +import clap.server.adapter.outbound.persistense.ApiLogPersistenceAdapter; +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import clap.server.application.port.inbound.log.CreateAnonymousLogsUsecase; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.domain.model.log.AnonymousLog; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; + +import java.time.LocalDateTime; + +@ApplicationService +@RequiredArgsConstructor +public class CreateAnonymousLogsService implements CreateAnonymousLogsUsecase { + private final ApiLogPersistenceAdapter apiLogPersistenceAdapter; + + @Override + public void createAnonymousLog(HttpServletRequest request, HttpServletResponse response, Object result, LogStatus logType, String customCode, String body, String nickName) { + AnonymousLog anonymousLog = AnonymousLog.createAnonymousLog(request, response, result, logType, customCode, body, nickName); + apiLogPersistenceAdapter.saveAnonymousLog(anonymousLog); + } +} diff --git a/src/main/java/clap/server/application/service/log/CreateMemberLogsService.java b/src/main/java/clap/server/application/service/log/CreateMemberLogsService.java new file mode 100644 index 00000000..9026e8f5 --- /dev/null +++ b/src/main/java/clap/server/application/service/log/CreateMemberLogsService.java @@ -0,0 +1,31 @@ +package clap.server.application.service.log; + +import clap.server.adapter.outbound.persistense.ApiLogPersistenceAdapter; +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import clap.server.application.port.inbound.domain.MemberService; +import clap.server.application.port.inbound.log.CreateMemberLogsUsecase; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.domain.model.log.MemberLog; +import clap.server.domain.model.member.Member; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; + +@ApplicationService +@RequiredArgsConstructor +public class CreateMemberLogsService implements CreateMemberLogsUsecase { + + private final ApiLogPersistenceAdapter apiLogPersistenceAdapter; + private final MemberService memberService; + + @Override + @Transactional + public void createMemberLog(HttpServletRequest request, HttpServletResponse response, Object result, LogStatus logType, String customCode, String body, Long userId) { + Member member = memberService.findById(userId); + MemberLog memberLog = MemberLog.createMemberLog(request, response, result, logType, customCode, body, member); + apiLogPersistenceAdapter.saveMemberLog(memberLog); + } +} diff --git a/src/main/java/clap/server/application/service/log/FindApiLogsService.java b/src/main/java/clap/server/application/service/log/FindApiLogsService.java new file mode 100644 index 00000000..0798c598 --- /dev/null +++ b/src/main/java/clap/server/application/service/log/FindApiLogsService.java @@ -0,0 +1,54 @@ +package clap.server.application.service.log; + +import clap.server.adapter.inbound.web.dto.common.PageResponse; +import clap.server.adapter.inbound.web.dto.log.AnonymousLogResponse; +import clap.server.adapter.inbound.web.dto.log.FilterLogRequest; +import clap.server.adapter.inbound.web.dto.log.MemberLogResponse; +import clap.server.adapter.outbound.persistense.ApiLogPersistenceAdapter; +import clap.server.application.mapper.response.LogMapper; +import clap.server.application.port.inbound.domain.LoginDomainService; +import clap.server.application.port.inbound.log.FindApiLogsUsecase; +import clap.server.application.port.outbound.log.LoadLogPort; +import clap.server.common.annotation.architecture.ApplicationService; +import clap.server.domain.model.log.AnonymousLog; +import clap.server.domain.model.log.ApiLog; +import clap.server.domain.model.log.MemberLog; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@ApplicationService +@RequiredArgsConstructor +@Transactional(readOnly = true) +public class FindApiLogsService implements FindApiLogsUsecase { + + private final ApiLogPersistenceAdapter apiLogPersistenceAdapter; + private final LoginDomainService loginDomainService; + private final LoadLogPort loadLogPort; + + @Override + public PageResponse filterAnonymousLogs(FilterLogRequest anonymousLogRequest, Pageable pageable) { + Page anonymousLogs = loadLogPort.filterAnonymousLogs(anonymousLogRequest, pageable); + Page anonymousLogResponses = anonymousLogs.map(anonymousLog -> { + int failedAttempts = loginDomainService.getFailedAttemptCount(anonymousLog.getLoginNickname()); + return LogMapper.toAnonymousLogResponse(anonymousLog, failedAttempts); + }); + return PageResponse.from(anonymousLogResponses); + } + + @Override + public PageResponse filterMemberLogs(FilterLogRequest memberLogRequest, Pageable pageable) { + Page memberLogs = loadLogPort.filterMemberLogs(memberLogRequest, pageable); + Page memberLogResponses = memberLogs.map(LogMapper::toMemberLogResponse); + return PageResponse.from(memberLogResponses); + } + + //테스트용 + @Override + public List getApiLogs() { + return apiLogPersistenceAdapter.findAllLogs(); + } +} diff --git a/src/main/java/clap/server/config/annotation/LogType.java b/src/main/java/clap/server/config/annotation/LogType.java new file mode 100644 index 00000000..fc39f225 --- /dev/null +++ b/src/main/java/clap/server/config/annotation/LogType.java @@ -0,0 +1,14 @@ +package clap.server.config.annotation; + +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; + +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) +public @interface LogType { + LogStatus value(); +} diff --git a/src/main/java/clap/server/config/aop/LoggingAspect.java b/src/main/java/clap/server/config/aop/LoggingAspect.java new file mode 100644 index 00000000..5355d373 --- /dev/null +++ b/src/main/java/clap/server/config/aop/LoggingAspect.java @@ -0,0 +1,128 @@ +package clap.server.config.aop; + +import clap.server.adapter.inbound.security.SecurityUserDetails; + +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import clap.server.application.port.inbound.log.CreateAnonymousLogsUsecase; +import clap.server.application.port.inbound.log.CreateMemberLogsUsecase; +import clap.server.config.annotation.LogType; +import clap.server.exception.ErrorContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.util.ContentCachingRequestWrapper; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; + +@Slf4j +@Aspect +@Component +@RequiredArgsConstructor +public class LoggingAspect { + private final ObjectMapper objectMapper; + private final CreateAnonymousLogsUsecase createAnonymousLogsUsecase; + private final CreateMemberLogsUsecase createMemberLogsUsecase; + + @Pointcut("execution(* clap.server.adapter.inbound.web..*Controller.*(..))") + public void controllerMethods() { + } + + @Around("controllerMethods()") + public Object logApiRequests(ProceedingJoinPoint joinPoint) throws Throwable { + ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); + HttpServletRequest request = attributes.getRequest(); + if (!(request instanceof ContentCachingRequestWrapper)) { + request = new ContentCachingRequestWrapper(request); + } + HttpServletResponse response = attributes.getResponse(); + + Object result = null; + try { + result = joinPoint.proceed(); + } catch (Exception ex) { + log.error("Exception occurred: {}", ex.getMessage()); + log.info("response.getStatus()={}",response.getStatus()); + log.info("getRequestBody()={}", getRequestBody(request)); + throw ex; + } finally { + MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); + LogStatus logType = getLogType(methodSignature); + String customCode = getCustomCode(response); + if (logType != null) { + if (LogStatus.LOGIN.equals(logType)) { + log.info("result={}",result); + log.info("response.getStatus()={}",response.getStatus()); + log.info("getRequestBody()={}", getRequestBody(request)); + log.info("getNicknameFromRequestBody()={}", getNicknameFromRequestBody(request)); + createAnonymousLogsUsecase.createAnonymousLog(request, response, result, logType, customCode, getRequestBody(request), getNicknameFromRequestBody(request)); + } else { + if (!isUserAuthenticated()) { + log.error("로그인 시도 로그를 기록할 수 없음"); + } else { + Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + if (principal instanceof SecurityUserDetails userDetails) { + createMemberLogsUsecase.createMemberLog(request, response, result, logType, customCode, getRequestBody(request), userDetails.getUserId()); + } + } + } + } + } + return result; + } + + private LogStatus getLogType(MethodSignature methodSignature) { + if (methodSignature.getMethod().isAnnotationPresent(LogType.class)) { + return methodSignature.getMethod().getAnnotation(LogType.class).value(); + } else { + return null; + } + } + + //TODO: 로그인 시도에 대한 에러 잡도록 수정 + private String getCustomCode(HttpServletResponse response) { + String customCode = ErrorContext.getCustomCode(); + return customCode != null ? customCode : "CUSTOM" + (response != null ? response.getStatus() : 500); + } + + //TODO: 로그인 시도 시 닉네임 파싱하도록 수정 + private String getNicknameFromRequestBody(HttpServletRequest request) { + try { + String requestBody = getRequestBody(request); + JsonNode jsonNode = objectMapper.readTree(requestBody); + return jsonNode.has("nickname") ? jsonNode.get("nickname").asText() : null; + } catch (Exception e) { + return null; + } + } + + //TODO: 제거 + private String getRequestBody(HttpServletRequest request) { + try { + ContentCachingRequestWrapper cachingRequest = (ContentCachingRequestWrapper) request; + byte[] content = cachingRequest.getContentAsByteArray(); + return new String(content, StandardCharsets.UTF_8); + } catch (Exception e) { + return "ERROR: Unable to read request body"; + } + } + + private boolean isUserAuthenticated() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return authentication != null && authentication.isAuthenticated() + && !"anonymousUser".equals(authentication.getPrincipal()); + } +} diff --git a/src/main/java/clap/server/domain/model/log/AnonymousLog.java b/src/main/java/clap/server/domain/model/log/AnonymousLog.java index 9c48e102..c0324740 100644 --- a/src/main/java/clap/server/domain/model/log/AnonymousLog.java +++ b/src/main/java/clap/server/domain/model/log/AnonymousLog.java @@ -1,12 +1,33 @@ package clap.server.domain.model.log; +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import clap.server.common.utils.ClientIpParseUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import java.time.LocalDateTime; + @Getter @SuperBuilder @NoArgsConstructor public class AnonymousLog extends ApiLog { private String loginNickname; + + public static AnonymousLog createAnonymousLog(HttpServletRequest request, HttpServletResponse response, Object responseResult, LogStatus logStatus, String customCode, String requestBody, String nickName) { + return AnonymousLog.builder() + .clientIp(ClientIpParseUtil.getClientIp(request)) + .requestUrl(request.getRequestURI()) + .requestMethod(request.getMethod()) + .statusCode(response.getStatus()) + .customStatusCode(customCode) + .requestBody(requestBody) + .responseBody(responseResult != null ? responseResult.toString() : "로그인 실패") + .requestAt(LocalDateTime.now()) + .logStatus(logStatus) + .loginNickname(nickName) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/clap/server/domain/model/log/ApiLog.java b/src/main/java/clap/server/domain/model/log/ApiLog.java index 045aab1a..061b5764 100644 --- a/src/main/java/clap/server/domain/model/log/ApiLog.java +++ b/src/main/java/clap/server/domain/model/log/ApiLog.java @@ -1,27 +1,25 @@ package clap.server.domain.model.log; +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; import clap.server.domain.model.common.BaseTime; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; import lombok.experimental.SuperBuilder; import java.time.LocalDateTime; @Getter @SuperBuilder +@AllArgsConstructor @NoArgsConstructor(access = AccessLevel.PROTECTED) public class ApiLog extends BaseTime { private Long logId; + private String clientIp; + private String requestUrl; + private String requestMethod; private Integer statusCode; - private Long memberId; - private LocalDateTime requestAt; - private LocalDateTime responseAt; - private String dtype; - private String request; - private String requestUrl; - private String response; - private String clientIp; private String customStatusCode; - private String serverIp; - } + private String requestBody; + private String responseBody; + private LocalDateTime requestAt; + private LogStatus logStatus; +} diff --git a/src/main/java/clap/server/domain/model/log/MemberLog.java b/src/main/java/clap/server/domain/model/log/MemberLog.java index 44483faf..566887d9 100644 --- a/src/main/java/clap/server/domain/model/log/MemberLog.java +++ b/src/main/java/clap/server/domain/model/log/MemberLog.java @@ -1,13 +1,34 @@ package clap.server.domain.model.log; +import clap.server.adapter.outbound.persistense.entity.log.constant.LogStatus; +import clap.server.common.utils.ClientIpParseUtil; import clap.server.domain.model.member.Member; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.experimental.SuperBuilder; +import java.time.LocalDateTime; + @Getter @SuperBuilder @NoArgsConstructor public class MemberLog extends ApiLog { private Member member; + + public static MemberLog createMemberLog(HttpServletRequest request, HttpServletResponse response, Object responseResult, LogStatus logStatus, String customCode, String requestBody, Member member) { + return MemberLog.builder() + .clientIp(ClientIpParseUtil.getClientIp(request)) + .requestUrl(request.getRequestURI()) + .requestMethod(request.getMethod()) + .statusCode(response.getStatus()) + .customStatusCode(customCode) + .requestBody(requestBody) + .responseBody(responseResult != null ? responseResult.toString() : "UNKNOWN") + .requestAt(LocalDateTime.now()) + .logStatus(logStatus) + .member(member) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/clap/server/exception/ErrorContext.java b/src/main/java/clap/server/exception/ErrorContext.java new file mode 100644 index 00000000..bc9221b3 --- /dev/null +++ b/src/main/java/clap/server/exception/ErrorContext.java @@ -0,0 +1,21 @@ +package clap.server.exception; + +public class ErrorContext { + + private static final ThreadLocal customCodeHolder = new ThreadLocal<>(); + + // customCode 설정 + public static void setCustomCode(String customCode) { + customCodeHolder.set(customCode); + } + + // customCode 가져오기 + public static String getCustomCode() { + return customCodeHolder.get(); + } + + // ThreadLocal 초기화 + public static void clear() { + customCodeHolder.remove(); + } +}