diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..3742c4db297857fdcb046f38032f0616bdbdddfd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,23 @@ +FROM gradle:jdk21 AS build + +WORKDIR /tmp +RUN wget -O lego.tar.gz "https://github.com/go-acme/lego/releases/download/v4.22.2/lego_v4.22.2_linux_amd64.tar.gz" && tar -xzf lego.tar.gz && rm -f lego.tar.gz + +WORKDIR /home/gradle/project + +COPY --chown=gradle:gradle build.gradle settings.gradle . +RUN gradle dependencies --no-daemon + +COPY --chown=gradle:gradle src ./src +RUN gradle clean bootJar --no-daemon + +FROM eclipse-temurin:21-jre-alpine + +COPY --from=build /tmp/lego /usr/local/bin/lego +RUN chmod +x /usr/local/bin/lego + +WORKDIR /app +COPY --from=build /home/gradle/project/build/libs/*.jar app.jar + +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] diff --git a/build.gradle b/build.gradle index 9305e703fb238deb9b38371a98864482965a4253..1b29e4ade60f1ece03c8e7680004ea772cfc1ef3 100644 --- a/build.gradle +++ b/build.gradle @@ -26,13 +26,23 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' + runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor "jakarta.annotation:jakarta.annotation-api" + annotationProcessor "jakarta.persistence:jakarta.persistence-api" + annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta" + } tasks.named('test') { useJUnitPlatform() } +clean { + delete file('src/main/generated') +} \ No newline at end of file diff --git a/src/main/java/com/aolda/itda/ItdaApplication.java b/src/main/java/com/aolda/itda/ItdaApplication.java index c7f4ef7eb3aae27b2c85e6297c9e74317f4ccdf6..674e65780f404587b1f8a528c8cb03087f919e60 100644 --- a/src/main/java/com/aolda/itda/ItdaApplication.java +++ b/src/main/java/com/aolda/itda/ItdaApplication.java @@ -2,8 +2,10 @@ package com.aolda.itda; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class ItdaApplication { public static void main(String[] args) { diff --git a/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..30df250f9ffd08dbf07b458750c7c464409faf82 --- /dev/null +++ b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java @@ -0,0 +1,152 @@ +package com.aolda.itda.aspect; + +import com.aolda.itda.dto.certificate.CertificateDTO; +import com.aolda.itda.entity.certificate.Certificate; +import com.aolda.itda.entity.log.Action; +import com.aolda.itda.entity.log.Log; +import com.aolda.itda.entity.log.ObjectType; +import com.aolda.itda.entity.user.User; +import com.aolda.itda.exception.CustomException; +import com.aolda.itda.exception.ErrorCode; +import com.aolda.itda.repository.certificate.CertificateRepository; +import com.aolda.itda.repository.log.LogRepository; +import com.aolda.itda.repository.user.UserRepository; +import jakarta.persistence.EntityManager; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +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; + +@Aspect +@Component +@RequiredArgsConstructor +public class CertificateLogAspect { + + private final CertificateRepository certificateRepository; + private final UserRepository userRepository; + 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") + public void createLogging(JoinPoint joinPoint, CertificateDTO result) { + + /* 사용자 조회 */ + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); + User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( + () -> new CustomException(ErrorCode.NOT_FOUND_USER) + ); + + /* 생성된 엔티티 조회 */ + Certificate certificate = certificateRepository.findByCertificateIdAndIsDeleted(result.getId(), false).orElse(null); + + /* 로그 메세지 작성 */ + String description = "domain: " + certificate.getDomain() + "\n" + + "email: " + certificate.getEmail() + "\n" + + "challenge: " + certificate.getChallenge() + "\n" + + "expiresAt: " + (certificate.getExpiresAt() != null ? certificate.getExpiresAt().format(LOG_DATE_FORMATTER) : "null"); + + /* 로그 엔티티 저장 */ + logRepository.save(Log.builder() + .user(user) + .objectType(ObjectType.CERTIFICATE) + .objectId(certificate.getCertificateId()) + .action(Action.CREATE) + .projectId(certificate.getProjectId()) + .description(description) + .build()); + } + + /* Delete 로깅 */ + @AfterReturning(pointcut = "execution(* com.aolda.itda.service.certificate.*Service.*delete*(..))") + public void deleteLogging(JoinPoint joinPoint) { + + /* 사용자 조회 */ + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); + User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( + () -> new CustomException(ErrorCode.NOT_FOUND_USER) + ); + + /* 삭제된 엔티티 조회 */ + Object[] args = joinPoint.getArgs(); + + Long id = (Long) args[0]; + Certificate certificate = certificateRepository.findByCertificateIdAndIsDeleted(id, true).orElse(null); + + /* 로그 메세지 작성 */ + String description = "domain: " + certificate.getDomain() + "\n" + + "email: " + certificate.getEmail() + "\n" + + "challenge: " + certificate.getChallenge() + "\n" + + "expiresAt: " + (certificate.getExpiresAt() != null ? certificate.getExpiresAt().format(LOG_DATE_FORMATTER) : "null"); + + /* 로그 엔티티 저장 */ + logRepository.save(Log.builder() + .user(user) + .objectType(ObjectType.CERTIFICATE) + .objectId(certificate.getCertificateId()) + .action(Action.DELETE) + .projectId(certificate.getProjectId()) + .description(description) + .build()); + } + + /* Update(edit) 로깅 */ + @Around("execution(* com.aolda.itda.service.certificate.*Service.*edit*(..))") + public Object editLogging(ProceedingJoinPoint joinPoint) throws Throwable { + + /* 사용자 조회 */ + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); + User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( + () -> new CustomException(ErrorCode.NOT_FOUND_USER) + ); + + /* 변경 전 엔티티 조회 */ + Object[] args = joinPoint.getArgs(); + + Long id = (Long) args[0]; + Certificate old = certificateRepository.findByCertificateIdAndIsDeleted(id, false).orElse(null); + if (old != null) { + entityManager.detach(old); + } + + /* 메소드 진행 */ + Object result = joinPoint.proceed(); + + /* 변경 후 엔티티 조회 */ + Certificate newObj = certificateRepository.findByCertificateIdAndIsDeleted(id, false).orElse(null); + + /* 로그 메세지 작성 */ + 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() != 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() + .user(user) + .objectType(ObjectType.CERTIFICATE) + .objectId(newObj.getCertificateId()) + .action(Action.UPDATE) + .projectId(newObj.getProjectId()) + .description(description) + .build()); + return result; + } +} \ No newline at end of file diff --git a/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..d50c2feaaedc63a3b5dbac4fa28666ee146ca2a0 --- /dev/null +++ b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java @@ -0,0 +1,155 @@ +package com.aolda.itda.aspect; + +import com.aolda.itda.dto.forwarding.ForwardingDTO; +import com.aolda.itda.entity.forwarding.Forwarding; +import com.aolda.itda.entity.log.Action; +import com.aolda.itda.entity.log.Log; +import com.aolda.itda.entity.log.ObjectType; +import com.aolda.itda.entity.user.User; +import com.aolda.itda.exception.CustomException; +import com.aolda.itda.exception.ErrorCode; +import com.aolda.itda.repository.certificate.CertificateRepository; +import com.aolda.itda.repository.forwarding.ForwardingRepository; +import com.aolda.itda.repository.log.LogRepository; +import com.aolda.itda.repository.routing.RoutingRepository; +import com.aolda.itda.repository.user.UserRepository; +import com.aolda.itda.service.AuthService; +import jakarta.persistence.EntityManager; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.Map; +import java.util.Objects; + +@Aspect +@Component +@RequiredArgsConstructor +public class ForwardingLogAspect { + + private final ForwardingRepository forwardingRepository; + private final LogRepository logRepository; + private final EntityManager entityManager; + private final UserRepository userRepository; + + /* Create 로깅 */ + @AfterReturning(pointcut = "execution(* com.aolda.itda.service.forwarding.*Service.*create*(..))" + , returning = "result") + public void createLogging(JoinPoint joinPoint, ForwardingDTO result) { + + /* 사용자 조회 */ + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); + User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( + () -> new CustomException(ErrorCode.NOT_FOUND_USER) + ); + + /* 생성된 엔티티 조회 */ + Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(result.getId(), false).orElse(null); + + /* 로그 메세지 작성 */ + String description = "name: " + forwarding.getName() + "\n" + + "serverPort: " + forwarding.getServerPort() + "\n" + + "instanceIp: " + forwarding.getInstanceIp() + "\n" + + "instancePort: " + forwarding.getInstancePort(); + + /* 로그 엔티티 저장 */ + logRepository.save(Log.builder() + .user(user) + .objectType(ObjectType.FORWARDING) + .objectId(forwarding.getForwardingId()) + .action(Action.CREATE) + .projectId(forwarding.getProjectId()) + .description(description) + .build()); + } + + /* Delete 로깅 */ + @AfterReturning(pointcut = "execution(* com.aolda.itda.service.forwarding.*Service.*delete*(..))") + public void deleteLogging(JoinPoint joinPoint) { + + /* 사용자 조회 */ + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); + User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( + () -> new CustomException(ErrorCode.NOT_FOUND_USER) + ); + + /* 삭제된 엔티티 조회 */ + Object[] args = joinPoint.getArgs(); + + Long id = (Long) args[0]; + Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(id, true).orElse(null); + + /* 로그 메세지 작성 */ + String description = "name: " + forwarding.getName() + "\n" + + "serverPort: " + forwarding.getServerPort() + "\n" + + "instanceIp: " + forwarding.getInstanceIp() + "\n" + + "instancePort: " + forwarding.getInstancePort(); + + /* 로그 엔티티 저장 */ + logRepository.save(Log.builder() + .user(user) + .objectType(ObjectType.FORWARDING) + .objectId(forwarding.getForwardingId()) + .action(Action.DELETE) + .projectId(forwarding.getProjectId()) + .description(description) + .build()); + } + + /* Update(edit) 로깅 */ + @Around("execution(* com.aolda.itda.service.forwarding.*Service.*edit*(..))") + public Object editLogging(ProceedingJoinPoint joinPoint) throws Throwable { + + /* 사용자 조회 */ + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); + User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( + () -> new CustomException(ErrorCode.NOT_FOUND_USER) + ); + + /* 변경 전 엔티티 조회 */ + Object[] args = joinPoint.getArgs(); + + Long id = (Long) args[0]; + Forwarding old = forwardingRepository.findByForwardingIdAndIsDeleted(id, false).orElse(null); + if (old != null) { + entityManager.detach(old); + } + + /* 메소드 진행 */ + Object result = joinPoint.proceed(); + + /* 변경 후 엔티티 조회*/ + Forwarding newObj = forwardingRepository.findByForwardingIdAndIsDeleted(id, false).orElse(null); + + /* 로그 메세지 작성 */ + String description = "name: " + old.getName() + (old.getName().equals(newObj.getName()) ? "" : (" -> " + newObj.getName())) + "\n" + + "serverPort: " + old.getServerPort() + (old.getServerPort().equals(newObj.getServerPort()) ? "" : (" -> " + newObj.getServerPort())) + "\n" + + "instanceIp: " + old.getInstanceIp() + (old.getInstanceIp().equals(newObj.getInstanceIp()) ? "" : (" -> " + newObj.getInstanceIp())) + "\n" + + "instancePort: " + old.getInstancePort() + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : (" -> " + newObj.getInstancePort())); + + /* 로그 엔티티 저장 */ + logRepository.save(Log.builder() + .user(user) + .objectType(ObjectType.FORWARDING) + .objectId(newObj.getForwardingId()) + .action(Action.UPDATE) + .projectId(newObj.getProjectId()) + .description(description) + .build()); + return result; + } + + + +} diff --git a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..8b9d5b9fab5e51248cf51ca4fb4c8a93d8b09822 --- /dev/null +++ b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java @@ -0,0 +1,175 @@ +package com.aolda.itda.aspect; + +import com.aolda.itda.dto.forwarding.ForwardingDTO; +import com.aolda.itda.dto.routing.RoutingDTO; +import com.aolda.itda.entity.forwarding.Forwarding; +import com.aolda.itda.entity.log.Action; +import com.aolda.itda.entity.log.Log; +import com.aolda.itda.entity.log.ObjectType; +import com.aolda.itda.entity.routing.Routing; +import com.aolda.itda.entity.user.User; +import com.aolda.itda.exception.CustomException; +import com.aolda.itda.exception.ErrorCode; +import com.aolda.itda.repository.forwarding.ForwardingRepository; +import com.aolda.itda.repository.log.LogRepository; +import com.aolda.itda.repository.routing.RoutingRepository; +import com.aolda.itda.repository.user.UserRepository; +import jakarta.persistence.EntityManager; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import java.util.Map; +import java.util.Objects; + +@Aspect +@Component +@RequiredArgsConstructor +public class RoutingLogAspect { + + private final RoutingRepository routingRepository; + private final UserRepository userRepository; + private final LogRepository logRepository; + private final EntityManager entityManager; + + /* Create 로깅 */ + @AfterReturning(pointcut = "execution(* com.aolda.itda.service.routing.*Service.*create*(..))" + , returning = "result") + public void createLogging(JoinPoint joinPoint, RoutingDTO result) { + + /* 사용자 조회 */ + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); + User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( + () -> new CustomException(ErrorCode.NOT_FOUND_USER) + ); + + /* 생성된 엔티티 조회 */ + Routing routing = routingRepository.findByRoutingIdAndIsDeleted(result.getId(), false).orElse(null); + + /* 로그 메세지 작성 */ + String description = "name: " + routing.getName() + "\n" + + "domain: " + routing.getDomain() + "\n" + + "ip: " + routing.getInstanceIp() + "\n" + + "port: " + routing.getInstancePort() + "\n" + + (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "") + + "caching: " + routing.getCaching(); + + /* 로그 엔티티 저장 */ + logRepository.save(Log.builder() + .user(user) + .objectType(ObjectType.ROUTING) + .objectId(routing.getRoutingId()) + .action(Action.CREATE) + .projectId(routing.getProjectId()) + .description(description) + .build()); + } + + /* Delete 로깅 */ + @AfterReturning(pointcut = "execution(* com.aolda.itda.service.routing.*Service.*delete*(..))") + public void deleteLogging(JoinPoint joinPoint) { + + /* 사용자 조회 */ + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); + User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( + () -> new CustomException(ErrorCode.NOT_FOUND_USER) + ); + + /* 삭제된 엔티티 조회 */ + Object[] args = joinPoint.getArgs(); + + Long id = (Long) args[0]; + Routing routing = routingRepository.findByRoutingIdAndIsDeleted(id, true).orElse(null); + + /* 로그 메세지 작성 */ + String description = "name: " + routing.getName() + "\n" + + "domain: " + routing.getDomain() + "\n" + + "ip: " + routing.getInstanceIp() + "\n" + + "port: " + routing.getInstancePort() + "\n" + + (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "") + + "caching: " + routing.getCaching(); + + /* 로그 엔티티 저장 */ + logRepository.save(Log.builder() + .user(user) + .objectType(ObjectType.ROUTING) + .objectId(routing.getRoutingId()) + .action(Action.DELETE) + .projectId(routing.getProjectId()) + .description(description) + .build()); + } + + /* Update(edit) 로깅 */ + @Around("execution(* com.aolda.itda.service.routing.*Service.*edit*(..))") + public Object editLogging(ProceedingJoinPoint joinPoint) throws Throwable { + + /* 사용자 조회 */ + HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); + User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( + () -> new CustomException(ErrorCode.NOT_FOUND_USER) + ); + + /* 변경 전 엔티티 조회 */ + Object[] args = joinPoint.getArgs(); + + Long id = (Long) args[0]; + Routing old = routingRepository.findByRoutingIdAndIsDeleted(id, false).orElse(null); + if (old != null) { + entityManager.detach(old); + } + + /* 메소드 진행 */ + Object result = joinPoint.proceed(); + + /* 변경 후 엔티티 조회 */ + Routing newObj = routingRepository.findByRoutingIdAndIsDeleted(id, false).orElse(null); + + /* 로그 메세지 작성 */ + String description = "name: " + old.getName() + (old.getName().equals(newObj.getName()) ? "" : " -> " + newObj.getName()) + "\n" + + "domain: " + old.getDomain() + (old.getDomain().equals(newObj.getDomain()) ? "" : " -> " + newObj.getDomain()) + "\n" + + "ip: " + old.getInstanceIp() + (old.getInstanceIp().equals(newObj.getInstanceIp()) ? "" : " -> " + newObj.getInstanceIp()) + "\n" + + "port: " + old.getInstancePort() + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : " -> " + newObj.getInstancePort()) + "\n"; + + if (isSameCertificate(old, newObj)) { + description = description + "certificateId: " + (old.getCertificate() == null ? "null" : old.getCertificate().getCertificateId()) + "\n"; + } else { + description = description + "certificateId: " + (old.getCertificate() == null ? "null" : old.getCertificate().getCertificateId()) + + (newObj.getCertificate() == null ? "null" : " -> " + newObj.getCertificate().getCertificateId()) + "\n"; + } + + description = description + "caching: " + (old.getCaching() == newObj.getCaching() ? newObj.getCaching() : (" -> " + newObj.getCaching())); + + /* 로그 엔티티 저장 */ + logRepository.save(Log.builder() + .user(user) + .objectType(ObjectType.ROUTING) + .objectId(newObj.getRoutingId()) + .action(Action.UPDATE) + .projectId(newObj.getProjectId()) + .description(description) + .build()); + return result; + } + + private boolean isSameCertificate(Routing old, Routing newObj) { + if (old.getCertificate() == null && newObj.getCertificate() == null) { + return true; + } else if (old.getCertificate() == null || newObj.getCertificate() == null) { + return false; + } else { + return old.getCertificate().getCertificateId().equals(newObj.getCertificate().getCertificateId()); + } + } +} diff --git a/src/main/java/com/aolda/itda/config/AuthFilter.java b/src/main/java/com/aolda/itda/config/AuthFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..f810c2c7452b862c85b64cd1a51dd6ff130189b8 --- /dev/null +++ b/src/main/java/com/aolda/itda/config/AuthFilter.java @@ -0,0 +1,76 @@ +package com.aolda.itda.config; + +import com.aolda.itda.dto.auth.IdAndNameDTO; +import com.aolda.itda.exception.CustomException; +import com.aolda.itda.exception.ErrorCode; +import com.aolda.itda.service.AuthService; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +@Component +@Slf4j +public class AuthFilter extends OncePerRequestFilter { + + private final AuthService authService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + if (request.getRequestURI().contains("/api/auth")) { + filterChain.doFilter(request, response); + return; + } + + String token = request.getHeader("X-Subject-Token"); + + // 토큰 헤더 검증 + if (token == null || token.isEmpty()) { + throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI()); + } + + // 유효 토큰 검증 + String userId = authService.validateTokenAndGetUserId(token); + if (userId == null) { + log.error("Token validation failed for URI {}: {}", request.getRequestURI(), request.getRemoteAddr()); + throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI()); + } + + // 프로젝트 권한 검증 + String projectId = request.getParameter("projectId"); + if (projectId != null) { + try { + authService.getBestRoleWithinProject(token, projectId).get("role"); + if (!request.getMethod().equals("GET") && !authService.getBestRoleWithinProject(token, projectId).get("role").equals("admin")) { + throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI()); + } + } catch (Exception e) { + throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI()); + } + } + + // 프로젝트 리스트 조회 + List<String> projects; + if (authService.isAdmin(Map.of("id", userId, "token", token))) { + projects = authService.getAllProjects(token).stream().map(IdAndNameDTO::getId).toList(); + } else { + projects = authService.getProjectsWithUser(Map.of("id", userId, "token", token)).stream().map(IdAndNameDTO::getId).toList(); + } + + request.setAttribute("projects", projects); + request.setAttribute("user", Map.of("id", userId, "token", token)); + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/aolda/itda/config/LoggingFilter.java b/src/main/java/com/aolda/itda/config/LoggingFilter.java new file mode 100644 index 0000000000000000000000000000000000000000..8c5c2f0e090df6f13d35f4c7df103849b9ea9c8e --- /dev/null +++ b/src/main/java/com/aolda/itda/config/LoggingFilter.java @@ -0,0 +1,57 @@ +package com.aolda.itda.config; + +import com.aolda.itda.exception.CustomException; +import com.aolda.itda.exception.ErrorCode; +import com.aolda.itda.service.AuthService; +import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; + +import java.io.IOException; +import java.util.Map; + +@Component +@RequiredArgsConstructor +public class LoggingFilter extends OncePerRequestFilter { + + private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); + private final AuthService authService; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + // Request Body를 읽을 수 있도록 래핑 + ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request); + filterChain.doFilter(cachingRequest, response); + + // 로그 기록 + logRequest(cachingRequest); + } + + private void logRequest(ContentCachingRequestWrapper request) throws JsonProcessingException { + String ip = request.getRemoteAddr(); + String method = request.getMethod(); + String uri = request.getRequestURI(); + String queryString = request.getQueryString(); + String body = getRequestBody(request); + Map<String, String> user = (Map<String, String>) request.getAttribute("user"); + + logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, User: {}, Request Body: {}", + ip, method, uri, (queryString != null ? queryString : "None"), (user != null ? user.get("id") : "None"), + (!body.isEmpty() ? body : "None")); + } + + private String getRequestBody(ContentCachingRequestWrapper request) { + byte[] buf = request.getContentAsByteArray(); + return (buf.length > 0) ? new String(buf) : ""; + } +} diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..a3559c4628d3c5854fc8386cbb1d173943f9ea42 --- /dev/null +++ b/src/main/java/com/aolda/itda/config/WebConfig.java @@ -0,0 +1,53 @@ +package com.aolda.itda.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import lombok.RequiredArgsConstructor; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +@RequiredArgsConstructor +public class WebConfig implements WebMvcConfigurer { + + private final AuthFilter authFilter; + private final LoggingFilter loggingFilter; + + @Override + public void addCorsMappings(CorsRegistry registry) { // 스프링단에서 cors 설정 + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH", "FETCH") + .allowedHeaders("*") + .allowCredentials(true) + .exposedHeaders("Authorization", "X-Refresh-Token", "Access-Control-Allow-Origin") + ; + } + + + @Bean + public FilterRegistrationBean<AuthFilter> authFilterRegistration() { + FilterRegistrationBean<AuthFilter> registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(authFilter); + registrationBean.setOrder(1); // AuthFilter의 순서를 1로 설정 + registrationBean.addUrlPatterns("/*"); + return registrationBean; + } + + @Bean + public FilterRegistrationBean<LoggingFilter> loggingFilterRegistration() { + FilterRegistrationBean<LoggingFilter> registrationBean = new FilterRegistrationBean<>(); + registrationBean.setFilter(loggingFilter); + registrationBean.setOrder(2); // LoggingFilter의 순서를 2로 설정 + registrationBean.addUrlPatterns("/*"); + return registrationBean; + } + + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager em) { + return new JPAQueryFactory(em); + } +} diff --git a/src/main/java/com/aolda/itda/controller/AuthController.java b/src/main/java/com/aolda/itda/controller/AuthController.java new file mode 100644 index 0000000000000000000000000000000000000000..79046d2d2459cde40b302cc0e190d3240280257e --- /dev/null +++ b/src/main/java/com/aolda/itda/controller/AuthController.java @@ -0,0 +1,31 @@ +package com.aolda.itda.controller; + +import com.aolda.itda.dto.auth.LoginRequestDTO; +import com.aolda.itda.service.AuthService; +import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/auth") +@RequiredArgsConstructor +public class AuthController { + + private final AuthService authService; + + @PostMapping("/login") + public ResponseEntity<Object> login(HttpServletResponse response, + @RequestBody LoginRequestDTO loginRequestDTO) throws JsonProcessingException { + + return ResponseEntity.ok(authService.userLogin(response, loginRequestDTO)); + } + + @GetMapping("/role") + public ResponseEntity<Object> roleWithinProject(@RequestHeader("X-Subject-Token") String token, + @RequestParam String projectId) throws JsonProcessingException { + + return ResponseEntity.ok(authService.getBestRoleWithinProject(token, projectId)); + } +} diff --git a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java new file mode 100644 index 0000000000000000000000000000000000000000..26a0322cc29fb6c642ccafbf9dd5cce005030a9e --- /dev/null +++ b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java @@ -0,0 +1,112 @@ +package com.aolda.itda.controller.certificate; + +import com.aolda.itda.dto.PageResp; +import com.aolda.itda.dto.certificate.CertificateDTO; +import com.aolda.itda.service.certificate.CertificateService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class CertificateController { + + private final CertificateService certificateService; + + /** + * 인증서 생성 + * POST /api/certificate?projectId=xxx + */ + @PostMapping("/certificate") + public ResponseEntity<Void> create( + @RequestParam String projectId, + @RequestBody CertificateDTO dto, + HttpServletRequest request + ) { + certificateService.createCertificate( + projectId, + dto, + (List<String>) request.getAttribute("projects") + ); + return ResponseEntity.ok().build(); + } + + /** + * 인증서 단건 조회 + * GET /api/certificate?certificateId=xxx + */ + @GetMapping("/certificate") + public ResponseEntity<CertificateDTO> view( + @RequestParam Long certificateId, + HttpServletRequest request + ) { + return ResponseEntity.ok( + certificateService.getCertificate( + certificateId, + (List<String>) request.getAttribute("projects") + ) + ); + } + + /** + * 인증서 목록 조회 (domain 필터링 optional) + * GET /api/certificates?projectId=xxx&domain=foo + */ + @GetMapping("/certificates") + public ResponseEntity<PageResp<CertificateDTO>> lists( + @RequestParam String projectId, + @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) + ); + } + + /** + * 인증서 수정 + * PATCH /api/certificate?certificateId=xxx + */ + @PatchMapping("/certificate") + public ResponseEntity<Void> edit( + @RequestParam Long certificateId, + @RequestBody CertificateDTO dto, + HttpServletRequest request + ) { + certificateService.editCertificate( + certificateId, + dto, + (List<String>) request.getAttribute("projects") + ); + return ResponseEntity.ok().build(); + } + + /** + * 인증서 삭제 + * DELETE /api/certificate?certificateId=xxx + */ + @DeleteMapping("/certificate") + public ResponseEntity<Void> delete( + @RequestParam Long certificateId, + HttpServletRequest request + ) { + certificateService.deleteCertificate( + certificateId, + (List<String>) request.getAttribute("projects") + ); + return ResponseEntity.ok().build(); + } +} diff --git a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java new file mode 100644 index 0000000000000000000000000000000000000000..999707aed33a9f559d2c396c6532df50aada5173 --- /dev/null +++ b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java @@ -0,0 +1,53 @@ +package com.aolda.itda.controller.forwarding; + +import com.aolda.itda.dto.forwarding.ForwardingDTO; +import com.aolda.itda.service.forwarding.ForwardingService; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class ForwardingController { + + private final ForwardingService forwardingService; + + @PostMapping("/forwarding") + public ResponseEntity<Object> create(@RequestParam String projectId, + @RequestBody ForwardingDTO dto) { + forwardingService.createForwarding(projectId, dto); + return ResponseEntity.ok().build(); + } + + @GetMapping("/forwarding") + public ResponseEntity<Object> view(@RequestParam Long forwardingId, HttpServletRequest request) { + return ResponseEntity.ok(forwardingService.getForwarding(forwardingId, (List<String>) request.getAttribute("projects"))); + } + + @GetMapping("/forwardings") + public ResponseEntity<Object> lists(@RequestParam String projectId, + @RequestParam(required = false) String query) { + return ResponseEntity.ok(forwardingService.getForwardingsWithSearch(projectId, query)); + } + + @PatchMapping("/forwarding") + public ResponseEntity<Object> edit(@RequestParam Long forwardingId, + @RequestBody ForwardingDTO dto, + HttpServletRequest request) { + forwardingService.editForwarding(forwardingId, dto, (List<String>) request.getAttribute("projects") ); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/forwarding") + public ResponseEntity<Object> delete(@RequestParam Long forwardingId, + HttpServletRequest request) { + forwardingService.deleteForwarding(forwardingId, (List<String>) request.getAttribute("projects")); + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/com/aolda/itda/controller/log/LogController.java b/src/main/java/com/aolda/itda/controller/log/LogController.java new file mode 100644 index 0000000000000000000000000000000000000000..0d4bfcf7bedf91d5fc7d1fbca6f4d68e6a5e48e8 --- /dev/null +++ b/src/main/java/com/aolda/itda/controller/log/LogController.java @@ -0,0 +1,41 @@ +package com.aolda.itda.controller.log; + +import com.aolda.itda.service.log.LogService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.http.ResponseEntity; +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.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class LogController { + + private final LogService logService; + + @GetMapping("/log") + public ResponseEntity<Object> view(@RequestParam Long logId, HttpServletRequest request) { + return ResponseEntity.ok(logService.getLog(logId, (List<String>) request.getAttribute("projects"))); + } + + @GetMapping("/logs") + public ResponseEntity<Object> lists(@RequestParam(required = false) String projectId + ,@RequestParam(required = false) String type + ,@RequestParam(required = false) String username + ,@RequestParam(required = false) String action + ,@RequestParam(defaultValue = "false") boolean isASC + ,@PageableDefault(size = 10) Pageable pageable + ,HttpServletRequest request) { + return ResponseEntity.ok(logService.getLogs(projectId, type, username, action, isASC, pageable, + (Map<String, String>) request.getAttribute("user"))); + } + +} diff --git a/src/main/java/com/aolda/itda/controller/main/MainController.java b/src/main/java/com/aolda/itda/controller/main/MainController.java new file mode 100644 index 0000000000000000000000000000000000000000..371cce5c41e82deb7dbe8bf10ac0c618d52e5a74 --- /dev/null +++ b/src/main/java/com/aolda/itda/controller/main/MainController.java @@ -0,0 +1,33 @@ +package com.aolda.itda.controller.main; + +import com.aolda.itda.service.main.MainService; +import com.fasterxml.jackson.core.JsonProcessingException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +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.util.List; +import java.util.Map; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class MainController { + + private final MainService mainService; + + @GetMapping("/projects") + public ResponseEntity<Object> projects(HttpServletRequest request) throws JsonProcessingException { + return ResponseEntity.ok(mainService.getAllProjects((Map<String, String>) request.getSession().getAttribute("user"))); + } + + @GetMapping("/main") + public ResponseEntity<Object> mainInfo(@RequestParam String projectId, HttpServletRequest request) { + return ResponseEntity.ok(mainService.getMainInfo(projectId, (List<String>) request.getAttribute("projects"))); + } + +} diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java new file mode 100644 index 0000000000000000000000000000000000000000..7ca1aa859c30e146f0552f2dc45cbefdc59ce677 --- /dev/null +++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java @@ -0,0 +1,55 @@ +package com.aolda.itda.controller.routing; + +import com.aolda.itda.dto.forwarding.ForwardingDTO; +import com.aolda.itda.dto.routing.RoutingDTO; +import com.aolda.itda.service.routing.RoutingService; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; +import java.util.List; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class RoutingController { + + private final RoutingService routingService; + + @PostMapping("/routing") + public ResponseEntity<Object> create(@RequestParam String projectId, + @RequestBody RoutingDTO dto) throws IOException { + routingService.createRouting(projectId, dto); + return ResponseEntity.ok().build(); + } + + @GetMapping("/routing") + public ResponseEntity<Object> view(@RequestParam Long routingId, + HttpServletRequest request) { + return ResponseEntity.ok(routingService.getRouting(routingId, (List<String>) request.getAttribute("projects"))); + } + + @GetMapping("/routings") + public ResponseEntity<Object> lists(@RequestParam String projectId, + @RequestParam(required = false) String query) { + return ResponseEntity.ok(routingService.getRoutingsWithSearch(projectId, query)); + } + + @PatchMapping("/routing") + public ResponseEntity<Object> edit(@RequestParam Long routingId, + @RequestBody RoutingDTO dto, + HttpServletRequest request) throws IOException { + routingService.editRouting(routingId, dto, (List<String>) request.getAttribute("projects")); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/routing") + public ResponseEntity<Object> delete(@RequestParam Long routingId, + HttpServletRequest request) { + routingService.deleteRouting(routingId, (List<String>) request.getAttribute("projects")); + return ResponseEntity.ok().build(); + } + +} diff --git a/src/main/java/com/aolda/itda/dto/PageResp.java b/src/main/java/com/aolda/itda/dto/PageResp.java new file mode 100644 index 0000000000000000000000000000000000000000..7079600bbabe8b47e3e4c57f5267e8b287c8a473 --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/PageResp.java @@ -0,0 +1,27 @@ +package com.aolda.itda.dto; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +@JsonInclude(JsonInclude.Include.NON_NULL) +public class PageResp<T> { + private Integer totalPages; + private Long totalElements; + private Integer size; + private List<T> contents; + private Boolean first; + private Boolean last; + + public static <T> PageRespBuilder<T> builderFor(Class<T> clazz) { + return (PageRespBuilder<T>) new PageRespBuilder<>(); + } +} diff --git a/src/main/java/com/aolda/itda/dto/auth/IdAndNameDTO.java b/src/main/java/com/aolda/itda/dto/auth/IdAndNameDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..5918105381265b66446aa2d221657508e177fb58 --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/auth/IdAndNameDTO.java @@ -0,0 +1,24 @@ +package com.aolda.itda.dto.auth; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class IdAndNameDTO { + + private String id; + private String name; + + @QueryProjection + public IdAndNameDTO(String id, String name) { + this.id = id; + this.name = name; + } +} diff --git a/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java b/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..b91b3af32112b005db11a228e682e42889db7505 --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java @@ -0,0 +1,11 @@ +package com.aolda.itda.dto.auth; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class LoginRequestDTO { + private String id; + private String password; +} diff --git a/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..a647b05835b9614cf21b1b8340a31b256fade9bc --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java @@ -0,0 +1,17 @@ +package com.aolda.itda.dto.auth; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class LoginResponseDTO { + private Boolean isAdmin; + private List<IdAndNameDTO> projects; +} diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..058fdd789d0f2c53bbda5ac10469bd9e72d6313b --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java @@ -0,0 +1,35 @@ +package com.aolda.itda.dto.certificate; + +import com.aolda.itda.entity.certificate.Challenge; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.persistence.Column; +import lombok.*; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CertificateDTO { + + private Long id; // 인증서 고유 ID + 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")// 도메인 소유자의 이메일 + private LocalDateTime expiresAt; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")// 인증서 만료일 + private LocalDateTime createdAt; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")// 인증서 생성일 + private LocalDateTime updatedAt; // 인증서 업데이트일 + private Challenge challenge; // 챌린지 방식 (HTTP, DNS_CLOUDFLARE) + private Boolean isDeleted; // 삭제 여부 (soft delete) + private String apiToken; +} +/* 이메일, 챌린지 방식, http인지 dns인지... "*/ +//도메인, 소유자 이메일, 챌린지 방식 확실하게 들어가야함!! +/*erd 보고 만들기*/ +//Challenge 키는 따로 (private으로 api 키 받기) \ No newline at end of file diff --git a/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..be003b6ca78d1f4e317c391559fd41e2c4a304ad --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java @@ -0,0 +1,50 @@ +package com.aolda.itda.dto.forwarding; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Pattern; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class ForwardingDTO { + + private Long id; + + @Pattern(regexp = "^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$", + message = "잘못된 IP 형식 (server)") + private String serverIp; + + @NotBlank(message = "serverPort 값이 존재하지 않습니다") + @Pattern(regexp = "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$", + message = "잘못된 포트 형식 (server)") + private String serverPort; + + @NotBlank(message = "instanceIp 값이 존재하지 않습니다") + @Pattern(regexp = "^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$", + message = "잘못된 IP 형식 (instance)") + private String instanceIp; + + @NotBlank(message = "instancePort 값이 존재하지 않습니다") + @Pattern(regexp = "^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$", + message = "잘못된 포트 형식 (instance)") + private String instancePort; + + @NotBlank(message = "name 값이 존재하지 않습니다") + private String name; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + private LocalDateTime createdAt; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/aolda/itda/dto/log/LogDTO.java b/src/main/java/com/aolda/itda/dto/log/LogDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..9946028fa254c24ea5197f8002c610ad02a083b5 --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/log/LogDTO.java @@ -0,0 +1,43 @@ +package com.aolda.itda.dto.log; + +import com.aolda.itda.dto.auth.IdAndNameDTO; +import com.aolda.itda.entity.log.Action; +import com.aolda.itda.entity.log.ObjectType; +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class LogDTO { + private Long id; + private IdAndNameDTO user; + private Action action; + private ObjectType type; + private Long objectId; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + private String description; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + private LocalDateTime createdAt; + + @QueryProjection + public LogDTO(Long id, IdAndNameDTO user, Action action, ObjectType type, Long objectId, String description, LocalDateTime createdAt) { + this.id = id; + this.user = user; + this.action = action; + this.type = type; + this.objectId = objectId; + this.description = description; + this.createdAt = createdAt; + } +} diff --git a/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..cbcdcd583831431a778791d15c8551ac834d5fe5 --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java @@ -0,0 +1,20 @@ +package com.aolda.itda.dto.main; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class MainInfoDTO { + + private Long routing; + private Long forwarding; + private Long certificate; + +} diff --git a/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java new file mode 100644 index 0000000000000000000000000000000000000000..985056ea3771b0a0fc3b7afc2db890c039f4db84 --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java @@ -0,0 +1,40 @@ +package com.aolda.itda.dto.routing; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonInclude; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class RoutingDTO { + @NotBlank + private String ip; + @NotBlank + private String domain; + @NotBlank + private String name; + @NotBlank + private String port; + private Long id; + @NotNull + private Long certificateId; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + private LocalDateTime createdAt; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + private LocalDateTime updatedAt; + + @NotNull + private Boolean caching; +} diff --git a/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java new file mode 100644 index 0000000000000000000000000000000000000000..b62af84f1afea4909e4af4e7cb72848ddd0feee1 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java @@ -0,0 +1,29 @@ +package com.aolda.itda.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.MappedSuperclass; +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public abstract class BaseTimeEntity { + + @CreatedDate + @Column(updatable = false, columnDefinition = "DATETIME") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at", columnDefinition = "DATETIME") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") + private LocalDateTime updatedAt; + +} diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java new file mode 100644 index 0000000000000000000000000000000000000000..9161b45397475cd8555591d962df43f3c8684d78 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -0,0 +1,58 @@ +package com.aolda.itda.entity.certificate; + +import com.aolda.itda.entity.BaseTimeEntity; +import com.aolda.itda.entity.user.User; +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.*; +import lombok.*; + +import java.time.LocalDateTime; + +@Entity +@Table(name = "certificate") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Certificate extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long certificateId; + + @Column(length = 64) + private String projectId; + + @Setter + @Column(length = 64) + private String domain; + + @Column(length = 64) + @Setter + private String email; + + @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) + private Challenge challenge; + + private Boolean isDeleted; + + + public String formatDomain() { + return domain == null ? null : domain.replace("*", "_"); + } + + public void setIsDeleted(boolean b) { + this.isDeleted = b; + } + + @Transient + private String apiToken; + +} + diff --git a/src/main/java/com/aolda/itda/entity/certificate/Challenge.java b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java new file mode 100644 index 0000000000000000000000000000000000000000..41f607e0289f9533cf6bc18c106e9d6037f1abed --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java @@ -0,0 +1,15 @@ +package com.aolda.itda.entity.certificate; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonValue; + +@JsonFormat(shape = JsonFormat.Shape.STRING) +public enum Challenge { + HTTP, DNS_CLOUDFLARE; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java new file mode 100644 index 0000000000000000000000000000000000000000..0ba96d651f09690952337da5a311e12d8f1f51e6 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java @@ -0,0 +1,82 @@ +package com.aolda.itda.entity.forwarding; + +import com.aolda.itda.dto.forwarding.ForwardingDTO; +import com.aolda.itda.entity.BaseTimeEntity; +import com.aolda.itda.entity.user.User; +import jakarta.persistence.*; +import lombok.*; + +@Entity +@Table(name = "forwarding") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +@ToString +public class Forwarding extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long forwardingId; + + @Column(length = 64) + private String projectId; + + @Column(length = 32) + private String serverIp; + + @Column(length = 8) + private String serverPort; + + @Column(length = 32) + private String instanceIp; + + @Column(length = 8) + private String instancePort; + + private Boolean isDeleted; + + @Column(length = 256) + private String name; + + public Forwarding(Forwarding forwarding) { + this.forwardingId = forwarding.getForwardingId(); + this.projectId = forwarding.getProjectId(); + this.serverIp = forwarding.getServerIp(); + this.serverPort = forwarding.getServerPort(); + this.instanceIp = forwarding.getInstanceIp(); + this.instancePort = forwarding.getInstancePort(); + this.isDeleted = forwarding.getIsDeleted(); + this.name = forwarding.getName(); + } + + public ForwardingDTO toForwardingDTO() { + return ForwardingDTO.builder() + .id(forwardingId) + .name(name) + .serverPort(serverPort) + .instanceIp(instanceIp) + .instancePort(instancePort) + .createdAt(getCreatedAt()) + .updatedAt(getUpdatedAt()) + .build(); + } + + public void edit(ForwardingDTO dto) { + this.name = dto.getName() != null ? dto.getName() : this.name; + this.serverPort = dto.getServerPort() != null && + dto.getServerPort().matches("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$") + ? dto.getServerPort() : this.serverPort; + this.instanceIp = dto.getInstanceIp() != null && + dto.getInstanceIp().matches("^(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\\.){3}(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])$") + ? dto.getInstanceIp() : this.instanceIp; + this.instancePort = dto.getInstancePort() != null && + dto.getInstancePort().matches("^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$") + ? dto.getInstancePort() : this.instancePort; + } + + public void delete() { + this.isDeleted = true; + } +} diff --git a/src/main/java/com/aolda/itda/entity/log/Action.java b/src/main/java/com/aolda/itda/entity/log/Action.java new file mode 100644 index 0000000000000000000000000000000000000000..448a65e6e29e793da43011c1f40fd64b01659dae --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/log/Action.java @@ -0,0 +1,15 @@ +package com.aolda.itda.entity.log; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonValue; + +@JsonFormat(shape = JsonFormat.Shape.STRING) +public enum Action { + CREATE, UPDATE, DELETE; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/com/aolda/itda/entity/log/Log.java b/src/main/java/com/aolda/itda/entity/log/Log.java new file mode 100644 index 0000000000000000000000000000000000000000..6bf607497bb95f9ba71ca677dbcb9af5d7b9a36d --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/log/Log.java @@ -0,0 +1,58 @@ +package com.aolda.itda.entity.log; + +import com.aolda.itda.dto.auth.IdAndNameDTO; +import com.aolda.itda.dto.log.LogDTO; +import com.aolda.itda.entity.BaseTimeEntity; +import com.aolda.itda.entity.user.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "log") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Log extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long logId; + + @ManyToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @Column(length = 64) + private String projectId; + + @Enumerated(EnumType.STRING) + private ObjectType objectType; + + @Column(length = 64) + private Long objectId; + + @Enumerated(EnumType.STRING) + private Action action; + + @Lob + @Column(length = 1024) + private String description; + + public LogDTO toLogDTO() { + return LogDTO.builder() + .id(logId) + .user(IdAndNameDTO.builder().id(user.getKeystoneId()).name(user.getKeystoneUsername()).build()) + .action(action) + .type(objectType) + .objectId(objectId) + .description(description) + .createdAt(getCreatedAt()) + .build(); + } + +} diff --git a/src/main/java/com/aolda/itda/entity/log/ObjectType.java b/src/main/java/com/aolda/itda/entity/log/ObjectType.java new file mode 100644 index 0000000000000000000000000000000000000000..c91f27911dfc4c661be7ab343b03c2391ddb1a56 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/log/ObjectType.java @@ -0,0 +1,15 @@ +package com.aolda.itda.entity.log; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonValue; + +@JsonFormat(shape = JsonFormat.Shape.STRING) +public enum ObjectType { + ROUTING, CERTIFICATE, FORWARDING; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } +} diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java new file mode 100644 index 0000000000000000000000000000000000000000..9412368edff4100d0d3db65d5a25faf97760cd97 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java @@ -0,0 +1,75 @@ +package com.aolda.itda.entity.routing; + +import com.aolda.itda.dto.forwarding.ForwardingDTO; +import com.aolda.itda.dto.routing.RoutingDTO; +import com.aolda.itda.entity.BaseTimeEntity; +import com.aolda.itda.entity.certificate.Certificate; +import com.aolda.itda.entity.user.User; +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "routing") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Routing extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long routingId; + + @ManyToOne + @JoinColumn(name = "certificate_id") + private Certificate certificate; + + @Column(length = 64) + private String projectId; + + @Column(length = 64) + private String domain; + + @Column(length = 32) + private String instanceIp; + + @Column(length = 8) + private String instancePort; + + private Boolean isDeleted; + + private Boolean caching; + + @Column(length = 256) + private String name; + + public RoutingDTO toRoutingDTO() { + return RoutingDTO.builder() + .id(routingId) + .name(name) + .port(instancePort) + .ip(instanceIp) + .certificateId(certificate == null ? null : certificate.getCertificateId()) + .caching(caching) + .domain(domain) + .createdAt(getCreatedAt()) + .updatedAt(getUpdatedAt()) + .build(); + } + + public void edit(RoutingDTO dto, Certificate certificate) { + this.name = dto.getName() != null ? dto.getName() : this.name; + this.instanceIp = dto.getIp() != null ? dto.getIp() : this.instanceIp; + this.instancePort = dto.getPort() != null ? dto.getPort() : this.instancePort; + this.caching = dto.getCaching() != null ? dto.getCaching() : this.caching; + this.domain = dto.getDomain() != null ? dto.getDomain() : this.domain; + this.certificate = certificate; + } + public void delete() { + this.isDeleted = true; + } +} diff --git a/src/main/java/com/aolda/itda/entity/user/User.java b/src/main/java/com/aolda/itda/entity/user/User.java new file mode 100644 index 0000000000000000000000000000000000000000..682a50e9004d3849bc795b1cd98dc0b392eefb02 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/user/User.java @@ -0,0 +1,28 @@ +package com.aolda.itda.entity.user; + +import jakarta.persistence.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "user") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long userId; + + private String keystoneUsername; + private String keystoneId; + + public void changeUsername(String username) { + this.keystoneUsername = username; + } +} diff --git a/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java new file mode 100644 index 0000000000000000000000000000000000000000..20ef9ec85a3b546782271043c8bebc1854dae3b2 --- /dev/null +++ b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java @@ -0,0 +1,28 @@ +package com.aolda.itda.exception; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class ApiExceptionHandler { + + @ExceptionHandler(value = CustomException.class) + public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) { + log.error("[handleCustomException] {} : {}, {}", e.getErrorCode().name(), e.getErrorCode().getMessage(), e.getStackTrace()); + return ErrorResponse.fromException(e); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity<ErrorResponse> handleException(Exception e) { + log.error("Unexpected error occurred: {}", e.getMessage(), e); + ErrorResponse response = new ErrorResponse( + ErrorCode.INTERNAL_SERVER_ERROR.getStatus(), + ErrorCode.INTERNAL_SERVER_ERROR.getCode(), + ErrorCode.INTERNAL_SERVER_ERROR.getMessage() + ); + return new ResponseEntity<>(response, ErrorCode.INTERNAL_SERVER_ERROR.getStatus()); + } +} diff --git a/src/main/java/com/aolda/itda/exception/CustomException.java b/src/main/java/com/aolda/itda/exception/CustomException.java new file mode 100644 index 0000000000000000000000000000000000000000..d110063b5baf441b8dfba80e64ef0eb21816cb09 --- /dev/null +++ b/src/main/java/com/aolda/itda/exception/CustomException.java @@ -0,0 +1,19 @@ +package com.aolda.itda.exception; + +import lombok.Getter; + +@Getter +public class CustomException extends RuntimeException{ + private ErrorCode errorCode; + + private String info; + + public CustomException(ErrorCode errorCode) { + this.errorCode = errorCode; + } + + public CustomException(ErrorCode errorCode, String info){ + this.errorCode = errorCode; + this.info = info; + } +} diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java new file mode 100644 index 0000000000000000000000000000000000000000..5adaa1ff1fab812ee5ba185453902715aaa9c522 --- /dev/null +++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java @@ -0,0 +1,57 @@ +package com.aolda.itda.exception; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum ErrorCode { + + // User + INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다"), + NOT_FOUND_USER(HttpStatus.BAD_REQUEST, "존재하지 않는 사용자입니다"), + UNAUTHORIZED_USER(HttpStatus.BAD_REQUEST, "권한이 없는 사용자입니다"), + + // Token + INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"), + + // Forwarding + NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"), + + // Routing + NOT_FOUND_ROUTING(HttpStatus.BAD_REQUEST, "라우팅 파일이 존재하지 않습니다"), + + // Routing + NOT_FOUND_LOG(HttpStatus.BAD_REQUEST, "로그가 존재하지 않습니다"), + + // Certificate + NOT_FOUND_CERTIFICATE(HttpStatus.BAD_REQUEST, "SSL 인증서가 존재하지 않습니다"), + + // CONF File + FAIL_CREATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 생성하지 못했습니다"), + FAIL_UPDATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 수정하지 못했습니다"), + INVALID_CONF_INPUT(HttpStatus.BAD_REQUEST, "잘못된 입력이 존재합니다"), + DUPLICATED_INSTANCE_INFO(HttpStatus.BAD_REQUEST, "중복된 인스턴스 IP와 포트입니다"), + DUPLICATED_SERVER_PORT(HttpStatus.BAD_REQUEST, "중복된 서버 포트입니다"), + DUPLICATED_DOMAIN_NAME(HttpStatus.BAD_REQUEST, "중복된 도메인 주소입니다"), + + // Nginx + FAIL_NGINX_CONF_TEST(HttpStatus.BAD_REQUEST, "Conf 파일 테스트에 실패했습니다"), + FAIL_NGINX_CONF_RELOAD(HttpStatus.BAD_REQUEST, "Nginx 재시작에 실패했습니다"), + + FAIL_DELETE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 삭제하지 못했습니다"), + FAIL_ROLL_BACK(HttpStatus.BAD_REQUEST, "롤백 실패"), + + FAIL_CREATE_CERT(HttpStatus.BAD_REQUEST, "인증서 생성에 실패했습니다"), + + // System + INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버 내부 오류가 발생했습니다"); + + private final HttpStatus status; + private final String message; + + public String getCode() { + return this.name(); + } +} diff --git a/src/main/java/com/aolda/itda/exception/ErrorResponse.java b/src/main/java/com/aolda/itda/exception/ErrorResponse.java new file mode 100644 index 0000000000000000000000000000000000000000..aa020023e72b23a971940f74b147a49950e0d74e --- /dev/null +++ b/src/main/java/com/aolda/itda/exception/ErrorResponse.java @@ -0,0 +1,31 @@ +package com.aolda.itda.exception; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@Getter +@Builder +@AllArgsConstructor +public class ErrorResponse { + + private final HttpStatus status; // HTTP 상태 코드 + private final String code; // 에러 코드 + private final String message; // 에러 메시지 + + public static ResponseEntity<ErrorResponse> fromException(CustomException e) { + String message = e.getErrorCode().getMessage(); + if (e.getInfo() != null) { + message += " " + e.getInfo(); // 추가 정보가 있는 경우 결합 + } + return ResponseEntity + .status(e.getErrorCode().getStatus()) + .body(ErrorResponse.builder() + .status(e.getErrorCode().getStatus()) + .code(e.getErrorCode().name()) + .message(message) + .build()); + } +} diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..0f3c137a9795352e199bf81f2df8e081812a82cc --- /dev/null +++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java @@ -0,0 +1,33 @@ +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; +import java.util.Optional; + +public interface CertificateRepository extends JpaRepository<Certificate, Long> { + + // 단건 조회 (Soft Delete 고려) + Optional<Certificate> findByCertificateIdAndIsDeleted(Long certificateId, Boolean isDeleted); + + // 프로젝트별 목록 조회 (Soft Delete 고려) + List<Certificate> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted); + + // 만료일이 주어진 날짜 이전인(=만료 30일 이내) 인증서 조회 + //List<Certificate> findByExpiresAtBeforeAndIsDeleted(LocalDateTime date, Boolean isDeleted); + + // 1) domain 필터링용 메서드 + 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/repository/forwarding/ForwardingRepository.java b/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..9a330f0dbc0fe111dd99aed57e29a960ff9f5031 --- /dev/null +++ b/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java @@ -0,0 +1,18 @@ +package com.aolda.itda.repository.forwarding; + +import com.aolda.itda.entity.forwarding.Forwarding; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface ForwardingRepository extends JpaRepository<Forwarding, Long> { + List<Forwarding> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted); + Optional<Forwarding> findByForwardingIdAndIsDeleted(Long forwardingId, Boolean isDeleted); + Boolean existsByInstanceIpAndInstancePortAndIsDeleted(String instanceIp, String instancePort, Boolean isDeleted); + Boolean existsByServerPortAndIsDeleted(String serverPort, Boolean isDeleted); + + @Query("SELECT f FROM Forwarding f WHERE f.projectId = ?1 AND f.isDeleted = ?3 AND (f.instanceIp LIKE %?2% OR f.serverPort LIKE %?2% OR f.name LIKE %?2%)") + List<Forwarding> findWithSearch(String projectId, String query, Boolean isDeleted); +} diff --git a/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java new file mode 100644 index 0000000000000000000000000000000000000000..19feb191a3f4f9dfaab6bb8b8c2925756a69c244 --- /dev/null +++ b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java @@ -0,0 +1,102 @@ +package com.aolda.itda.repository.log; + +import com.aolda.itda.dto.PageResp; +import com.aolda.itda.dto.auth.QIdAndNameDTO; +import com.aolda.itda.dto.log.LogDTO; +import com.aolda.itda.dto.log.QLogDTO; +import com.aolda.itda.entity.log.Action; +import com.aolda.itda.entity.log.ObjectType; +import com.querydsl.core.BooleanBuilder; +import com.querydsl.jpa.impl.JPAQuery; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.support.PageableExecutionUtils; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.aolda.itda.entity.log.QLog.*; +import static com.aolda.itda.entity.user.QUser.*; + +@Repository +@RequiredArgsConstructor +public class LogQueryDSL { + + private final JPAQueryFactory jpaQueryFactory; + + /* log 목록 반환 */ + public PageResp<LogDTO> getLogs(String projectId, String type, + String username, String action, Boolean isASC, Pageable pageable) { + + List<LogDTO> content = jpaQueryFactory + .select(new QLogDTO( + log.logId, + new QIdAndNameDTO(user.keystoneId, user.keystoneUsername), + log.action, + log.objectType, + log.objectId, + log.description, + log.createdAt + )).from(log) + .join(log.user, user).on(log.user.eq(user)) + .where(getFilter(projectId, type, username, action)) + .orderBy(isASC ? log.logId.asc() : log.logId.desc()) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .fetch(); + + JPAQuery<Long> cnt = jpaQueryFactory + .select(log.count()) + .from(log) + .where(getFilter(projectId, type, username, action)); + + Page<LogDTO> page = PageableExecutionUtils.getPage(content, pageable, cnt::fetchOne); + + return PageResp.<LogDTO>builder() + .contents(page.getContent()) + .first(page.isFirst()) + .last(page.isLast()) + .size(page.getSize()) + .totalPages(page.getTotalPages()) + .totalElements(page.getTotalElements()) + .build(); + } + + /* Where 필터 */ + private BooleanBuilder getFilter(String projectId, String type, + String username, String action) { + BooleanBuilder builder = new BooleanBuilder(); + + /* 프로젝트 조건 */ + if (projectId != null) { + builder.and(log.projectId.eq(projectId)); + } + + /* 오브젝트 타입 조건 */ + if (type != null) { + switch (type) { + case "certificate" -> builder.and(log.objectType.eq(ObjectType.CERTIFICATE)); + case "forwarding" -> builder.and(log.objectType.eq(ObjectType.FORWARDING)); + case "routing" -> builder.and(log.objectType.eq(ObjectType.ROUTING)); + } + } + + + /* 사용자 ID 조건 */ + if (username != null) { + builder.and(log.user.keystoneUsername.contains(username)); + } + + /* CUD 조건 */ + if (action != null) { + switch (action) { + case "create" -> builder.and(log.action.eq(Action.CREATE)); + case "update" -> builder.and(log.action.eq(Action.UPDATE)); + case "delete" -> builder.and(log.action.eq(Action.DELETE)); + } + } + return builder; + } +} diff --git a/src/main/java/com/aolda/itda/repository/log/LogRepository.java b/src/main/java/com/aolda/itda/repository/log/LogRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..de0a769f6b1b206f5d350aae620fa997f7763006 --- /dev/null +++ b/src/main/java/com/aolda/itda/repository/log/LogRepository.java @@ -0,0 +1,10 @@ +package com.aolda.itda.repository.log; + +import com.aolda.itda.entity.log.Log; +import com.aolda.itda.entity.routing.Routing; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface LogRepository extends JpaRepository<Log, Long> { +} diff --git a/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java b/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..44b88aecdc26ff76583c01149a9862503a0c8d79 --- /dev/null +++ b/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java @@ -0,0 +1,18 @@ +package com.aolda.itda.repository.routing; + +import com.aolda.itda.entity.forwarding.Forwarding; +import com.aolda.itda.entity.routing.Routing; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; +import java.util.Optional; + +public interface RoutingRepository extends JpaRepository<Routing, Long> { + List<Routing> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted); + Optional<Routing> findByRoutingIdAndIsDeleted(Long routingId, Boolean isDeleted); + Boolean existsByDomainAndIsDeleted(String domain, 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<Routing> findWithSearch(String projectId, String query, Boolean isDeleted); +} diff --git a/src/main/java/com/aolda/itda/repository/user/UserRepository.java b/src/main/java/com/aolda/itda/repository/user/UserRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..44cae688e4032b6caec7482c6c6f7e3c038736e2 --- /dev/null +++ b/src/main/java/com/aolda/itda/repository/user/UserRepository.java @@ -0,0 +1,11 @@ +package com.aolda.itda.repository.user; + +import com.aolda.itda.entity.user.User; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface UserRepository extends JpaRepository<User, Long> { + Optional<User> findByKeystoneId(String keystoneId); + Optional<User> findByKeystoneUsername(String keystoneUsername); +} diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java new file mode 100644 index 0000000000000000000000000000000000000000..4c1740ec38b088511b9e4cf68afc54ac71fb365f --- /dev/null +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -0,0 +1,351 @@ +package com.aolda.itda.service; + +import com.aolda.itda.dto.auth.LoginRequestDTO; +import com.aolda.itda.dto.auth.LoginResponseDTO; +import com.aolda.itda.dto.auth.IdAndNameDTO; +import com.aolda.itda.entity.user.User; +import com.aolda.itda.exception.CustomException; +import com.aolda.itda.exception.ErrorCode; +import com.aolda.itda.repository.user.UserRepository; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; + +import java.util.*; + +@Service +@RequiredArgsConstructor +public class AuthService { + + @Value("${spring.server.keystone}") + private String keystone; + @Value("${spring.server.admin-id}") + private String adminId; + @Value("${spring.server.admin-password}") + private String adminPassword; + private final RestTemplate restTemplate = new RestTemplate(); + private final ObjectMapper objectMapper = new ObjectMapper(); + private final UserRepository userRepository; + + // 사용자 로그인 후 토큰 발행 및 Role 반환 + public LoginResponseDTO userLogin(HttpServletResponse response, LoginRequestDTO loginRequestDTO) throws JsonProcessingException { + Map<String, String> user = getToken(loginRequestDTO.getId(), loginRequestDTO.getPassword()); + + String userId = user.get("id"); + String token = user.get("token"); + String systemToken = getSystemToken(userId, loginRequestDTO.getPassword()); + + if (userId == null || token == null) { + throw new CustomException(ErrorCode.INVALID_USER_INFO); + } + + + User entity = userRepository.findByKeystoneId(userId).orElse(null); + if (entity == null) { + userRepository.save(User.builder().keystoneId(userId). + keystoneUsername(loginRequestDTO.getId()).build()); + } + else if (!entity.getKeystoneUsername().equals(loginRequestDTO.getId())) { + entity.changeUsername(loginRequestDTO.getId()); + userRepository.save(entity); + } + + response.addHeader("X-Subject-Token", systemToken != null ? systemToken : token); + return LoginResponseDTO.builder() + .isAdmin(systemToken != null) + .projects(getProjectsWithUser(user)) + .build(); + } + + // 특정 사용자의 토큰 발행 + private Map<String, String> getToken(String id, String password) { + + String url = keystone + "/auth/tokens"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + String requestBody = "{\n" + + " \"auth\": {\n" + + " \"identity\": {\n" + + " \"methods\": [\n" + + " \"password\"\n" + + " ],\n" + + " \"password\": {\n" + + " \"user\": {\n" + + " \"name\": \""+ id + "\",\n" + + " \"domain\": {\n" + + " \"name\": \"Default\"\n" + + " },\n" + + " \"password\": \"" + password + "\"\n" + + " }\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + + HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers); + ResponseEntity<Map> res; + try { + res = restTemplate.postForEntity(url, requestEntity, Map.class); + } catch (Exception e) { + throw new CustomException(ErrorCode.INVALID_USER_INFO); + } + Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token"); + Map<String, Object> resUser = (Map<String, Object>) resToken.get("user"); + String userId = (String) resUser.get("id"); + String token = res.getHeaders().getFirst("X-Subject-Token"); + + return Map.of("id", userId, + "token", token); + } + + private String getSystemToken(String id, String password) { + + String url = keystone + "/auth/tokens"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + String requestBody = "{\n" + + " \"auth\": {\n" + + " \"identity\": {\n" + + " \"methods\": [\n" + + " \"password\"\n" + + " ],\n" + + " \"password\": {\n" + + " \"user\": {\n" + + " \"id\": \"" + id + "\",\n" + + " \"password\": \"" + password + "\"\n" + + " }\n" + + " }\n" + + " },\n" + + " \"scope\": {\n" + + " \"system\": {\n" + + " \"all\": true\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + + HttpEntity<String> requestEntity; + ResponseEntity<Map> res; + try { + requestEntity = new HttpEntity<>(requestBody, headers); + res = restTemplate.postForEntity(url, requestEntity, Map.class); + } catch (Exception e) { + return null; + } + + Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token"); + Map<String, Object> resUser = (Map<String, Object>) resToken.get("user"); + String userId = (String) resUser.get("id"); + String token = res.getHeaders().getFirst("X-Subject-Token"); + + return token; + } + + private String getProjectToken(String unscopedToken, String projectId) { + + String url = keystone + "/auth/tokens"; + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + + + String requestBody = "{\n" + + " \"auth\": {\n" + + " \"identity\": {\n" + + " \"methods\": [\n" + + " \"token\"\n" + + " ],\n" + + " \"token\": {\n" + + " \"id\": \"" + unscopedToken +"\"\n" + + " }\n" + + " },\n" + + " \"scope\": {\n" + + " \"project\": {\n" + + " \"id\": \""+ projectId +"\"\n" + + " }\n" + + " }\n" + + " }\n" + + "}"; + + HttpEntity<String> requestEntity; + ResponseEntity<Map> res; + try { + requestEntity = new HttpEntity<>(requestBody, headers); + res = restTemplate.postForEntity(url, requestEntity, Map.class); + } catch (HttpClientErrorException.Forbidden e) { + return unscopedToken; + } + catch (Exception e) { + throw new CustomException(ErrorCode.INVALID_TOKEN); + } + + Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token"); + Map<String, Object> resUser = (Map<String, Object>) resToken.get("user"); + String userId = (String) resUser.get("id"); + String token = res.getHeaders().getFirst("X-Subject-Token"); + + return token; + } + + + // 특정 사용자의 특정 프로젝트 내 최고 권한 반환 + public Map<String, String> getBestRoleWithinProject(String token, String projectId) throws JsonProcessingException { + + return getBestRoleWithinProject(Map.of( + "id", validateTokenAndGetUserId(token), + "token", getProjectToken(token, projectId)), + projectId); + } + + private Map<String, String> getBestRoleWithinProject(Map<String, String> user, String projectId) throws JsonProcessingException { + String userId = user.get("id"); + String token = user.get("token"); + + if (userId == null || token == null) { + throw new CustomException(ErrorCode.INVALID_USER_INFO); + } + + String url = keystone + "/role_assignments?user.id=" + userId + "&effective&include_names=true&scope.project.id=" + projectId; + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Auth-Token", token); + + HttpEntity<String> requestEntity = new HttpEntity<>(headers); + ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); + + JsonNode node = objectMapper.readTree(res.getBody()); + ArrayNode arrayNode = (ArrayNode) node.get("role_assignments"); + + String bestRole = "reader"; + + for (JsonNode assignment : arrayNode) { + + String roleName = assignment.path("role").path("name").asText(); + + if (roleName.equals("admin")) { // admin인 경우 + bestRole = roleName; + } else if (roleName.equals("manager") && !bestRole.equals("admin")) { // 최고 권한이 admin이 아닌 경우 + bestRole = roleName; + } else if (roleName.equals("member") && bestRole.equals("reader")) { // 최고 권한이 reader인 경우 + bestRole = roleName; + } + + } + + return Map.of("role", bestRole); + } + + // 관리자용 토큰 발행 + public String getAdminToken() { + Map<String, String> user = getToken(adminId, adminPassword); + return user.get("token"); + } + + // 특정 사용자의 참여 프로젝트 반환 + public List<IdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException { + String userId = user.get("id"); + String token = user.get("token"); + if (userId == null || token == null) { + throw new CustomException(ErrorCode.INVALID_USER_INFO); + } + + String url = keystone + "/users/" + userId + "/projects"; + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Auth-Token", token); + + HttpEntity<String> requestEntity = new HttpEntity<>(headers); + ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); + + JsonNode node = objectMapper.readTree(res.getBody()); + ArrayNode arrayNode = (ArrayNode) node.get("projects"); + + List<IdAndNameDTO> lists = new ArrayList<>(); + + for (JsonNode assignment : arrayNode) { + String projectId = assignment.path("id").asText(); + String projectName = assignment.path("name").asText(); + lists.add(new IdAndNameDTO(projectId, projectName)); + } + return lists; + } + + public String validateTokenAndGetUserId(String token) throws JsonProcessingException { + String url = keystone + "/auth/tokens"; + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Auth-Token", token); + headers.set("X-Subject-Token", token); + HttpEntity<String> requestEntity = new HttpEntity<>(headers); + ResponseEntity<String> res; + try { + res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); + } catch (Exception e) { + throw new CustomException(ErrorCode.INVALID_TOKEN); + } + return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText(); + + } + + public List<IdAndNameDTO> getAllProjects(String token) throws JsonProcessingException { + String url = keystone + "/projects"; + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Auth-Token", token); + HttpEntity<String> requestEntity = new HttpEntity<>(headers); + ResponseEntity<String> res; + try { + res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); + } catch (Exception e) { + throw new CustomException(ErrorCode.INVALID_TOKEN); + } + + JsonNode node = objectMapper.readTree(res.getBody()); + ArrayNode arrayNode = (ArrayNode) node.get("projects"); + + List<IdAndNameDTO> lists = new ArrayList<>(); + + for (JsonNode assignment : arrayNode) { + String projectId = assignment.path("id").asText(); + String projectName = assignment.path("name").asText(); + lists.add(new IdAndNameDTO(projectId, projectName)); + } + + return lists; + + } + + public void validateProjectAuth(List<String> projects, String projectId) { + if (projects != null && !projects.contains(projectId)) { + throw new CustomException(ErrorCode.UNAUTHORIZED_USER); + } + } + + public Boolean isAdmin(Map<String, String> user) throws JsonProcessingException { + String url = keystone + "/role_assignments?user.id=" + user.get("id") + "&scope.system&include_names"; + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Auth-Token", user.get("token")); + HttpEntity<String> requestEntity = new HttpEntity<>(headers); + ResponseEntity<String> res; + try { + res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); + } catch (Exception e) { + return false; + } + JsonNode node = objectMapper.readTree(res.getBody()).path("role_assignments"); + String system_all = node.path("scope").path("system").path("all").asText(); + String role = node.path("role").path("name").asText(); + if (system_all.equals("true") && role.equals("admin")) { + return true; + } + return false; + + } +} diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java new file mode 100644 index 0000000000000000000000000000000000000000..7f426709d5480a8ed5564376ec12616c3a8bb8e6 --- /dev/null +++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java @@ -0,0 +1,279 @@ +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; +import com.aolda.itda.service.AuthService; +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; + +import java.io.BufferedReader; +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 +@RequiredArgsConstructor +@Slf4j +public class CertificateService { + + @Value("${spring.server.admin-project}") + private String adminProject; + private final CertificateRepository certificateRepository; + private final AuthService authService; + + /** 1) 단건 조회 **/ + public CertificateDTO getCertificate(Long certificateId, List<String> projects) { + Certificate cert = certificateRepository + .findByCertificateIdAndIsDeleted(certificateId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + authService.validateProjectAuth(projects, cert.getProjectId()); + 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) { + Set<Certificate> set = new HashSet<>(); + // 도메인이 입력된 경우 처리 + if (domain != null && !domain.isBlank()) { + // 서브도메인이 있는 경우 + 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 { + set.addAll(certificateRepository + .findByProjectIdAndIsDeleted(projectId, false)); + set.addAll(certificateRepository + .findByProjectIdAndIsDeleted(adminProject, false)); + } + List<CertificateDTO> dtos = set.stream() + .map(this::toDTO) + .toList(); + return PageResp.<CertificateDTO>builder() + .contents(dtos) + .build(); + } + + /** 2) 생성: expiredAt 자동 90일 설정 + 로깅 **/ + public CertificateDTO createCertificate(String projectId, + CertificateDTO dto, + List<String> projects) { + log.info("createCertificate start (project={})", projectId); + authService.validateProjectAuth(projects, projectId); + validateDTO(dto); + + // 발급 + executeLego(dto); + log.info("certificate issued for domain={}", dto.getDomain()); + + // 엔티티 저장 (expiredAt 기본 90일 뒤) + Certificate cert = Certificate.builder() + .projectId(projectId) + .domain(dto.getDomain()) + .email(dto.getEmail()) + .challenge(dto.getChallenge()) + .expiresAt(LocalDateTime.now().plusDays(90)) + .isDeleted(false) + .apiToken(dto.getApiToken()) + .build(); + certificateRepository.save(cert); + log.info("certificate saved (id={}, domain={})", + cert.getCertificateId(), cert.getDomain()); + + return toDTO(cert); + } + + /** 4) 수정 **/ + public CertificateDTO editCertificate(Long certificateId, + CertificateDTO dto, + List<String> projects) { + Certificate cert = certificateRepository + .findByCertificateIdAndIsDeleted(certificateId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + authService.validateProjectAuth(projects, cert.getProjectId()); + + if (dto.getDomain() != null) cert.setDomain(dto.getDomain()); + if (dto.getEmail() != null) cert.setEmail(dto.getEmail()); + + certificateRepository.save(cert); + log.info("certificate edited (id={}, domain={})", + cert.getCertificateId(), cert.getDomain()); + return toDTO(cert); + } + + /** 4) 삭제 + 로깅 **/ + public void deleteCertificate(Long certificateId, List<String> projects) { + Certificate cert = certificateRepository + .findByCertificateIdAndIsDeleted(certificateId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + authService.validateProjectAuth(projects, cert.getProjectId()); + + cert.setIsDeleted(true); + certificateRepository.save(cert); + log.info("certificate deleted (id={}, domain={})", + cert.getCertificateId(), cert.getDomain()); + } + + /** 3) 만료 30일 전 자동 갱신 배치 **/ + @Scheduled(cron = "0 0 3 * * *", zone = "Asia/Seoul") + public void renewExpiringCertificates() { + LocalDateTime threshold = LocalDateTime.now().plusDays(30); + List<Certificate> expiring = certificateRepository + .findByExpiresAtBeforeAndIsDeleted(threshold, false); + + for (Certificate cert : expiring) { + try { + log.info("renewing (id={}, domain={})", cert.getCertificateId(), cert.getDomain()); + CertificateDTO dto = CertificateDTO.builder() + .domain(cert.getDomain()) + .email(cert.getEmail()) + .challenge(cert.getChallenge()) + .apiToken(cert.getApiToken()) + .build(); + executeLego(dto); + + cert.setExpiresAt(LocalDateTime.now().plusDays(90)); + certificateRepository.save(cert); + log.info("renewed (id={}, newExpiry={})", + cert.getCertificateId(), cert.getExpiresAt()); + } catch (Exception e) { + log.error("failed to renew (id={}, domain={}): {}", + cert.getCertificateId(), cert.getDomain(), e.getMessage()); + } + } + } + + /** DTO 유효성 검사 **/ + private void validateDTO(CertificateDTO dto) { + for (ConstraintViolation<CertificateDTO> v : + Validation.buildDefaultValidatorFactory() + .getValidator().validate(dto)) { + throw new CustomException(ErrorCode.INVALID_CONF_INPUT, v.getMessage()); + } + } + + /** Entity→DTO 변환 **/ + private CertificateDTO toDTO(Certificate cert) { + return CertificateDTO.builder() + .id(cert.getCertificateId()) + + .domain(cert.getDomain()) + .email(cert.getEmail()) + .challenge(cert.getChallenge()) + .expiresAt(cert.getExpiresAt()) + .createdAt(cert.getCreatedAt()) + .updatedAt(cert.getUpdatedAt()) + .isDeleted(cert.getIsDeleted()) + .apiToken(cert.getApiToken()) + .projectId(cert.getProjectId()) + .build(); + } + + /** 인증서 발급용 lego 실행 **/ + private void executeLego(CertificateDTO dto) { + if (dto.getChallenge() == Challenge.DNS_CLOUDFLARE && dto.getApiToken() == null) { + throw new CustomException(ErrorCode.INVALID_CONF_INPUT, + "DNS_CLOUDFLARE 챌린지는 apiToken 필요"); + } + + List<String> cmd = new ArrayList<>(); + cmd.add("/usr/local/bin/lego"); + cmd.add("--accept-tos"); + cmd.add("--email"); cmd.add(dto.getEmail()); + cmd.add("--path"); cmd.add("/data/lego"); + + if (dto.getChallenge() == Challenge.HTTP) { + cmd.add("--http"); + cmd.add("--http.webroot"); cmd.add("/data/letsencrypt-acme-challenge"); + } else { + cmd.add("--dns"); cmd.add("cloudflare"); + } + + cmd.add("--domains"); cmd.add(dto.getDomain()); + cmd.add("run"); + + log.info("executing lego: {}", String.join(" ", cmd)); + ProcessBuilder pb = new ProcessBuilder(cmd) + .redirectErrorStream(true); + pb.environment().put("CF_DNS_API_TOKEN", dto.getApiToken()); + + try { + Process p = pb.start(); + try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + r.lines().forEach(line -> log.info("[lego] {}", line)); + } + if (p.waitFor() != 0) { + throw new CustomException(ErrorCode.FAIL_CREATE_CERT, + "lego exit code=" + p.exitValue()); + } + } catch (IOException | InterruptedException e) { + log.error("lego error", e); + throw new CustomException(ErrorCode.FAIL_CREATE_CERT, + "lego 실행 실패: " + e.getMessage()); + } + } + +} diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java new file mode 100644 index 0000000000000000000000000000000000000000..e6fc9d45ee66e4f74ac7898bd2c43de0544de86a --- /dev/null +++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java @@ -0,0 +1,307 @@ +package com.aolda.itda.service.forwarding; + +import com.aolda.itda.dto.PageResp; +import com.aolda.itda.dto.forwarding.ForwardingDTO; +import com.aolda.itda.entity.forwarding.Forwarding; +import com.aolda.itda.exception.CustomException; +import com.aolda.itda.exception.ErrorCode; +import com.aolda.itda.repository.forwarding.ForwardingRepository; +import com.aolda.itda.service.AuthService; +import com.aolda.itda.template.ForwardingTemplate; +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.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.regex.Pattern; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class ForwardingService { + + @Value("${spring.server.base-ip}") + private String serverBaseIp; + private final ForwardingTemplate forwardingTemplate; + private final ForwardingRepository forwardingRepository; + private final AuthService authService; + private final RestTemplate restTemplate = new RestTemplate(); + + /* 포트포워딩 정보 조회 */ + public ForwardingDTO getForwarding(Long forwardingId, List<String> projects) { + Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + + /* 프로젝트 권한 검증 */ + authService.validateProjectAuth(projects, forwarding.getProjectId()); + + return forwarding.toForwardingDTO(); + } + + /* 포트포워딩 목록 조회 + 검색 */ + public PageResp<ForwardingDTO> getForwardingsWithSearch(String projectId, String query) { + + /* 입력 검증 */ + if (query == null || query.isBlank()) { + return PageResp.<ForwardingDTO>builder() + .contents(forwardingRepository.findByProjectIdAndIsDeleted(projectId, false) + .stream() + .map(Forwarding::toForwardingDTO) + .toList()).build(); + } + + return PageResp.<ForwardingDTO>builder() + .contents(forwardingRepository.findWithSearch(projectId, query, false) + .stream() + .map(Forwarding::toForwardingDTO) + .toList()).build(); + } + + /* 포트포워딩 생성 */ + public ForwardingDTO createForwarding(String projectId, ForwardingDTO dto) { + + /* 입력 DTO 검증 */ + validateDTO(dto); + + /* 중복 검증 */ + if (forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(dto.getInstanceIp(), dto.getInstancePort(), false)) { + throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO); + } + + if (forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) { + throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT); + } + + /* 포트포워딩 엔티티 생성 */ + Forwarding forwarding = Forwarding.builder() + .isDeleted(false) + .projectId(projectId) + .name(dto.getName()) + .serverIp(dto.getServerIp() == null ? serverBaseIp : dto.getServerIp()) + .serverPort(dto.getServerPort()) + .instanceIp(dto.getInstanceIp()) + .instancePort(dto.getInstancePort()) + .build(); + + forwardingRepository.save(forwarding); + + /* nginx conf 파일 생성 및 예외 처리 */ + String content = forwardingTemplate.getPortForwardingWithTCP(dto.getServerPort(), dto.getInstanceIp(), dto.getInstancePort(), dto.getName()); + String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf"; + + File file = new File(confPath); + try { + Path path = Paths.get(confPath); + Files.createDirectories(path.getParent()); + if (!file.createNewFile()) { + throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "중복된 포트포워딩 Conf 파일이 존재합니다"); + } + } catch (IOException e) { + throw new CustomException(ErrorCode.FAIL_CREATE_CONF); + } + + /* conf 파일 작성 및 예외 처리 */ + try { + BufferedWriter bw = new BufferedWriter(new FileWriter(file, false)); // 예외처리 필요 + bw.write(content); + bw.flush(); + bw.close(); + } catch (Exception e) { + if (file.delete()) { + throw new CustomException(ErrorCode.FAIL_DELETE_CONF); + } + throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다"); + } + + /* nginx test */ + String url = "http://nginx:8081/nginx-api/test"; + try { + restTemplate.getForEntity(url, String.class); + } catch (HttpServerErrorException.InternalServerError e) { + log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); + if (file.delete()) { + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST); + } catch (Exception e) { + if (file.delete()) { + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST); + } + + /* nginx reload */ + url = "http://nginx:8081/nginx-api/reload"; + try { + restTemplate.getForEntity(url, String.class); + } catch (HttpServerErrorException.InternalServerError e) { + log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); + if (file.delete()) { + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); + } catch (Exception e) { + if (file.delete()) { + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); + } + return forwarding.toForwardingDTO(); + } + + /* 포트포워딩 정보 수정 */ + public void editForwarding(Long forwardingId, ForwardingDTO dto, List<String> projects) { + Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + + /* 프로젝트 권한 검증 */ + authService.validateProjectAuth(projects, forwarding.getProjectId()); + + /* 중복 검증 */ + if (dto.getServerPort() != null && forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) { + forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false); + throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT); + } + + if (!(dto.getInstanceIp() == null && dto.getInstancePort() == null) && + forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted( + dto.getInstanceIp() == null ? forwarding.getInstanceIp() : dto.getInstanceIp() + , dto.getInstancePort() == null ? forwarding.getInstancePort() : dto.getInstancePort() + , false)) { + throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO); + } + + /* 파일 수정 */ + forwarding.edit(dto); + String content = forwardingTemplate.getPortForwardingWithTCP(forwarding.getServerPort(), + forwarding.getInstanceIp(), + forwarding.getInstancePort(), + forwarding.getName()); + String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf"; + File file = new File(confPath); + if (!file.exists()) { + throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 수정할 수 없습니다"); + } + + Path backup; + try { + backup = Files.createTempFile("temp_", ".tmp"); + Files.copy(Paths.get(confPath), backup, StandardCopyOption.REPLACE_EXISTING + , StandardCopyOption.COPY_ATTRIBUTES); + + BufferedWriter bw = new BufferedWriter(new FileWriter(file, false)); + bw.write(content); + bw.flush(); + bw.close(); + } catch (Exception e) { + throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "포트포워딩 Conf 파일을 수정하지 못했습니다"); + } + + /* nginx test */ + String url = "http://nginx:8081/nginx-api/test"; + try { + restTemplate.getForEntity(url, String.class); + } catch (RuntimeException e) { + + try { + Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING + , StandardCopyOption.COPY_ATTRIBUTES); + Files.delete(backup); + } catch (IOException e1) { + throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(포트포워딩 Conf 파일 수정)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST); + } + + /* nginx reload */ + url = "http://nginx:8081/nginx-api/reload"; + try { + restTemplate.getForEntity(url, String.class); + } catch (RuntimeException e) { + try { + Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING + , StandardCopyOption.COPY_ATTRIBUTES); + Files.delete(backup); + } catch (IOException e1) { + throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(포트포워딩 Conf 파일 수정)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); + } + + /* DB 정보 수정 */ + forwardingRepository.save(forwarding); + } + + /* 포트포워딩 삭제 (소프트) */ + public void deleteForwarding(Long forwardingId, List<String> projects) { + Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + + /* 프로젝트 권한 검증 */ + authService.validateProjectAuth(projects, forwarding.getProjectId()); + + /* 파일 삭제 */ + String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf"; + String deletePath = confPath + ".deleted"; + try { + Files.move(Paths.get(confPath), Paths.get(deletePath)); + } catch (IOException e) { + throw new CustomException(ErrorCode.FAIL_DELETE_CONF); + } + + /* nginx test */ + String url = "http://nginx:8081/nginx-api/test"; + try { + restTemplate.getForEntity(url, String.class); + } catch (Exception e) { + try { + Files.move(Paths.get(deletePath), Paths.get(confPath)); + } catch (IOException e1) { + throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(포트포워딩 Conf 삭제)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST); + } + + /* nginx reload */ + url = "http://nginx:8081/nginx-api/reload"; + try { + restTemplate.getForEntity(url, String.class); + } catch (Exception e) { + try { + Files.move(Paths.get(deletePath), Paths.get(confPath)); + } catch (IOException e1) { + throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(포트포워딩 Conf 삭제)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); + } + + /* DB */ + forwarding.delete(); + forwardingRepository.save(forwarding); + + } + + /* 입력 DTO 검증 */ + private void validateDTO(ForwardingDTO dto) { + + for (ConstraintViolation<ForwardingDTO> violation : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) { + throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage()); + } + + } +} diff --git a/src/main/java/com/aolda/itda/service/log/LogService.java b/src/main/java/com/aolda/itda/service/log/LogService.java new file mode 100644 index 0000000000000000000000000000000000000000..b33cc2e4a1e923ed046bea7aaff6ddc18c345c53 --- /dev/null +++ b/src/main/java/com/aolda/itda/service/log/LogService.java @@ -0,0 +1,57 @@ +package com.aolda.itda.service.log; + +import com.aolda.itda.dto.PageResp; +import com.aolda.itda.dto.log.LogDTO; +import com.aolda.itda.entity.log.Log; +import com.aolda.itda.exception.CustomException; +import com.aolda.itda.exception.ErrorCode; +import com.aolda.itda.repository.log.LogRepository; +import com.aolda.itda.repository.log.LogQueryDSL; +import com.aolda.itda.service.AuthService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class LogService { + + private final LogRepository logRepository; + private final LogQueryDSL logQueryDSL; + private final AuthService authService; + + /* CUD 로그 조회 */ + public LogDTO getLog(Long logId, List<String> projects) { + Log log = logRepository.findById(logId) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_LOG)); + + /* 프로젝트 권한 검증 */ + authService.validateProjectAuth(projects, log.getProjectId()); + + return log.toLogDTO(); + } + + /* CUD 로그 목록 조회 */ + public PageResp<LogDTO> getLogs(String projectId, String type, String username, String action, Boolean isASC, + Pageable pageable, Map<String, String> user) { + + if (projectId == null) { + try { + if(!authService.isAdmin(user)) throw new CustomException(ErrorCode.UNAUTHORIZED_USER); + } + catch (Exception e) { + throw new CustomException(ErrorCode.UNAUTHORIZED_USER); + } + } + + return logQueryDSL.getLogs(projectId, type, username, action, isASC, pageable); + } + +} diff --git a/src/main/java/com/aolda/itda/service/main/MainService.java b/src/main/java/com/aolda/itda/service/main/MainService.java new file mode 100644 index 0000000000000000000000000000000000000000..8a4345eb4ab48685fbd7e80e5b6900bbd433bffa --- /dev/null +++ b/src/main/java/com/aolda/itda/service/main/MainService.java @@ -0,0 +1,61 @@ +package com.aolda.itda.service.main; + +import com.aolda.itda.dto.PageResp; +import com.aolda.itda.dto.auth.IdAndNameDTO; +import com.aolda.itda.dto.main.MainInfoDTO; +import com.aolda.itda.repository.certificate.CertificateRepository; +import com.aolda.itda.repository.forwarding.ForwardingRepository; +import com.aolda.itda.repository.routing.RoutingRepository; +import com.aolda.itda.service.AuthService; +import com.fasterxml.jackson.core.JsonProcessingException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; + +@Service +@Transactional +@RequiredArgsConstructor +public class MainService { + + private final AuthService authService; + private final RoutingRepository routingRepository; + private final ForwardingRepository forwardingRepository; + private final CertificateRepository certificateRepository; + + /* 메인 페이지에 필요한 정보 반환 */ + public MainInfoDTO getMainInfo(String projectId, List<String> projects) { + + /* 프로젝트 권한 검증 */ + authService.validateProjectAuth(projects, projectId); + + /* 카운팅 */ + Long routing = (long) routingRepository.findByProjectIdAndIsDeleted(projectId, false).size(); + Long forwarding = (long) forwardingRepository.findByProjectIdAndIsDeleted(projectId, false).size(); + Long certificate = 0L; + + return MainInfoDTO.builder() + .routing(routing) + .forwarding(forwarding) + .certificate(certificate) + .build(); + } + + /* 접근 가능한 프로젝트 조회 */ + public PageResp<IdAndNameDTO> getAllProjects(Map<String, String> user) throws JsonProcessingException { + + List<IdAndNameDTO> projects; + if (authService.isAdmin(user)) { + projects = authService.getAllProjects(user.get("token")); + } + + else { + projects = authService.getProjectsWithUser(user); + } + + return PageResp.<IdAndNameDTO>builder() + .contents(projects).build(); + } +} diff --git a/src/main/java/com/aolda/itda/service/routing/RoutingService.java b/src/main/java/com/aolda/itda/service/routing/RoutingService.java new file mode 100644 index 0000000000000000000000000000000000000000..8a50622ae8ec705c49df00623796dc07d2184eb0 --- /dev/null +++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java @@ -0,0 +1,325 @@ +package com.aolda.itda.service.routing; + +import com.aolda.itda.dto.PageResp; +import com.aolda.itda.dto.routing.RoutingDTO; +import com.aolda.itda.entity.certificate.Certificate; +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; +import com.aolda.itda.repository.routing.RoutingRepository; +import com.aolda.itda.service.AuthService; +import com.aolda.itda.template.RoutingTemplate; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestTemplate; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; +import java.util.regex.Pattern; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class RoutingService { + + private final RoutingRepository routingRepository; + private final CertificateRepository certificateRepository; + private final AuthService authService; + private final RoutingTemplate routingTemplate; + private final RestTemplate restTemplate = new RestTemplate(); + + /* Routing 조회 */ + public RoutingDTO getRouting(Long routingId, List<String> projects) { + Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING)); + + /* 프로젝트 권한 검증 */ + authService.validateProjectAuth(projects, routing.getProjectId()); + + return routing.toRoutingDTO(); + } + + /* Routing 목록 조회 + 검색 */ + public PageResp<RoutingDTO> getRoutingsWithSearch(String projectId, String query) { + + /* 입력 검증 */ + if (query == null || query.isBlank()) { + return PageResp.<RoutingDTO>builder() + .contents(routingRepository.findByProjectIdAndIsDeleted(projectId, false) + .stream() + .map(Routing::toRoutingDTO) + .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.<RoutingDTO>builder() + .contents(routingRepository.findWithSearch(projectId, query, false) + .stream() + .map(Routing::toRoutingDTO) + .toList()).build(); + } + + /* Routing 생성 */ + public RoutingDTO createRouting(String projectId, RoutingDTO dto) throws IOException { + /* 입력 DTO 검증 */ + validateDTO(dto); + + /* 중복 검증 */ + if (routingRepository.existsByDomainAndIsDeleted(dto.getDomain(), false)) { + throw new CustomException(ErrorCode.DUPLICATED_DOMAIN_NAME); + } + + /* SSL 인증서 조회 */ + Certificate certificate = dto.getCertificateId() == -1 ? null : + certificateRepository.findById(dto.getCertificateId()) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요 + + /* 라우팅 엔티티 생성 */ + Routing routing = Routing.builder() + .isDeleted(false) + .projectId(projectId) + .name(dto.getName()) + .instanceIp(dto.getIp()) + .instancePort(dto.getPort()) + .domain(dto.getDomain()) + .certificate(certificate) + .caching(dto.getCaching()) + .build(); + + routingRepository.save(routing); + + /* nginx conf 파일 생성 및 예외 처리 */ + String content = routingTemplate.getRouting(dto, certificate == null ? null : certificate.formatDomain()); + String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf"; + + File file = new File(confPath); + try { + Path path = Paths.get(confPath); + Files.createDirectories(path.getParent()); + if (!file.createNewFile()) { + throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "중복된 라우팅 Conf 파일이 존재합니다"); + } + } catch (IOException e) { + throw new CustomException(ErrorCode.FAIL_CREATE_CONF); + } + + /* conf 파일 작성 및 예외 처리 */ + try { + BufferedWriter bw = new BufferedWriter(new FileWriter(file, false)); // 예외처리 필요 + bw.write(content); + bw.flush(); + bw.close(); + } catch (Exception e) { + if (!file.delete()) { + throw new CustomException(ErrorCode.FAIL_DELETE_CONF); + } + throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다"); + } + + /* nginx test */ + String url = "http://nginx:8081/nginx-api/test"; + try { + restTemplate.getForEntity(url, String.class); + + } catch (HttpClientErrorException | HttpServerErrorException e) { + String responseBody = e.getResponseBodyAsString(); + System.err.println("Response Body: " + responseBody); + + Path filePath = Paths.get(confPath); + List<String> lines = Files.readAllLines(filePath); + + // 파일 내용 출력 + for (String line : lines) { + System.out.println(line); + } + + if (!file.delete()) { + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST); + } + + /* nginx reload */ + url = "http://nginx:8081/nginx-api/reload"; + try { + restTemplate.getForEntity(url, String.class); + } catch (Exception e) { + if (!file.delete()) { + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD, "(롤백 실패)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); + } + + return routing.toRoutingDTO(); + } + + /* Routing 수정 */ + public void editRouting(Long routingId, RoutingDTO dto, List<String> projects) throws IOException { + Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING)); + + /* 프로젝트 권한 검증 */ + authService.validateProjectAuth(projects, routing.getProjectId()); + + /* 중복 검증 */ + if (dto.getDomain() != null && routingRepository.existsByDomainAndIsDeleted(dto.getDomain(), false)) { + throw new CustomException(ErrorCode.DUPLICATED_DOMAIN_NAME); + } + + /* SSL 인증서 조회 */ + Certificate certificate; + if (dto.getCertificateId() == null) { + certificate = routing.getCertificate(); + } + else if (dto.getCertificateId() == -1) { + certificate = null; + } else { + certificate = certificateRepository.findById(dto.getCertificateId()) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요 + } + + /* 파일 수정 */ + routing.edit(dto, certificate); + RoutingDTO tmp = routing.toRoutingDTO(); + if (tmp.getCertificateId() == null) tmp.setCertificateId( (long) -1); + String content = routingTemplate.getRouting(tmp, certificate == null ? null : certificate.formatDomain()); + String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf"; + File file = new File(confPath); + if (!file.exists()) { + throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 수정할 수 없습니다"); + } + + Path backup; + try { + backup = Files.createTempFile("temp_", ".tmp"); + Files.copy(Paths.get(confPath), backup, StandardCopyOption.REPLACE_EXISTING + , StandardCopyOption.COPY_ATTRIBUTES); + + BufferedWriter bw = new BufferedWriter(new FileWriter(file, false)); + bw.write(content); + bw.flush(); + bw.close(); + } catch (Exception e) { + throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "라우팅 Conf 파일을 수정하지 못했습니다"); + } + + /* nginx test */ + String url = "http://nginx:8081/nginx-api/test"; + try { + restTemplate.getForEntity(url, String.class); + } catch (HttpClientErrorException | HttpServerErrorException e) { + String responseBody = e.getResponseBodyAsString(); + System.err.println("Response Body: " + responseBody); + + Path filePath = Paths.get(confPath); + List<String> lines = Files.readAllLines(filePath); + + // 파일 내용 출력 + for (String line : lines) { + System.out.println(line); + } + try { + Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING + , StandardCopyOption.COPY_ATTRIBUTES); + Files.delete(backup); + } catch (IOException e1) { + throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST); + } + + /* nginx reload */ + url = "http://nginx:8081/nginx-api/reload"; + try { + restTemplate.getForEntity(url, String.class); + } catch (RuntimeException e) { + try { + Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING + , StandardCopyOption.COPY_ATTRIBUTES); + Files.delete(backup); + } catch (IOException e1) { + throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "(라우팅 Conf 파일 수정)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); + } + + /* DB 정보 수정 */ + routingRepository.save(routing); + } + + /* Routing 삭제 */ + public void deleteRouting(Long routingId, List<String> projects) { + Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING)); + + /* 프로젝트 권한 검증 */ + authService.validateProjectAuth(projects, routing.getProjectId()); + + /* 파일 삭제 */ + String confPath = "/data/nginx/proxy_host/" + routing.getRoutingId() + ".conf"; + String deletePath = confPath + ".deleted"; + try { + Files.move(Paths.get(confPath), Paths.get(deletePath)); + } catch (IOException e) { + throw new CustomException(ErrorCode.FAIL_DELETE_CONF); + } + + /* nginx test */ + String url = "http://nginx:8081/nginx-api/test"; + try { + restTemplate.getForEntity(url, String.class); + } catch (Exception e) { + try { + Files.move(Paths.get(deletePath), Paths.get(confPath)); + } catch (IOException e1) { + throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST); + } + + /* nginx reload */ + url = "http://nginx:8081/nginx-api/reload"; + try { + restTemplate.getForEntity(url, String.class); + } catch (Exception e) { + try { + Files.move(Paths.get(deletePath), Paths.get(confPath)); + } catch (IOException e1) { + throw new CustomException(ErrorCode.FAIL_ROLL_BACK, "(라우팅 Conf 삭제)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); + } + + /* DB */ + routing.delete(); + routingRepository.save(routing); + } + + private void validateDTO(RoutingDTO dto) { + + for (ConstraintViolation<RoutingDTO> violation : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) { + throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage()); + } + + } +} diff --git a/src/main/java/com/aolda/itda/template/ForwardingTemplate.java b/src/main/java/com/aolda/itda/template/ForwardingTemplate.java new file mode 100644 index 0000000000000000000000000000000000000000..6d8bf2cedcba6f6122cb7b93b80b95078745554d --- /dev/null +++ b/src/main/java/com/aolda/itda/template/ForwardingTemplate.java @@ -0,0 +1,16 @@ +package com.aolda.itda.template; + +import org.springframework.stereotype.Component; + +@Component +public class ForwardingTemplate { + + public String getPortForwardingWithTCP(String serverPort, String instanceIp, String instancePort, String name) { + return "# " + name + "\n" + + "server { \n" + + " listen " + serverPort + "; \n" + + " listen [::]:" + serverPort + "; \n" + + " proxy_pass " + instanceIp + ":" + instancePort + ";\n" + + "} \n"; + } +} diff --git a/src/main/java/com/aolda/itda/template/OptionTemplate.java b/src/main/java/com/aolda/itda/template/OptionTemplate.java new file mode 100644 index 0000000000000000000000000000000000000000..add4dfcceee3a419aad336c369ec434f47a8b8e7 --- /dev/null +++ b/src/main/java/com/aolda/itda/template/OptionTemplate.java @@ -0,0 +1,26 @@ +package com.aolda.itda.template; + +import org.springframework.stereotype.Component; + +@Component +public class OptionTemplate { + + public String getSSL(String certificateDomain) { + return "\ninclude conf.d/include/letsencrypt-acme-challenge.conf;\n" + + "include conf.d/include/ssl-ciphers.conf;\n" + + "ssl_certificate /data/lego/certificates/" + certificateDomain + ".crt;\n" + + "ssl_certificate_key /data/lego/certificates/" + certificateDomain + ".key;\n"; + } + + public String getAssetCaching() { + return "\ninclude conf.d/include/assets.conf;\n"; + } + + public String getBlockExploits() { + return "\ninclude conf.d/include/block-exploits.conf;\n"; + } + + public String getForceSSL() { + return "\ninclude conf.d/include/force-ssl.conf;\n"; + } +} diff --git a/src/main/java/com/aolda/itda/template/RoutingTemplate.java b/src/main/java/com/aolda/itda/template/RoutingTemplate.java new file mode 100644 index 0000000000000000000000000000000000000000..8a53785b704298cf74767176635369806265bf73 --- /dev/null +++ b/src/main/java/com/aolda/itda/template/RoutingTemplate.java @@ -0,0 +1,44 @@ +package com.aolda.itda.template; + +import com.aolda.itda.dto.routing.RoutingDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RoutingTemplate { + + private final OptionTemplate optionTemplate; + + public String getRouting(RoutingDTO dto, String certificateDomain) { + return "server { \n" + + "set $forward_scheme http;\n" + + "set $server \"" + dto.getIp() +"\";\n" + + "set $port "+ dto.getPort() +";\n" + + "\n" + + "listen 80;\n" + + "listen [::]:80;\n" + + (dto.getCertificateId() == -1 ? "" : + "listen 443 ssl;\nlisten [::]:443 ssl;\n") + + "server_name " + dto.getDomain() + ";\n" + + (dto.getCertificateId() == -1 ? "" : + optionTemplate.getSSL(certificateDomain)) + + (!dto.getCaching() ? "" : + optionTemplate.getAssetCaching() + ) + + "proxy_set_header Upgrade $http_upgrade;\n" + + "proxy_set_header Connection $http_connection;\n" + + "proxy_http_version 1.1;\n" + + "\n" + + "access_log /data/logs/proxy-host-" + dto.getId() + "_access.log proxy;\n" + + "error_log /data/logs/proxy-host-" + dto.getId() + "_error.log warn;\n" + + "location / { \n" + + "proxy_set_header Upgrade $http_upgrade;\n" + + "proxy_set_header Connection $http_connection;\n" + + "proxy_http_version 1.1;\n" + + "include conf.d/include/proxy.conf;\n" + + "}\n" + + "}\n"; + } + +} diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000000000000000000000000000000000..39e9337fdac44b059f0a540fc6764747ed91d193 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<configuration> + + <property name="MAX_FILE_SIZE" value="10MB" /> + <property name="TOTAL_SIZE" value="1GB" /> + <property name="MAX_HISTORY" value="30" /> + + <!-- 콘솔에 출력할 로그 형식 --> + <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> + <encoder> + <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern> + </encoder> + </appender> + + <!-- INFO 로그 파일 저장 (1개당 10MB, 5개까지 유지, 이후 압축) --> + <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>/data/logs/info.log</file> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <fileNamePattern>/data/logs/info.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> + <maxHistory>${MAX_HISTORY}</maxHistory> + <totalSizeCap>${TOTAL_SIZE}</totalSizeCap> + <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> + <maxFileSize>${MAX_FILE_SIZE}</maxFileSize> + </timeBasedFileNamingAndTriggeringPolicy> + </rollingPolicy> + <encoder> + <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern> + </encoder> + <!-- INFO 레벨만 허용 --> + <filter class="ch.qos.logback.classic.filter.LevelFilter"> + <level>INFO</level> + <onMatch>ACCEPT</onMatch> + <onMismatch>DENY</onMismatch> + </filter> + </appender> + + <!-- WARN 로그 파일 저장 --> + <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>/data/logs/warn.log</file> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <fileNamePattern>/data/logs/warn.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> + <maxHistory>${MAX_HISTORY}</maxHistory> + <totalSizeCap>${TOTAL_SIZE}</totalSizeCap> + <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> + <maxFileSize>${MAX_FILE_SIZE}</maxFileSize> + </timeBasedFileNamingAndTriggeringPolicy> + </rollingPolicy> + <encoder> + <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern> + </encoder> + <!-- WARN 레벨만 허용 --> + <filter class="ch.qos.logback.classic.filter.LevelFilter"> + <level>WARN</level> + <onMatch>ACCEPT</onMatch> + <onMismatch>DENY</onMismatch> + </filter> + </appender> + + <!-- ERROR 로그 파일 저장 --> + <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> + <file>/data/logs/error.log</file> + <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> + <fileNamePattern>/data/logs/error.%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern> + <maxHistory>${MAX_HISTORY}</maxHistory> + <totalSizeCap>${TOTAL_SIZE}</totalSizeCap> + <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> + <maxFileSize>${MAX_FILE_SIZE}</maxFileSize> + </timeBasedFileNamingAndTriggeringPolicy> + </rollingPolicy> + <encoder> + <pattern>[%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n</pattern> + </encoder> + <!-- ERROR 레벨만 허용 --> + <filter class="ch.qos.logback.classic.filter.LevelFilter"> + <level>ERROR</level> + <onMatch>ACCEPT</onMatch> + <onMismatch>DENY</onMismatch> + </filter> + </appender> + + <logger name="com.aolda.itda" additivity="false"> + <!-- 각 Appender 참조 (필터는 Appender 내부에 정의됨) --> + <appender-ref ref="INFO_FILE"/> + <appender-ref ref="WARN_FILE"/> + <appender-ref ref="ERROR_FILE"/> + <!-- 콘솔 출력 --> + <appender-ref ref="CONSOLE"/> + </logger> + +</configuration> \ No newline at end of file