diff --git a/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java index 61998c80c2383b070315e5b7068e15aacc7ce773..30df250f9ffd08dbf07b458750c7c464409faf82 100644 --- a/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java +++ b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java @@ -23,6 +23,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.Objects; @@ -36,6 +37,8 @@ public class CertificateLogAspect { private final LogRepository logRepository; private final EntityManager entityManager; + private static final DateTimeFormatter LOG_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + /* Create 로깅 */ @AfterReturning(pointcut = "execution(* com.aolda.itda.service.certificate.*Service.*create*(..))" , returning = "result") @@ -55,7 +58,7 @@ public class CertificateLogAspect { String description = "domain: " + certificate.getDomain() + "\n" + "email: " + certificate.getEmail() + "\n" + "challenge: " + certificate.getChallenge() + "\n" - + "expiresAt: " + certificate.getExpiresAt(); + + "expiresAt: " + (certificate.getExpiresAt() != null ? certificate.getExpiresAt().format(LOG_DATE_FORMATTER) : "null"); /* 로그 엔티티 저장 */ logRepository.save(Log.builder() @@ -89,7 +92,7 @@ public class CertificateLogAspect { String description = "domain: " + certificate.getDomain() + "\n" + "email: " + certificate.getEmail() + "\n" + "challenge: " + certificate.getChallenge() + "\n" - + "expiresAt: " + certificate.getExpiresAt(); + + "expiresAt: " + (certificate.getExpiresAt() != null ? certificate.getExpiresAt().format(LOG_DATE_FORMATTER) : "null"); /* 로그 엔티티 저장 */ logRepository.save(Log.builder() @@ -132,7 +135,8 @@ public class CertificateLogAspect { String description = "domain: " + old.getDomain() + (old.getDomain().equals(newObj.getDomain()) ? "" : " -> " + newObj.getDomain()) + "\n" + "email: " + old.getEmail() + (old.getEmail().equals(newObj.getEmail()) ? "" : " -> " + newObj.getEmail()) + "\n" + "challenge: " + old.getChallenge() + (old.getChallenge() == newObj.getChallenge() ? "" : " -> " + newObj.getChallenge()) + "\n" - + "expiresAt: " + old.getExpiresAt() + (old.getExpiresAt().equals(newObj.getExpiresAt()) ? "" : " -> " + newObj.getExpiresAt()); + + "expiresAt: " + (old.getExpiresAt() != null ? old.getExpiresAt().format(LOG_DATE_FORMATTER) : "null") + + (old.getExpiresAt() != null && newObj.getExpiresAt() != null && !old.getExpiresAt().equals(newObj.getExpiresAt()) ? " -> " + newObj.getExpiresAt().format(LOG_DATE_FORMATTER) : ""); /* 로그 엔티티 저장 */ logRepository.save(Log.builder() diff --git a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java index d08968bd39f0076c4ffcaf6d3881b792020f8c7c..26a0322cc29fb6c642ccafbf9dd5cce005030a9e 100644 --- a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java +++ b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java @@ -59,8 +59,18 @@ public class CertificateController { @GetMapping("/certificates") public ResponseEntity<PageResp<CertificateDTO>> lists( @RequestParam String projectId, - @RequestParam(required = false) String domain + @RequestParam(required = false) String domain, + @RequestParam(required = false) String query ) { + + if (query != null) { + return ResponseEntity.ok( + certificateService.getCertificatesWithSearch( + projectId, + query + ) + ); + } return ResponseEntity.ok( certificateService.getCertificates(projectId, domain) ); diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java index 23b9e95b71c2fbbcf5aaf86171f29b13d363b6d3..058fdd789d0f2c53bbda5ac10469bd9e72d6313b 100644 --- a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java +++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java @@ -16,7 +16,7 @@ import java.time.LocalDateTime; public class CertificateDTO { private Long id; // 인증서 고유 ID -// private String projectId; // 프로젝트 식별자 + private String projectId; // 프로젝트 식별자 private String domain; // SSL 인증받을 도메인 주소 private String email; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")// 도메인 소유자의 이메일 diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java index 8ebc45449a7e18a768ea85f34f2d55f46ff3c83f..9161b45397475cd8555591d962df43f3c8684d78 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -34,6 +34,7 @@ public class Certificate extends BaseTimeEntity { @Setter @Column(columnDefinition = "DATETIME") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") private LocalDateTime expiresAt; //인증서 만료일 @Enumerated(EnumType.STRING) diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java index 6e0996b4ffadd631e3506332242de193f602b3ef..0f3c137a9795352e199bf81f2df8e081812a82cc 100644 --- a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java +++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java @@ -1,7 +1,9 @@ package com.aolda.itda.repository.certificate; import com.aolda.itda.entity.certificate.Certificate; +import com.aolda.itda.entity.routing.Routing; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.time.LocalDateTime; import java.util.List; @@ -19,10 +21,13 @@ public interface CertificateRepository extends JpaRepository<Certificate, Long> //List<Certificate> findByExpiresAtBeforeAndIsDeleted(LocalDateTime date, Boolean isDeleted); // 1) domain 필터링용 메서드 - List<Certificate> findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted( + List<Certificate> findByProjectIdAndDomainContainingAndIsDeleted( String projectId, String domain, Boolean isDeleted); // 3) 만료 30일 이내 대상 조회 List<Certificate> findByExpiresAtBeforeAndIsDeleted( LocalDateTime date, Boolean isDeleted); + + @Query("SELECT r FROM Routing r WHERE r.projectId = ?1 AND r.isDeleted = ?3 AND (r.domain LIKE %?2% OR r.instanceIp LIKE %?2% OR r.name LIKE %?2%)") + List<Certificate> findWithSearch(String projectId, String query, Boolean isDeleted); } diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java index f8d8cfe69aceff9f00c99937293740446ab67b25..7f426709d5480a8ed5564376ec12616c3a8bb8e6 100644 --- a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java +++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java @@ -2,8 +2,10 @@ package com.aolda.itda.service.certificate; import com.aolda.itda.dto.PageResp; import com.aolda.itda.dto.certificate.CertificateDTO; +import com.aolda.itda.dto.routing.RoutingDTO; import com.aolda.itda.entity.certificate.Certificate; import com.aolda.itda.entity.certificate.Challenge; +import com.aolda.itda.entity.routing.Routing; import com.aolda.itda.exception.CustomException; import com.aolda.itda.exception.ErrorCode; import com.aolda.itda.repository.certificate.CertificateRepository; @@ -12,6 +14,7 @@ import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,7 +24,10 @@ import java.io.IOException; import java.io.InputStreamReader; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; @Service @Transactional @@ -29,6 +35,8 @@ import java.util.List; @Slf4j public class CertificateService { + @Value("${spring.server.admin-project}") + private String adminProject; private final CertificateRepository certificateRepository; private final AuthService authService; @@ -41,18 +49,67 @@ public class CertificateService { return toDTO(cert); } + /* Certificate 목록 조회 + 검색 */ + public PageResp<CertificateDTO> getCertificatesWithSearch(String projectId, String query) { + + /* 입력 검증 */ + if (query == null || query.isBlank()) { + return PageResp.<CertificateDTO>builder() + .contents(certificateRepository.findByProjectIdAndIsDeleted(projectId, false) + .stream() + .map(this::toDTO) + .toList()).build(); + } + + /* 도메인 패턴 검증 */ + String domainPattern = "^(\\*\\.)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"; + if (Pattern.matches(domainPattern, query) && query.startsWith("*.")) { + query = query.substring(2); + } + + return PageResp.<CertificateDTO>builder() + .contents(certificateRepository.findWithSearch(projectId, query, false) + .stream() + .map(this::toDTO) + .toList()).build(); + } + /** 1) 목록 조회 (domain 필터 optional) **/ public PageResp<CertificateDTO> getCertificates(String projectId, String domain) { - List<Certificate> list; + Set<Certificate> set = new HashSet<>(); + // 도메인이 입력된 경우 처리 if (domain != null && !domain.isBlank()) { - list = certificateRepository - .findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted( - projectId, domain, false); + // 서브도메인이 있는 경우 + if (domain.indexOf('.') != domain.lastIndexOf('.')) { + String wildcardDomain = "*." + domain.substring(domain.indexOf('.') + 1); + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + projectId, wildcardDomain, false)); + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + projectId, domain, false)); + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + adminProject, wildcardDomain, false)); + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + adminProject, domain, false)); + } else { + // 서브도메인이 없는 경우 일반 검색 + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + projectId, domain, false)); + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + adminProject, domain, false)); + } } else { - list = certificateRepository - .findByProjectIdAndIsDeleted(projectId, false); + set.addAll(certificateRepository + .findByProjectIdAndIsDeleted(projectId, false)); + set.addAll(certificateRepository + .findByProjectIdAndIsDeleted(adminProject, false)); } - List<CertificateDTO> dtos = list.stream() + List<CertificateDTO> dtos = set.stream() .map(this::toDTO) .toList(); return PageResp.<CertificateDTO>builder() @@ -171,6 +228,7 @@ public class CertificateService { .updatedAt(cert.getUpdatedAt()) .isDeleted(cert.getIsDeleted()) .apiToken(cert.getApiToken()) + .projectId(cert.getProjectId()) .build(); }