diff --git a/build.gradle b/build.gradle index b88f4542cc05422dcee3e5491eb9d93ed75f7ec7..1b29e4ade60f1ece03c8e7680004ea772cfc1ef3 100644 --- a/build.gradle +++ b/build.gradle @@ -30,12 +30,19 @@ dependencies { compileOnly 'org.projectlombok:lombok' runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' runtimeOnly 'com.mysql:mysql-connector-j' - implementation 'org.pacesys:openstack4j:3.1.0' 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/aspect/ForwardingLogAspect.java b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java new file mode 100644 index 0000000000000000000000000000000000000000..f515ab60257a9ff15d65d91080ed3fb94967d53a --- /dev/null +++ b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java @@ -0,0 +1,154 @@ +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.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.getSession().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.UPDATE) + .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.getSession().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.UPDATE) + .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.getSession().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().equals(newObj.getInstanceIp()) ? "" : (" -> " + newObj.getInstanceIp())) + "\n" + + "instancePort: " + (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..c8b052a7d260dc4e27c36b978f76e2375a478a2e --- /dev/null +++ b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java @@ -0,0 +1,157 @@ +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.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.getSession().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" + + "ip: " + routing.getInstancePort() + "\n" + + "certificateId: " + routing.getCertificate().getCertificateId() + "\n" + + "caching: " + routing.getCaching() + "\n"; + + /* 로그 엔티티 저장 */ + logRepository.save(Log.builder() + .user(user) + .objectType(ObjectType.FORWARDING) + .objectId(routing.getRoutingId()) + .action(Action.UPDATE) + .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.getSession().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" + + "ip: " + routing.getInstancePort() + "\n" + + "certificateId: " + routing.getCertificate().getCertificateId() + "\n" + + "caching: " + routing.getCaching() + "\n"; + + /* 로그 엔티티 저장 */ + logRepository.save(Log.builder() + .user(user) + .objectType(ObjectType.FORWARDING) + .objectId(routing.getRoutingId()) + .action(Action.UPDATE) + .projectId(routing.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.getSession().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().equals(newObj.getInstanceIp()) ? "" : (" -> " + newObj.getInstanceIp())) + "\n" + + "port: " + (old.getInstancePort().equals(newObj.getInstancePort()) ? "" : (" -> " + newObj.getInstancePort())) + "\n" + + "certificateId: " + (old.getCertificate().getCertificateId() == newObj.getCertificate().getCertificateId() ? "" : (" -> " + newObj.getCertificate().getCertificateId())) + + "certificateId: " + (old.getCaching() == newObj.getCaching() ? "" : (" -> " + newObj.getCaching())); + + /* 로그 엔티티 저장 */ + logRepository.save(Log.builder() + .user(user) + .objectType(ObjectType.FORWARDING) + .objectId(newObj.getRoutingId()) + .action(Action.UPDATE) + .projectId(newObj.getProjectId()) + .description(description) + .build()); + return result; + } +} diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java index 83c38212a4b0692ddd12039b3b027f3708d85a13..3a95f8a4e329d27457a75d18fac600f6fe84a836 100644 --- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java +++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java @@ -1,10 +1,9 @@ package com.aolda.itda.config; -import com.aolda.itda.dto.auth.ProjectIdAndNameDTO; +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 com.fasterxml.jackson.core.JsonProcessingException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; @@ -14,7 +13,6 @@ import org.springframework.web.servlet.HandlerInterceptor; import java.util.List; import java.util.Map; -import java.util.stream.Collectors; @RequiredArgsConstructor @Component @@ -59,9 +57,10 @@ public class AuthInterceptor implements HandlerInterceptor { /* 프로젝트 리스트 조회 */ List<String> projects = authService.getProjectsWithUser(Map.of("id", userId, "token", token)) - .stream().map(ProjectIdAndNameDTO::getId) + .stream().map(IdAndNameDTO::getId) .toList(); request.setAttribute("projects", projects); + request.setAttribute("user", Map.of("id", userId, "token", token)); return true; } diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java index 71e134bebee94ab01c47d0a032e6026d9929b62b..e3893aeecd8b181adc3bc069fbb7724fef9c38ab 100644 --- a/src/main/java/com/aolda/itda/config/WebConfig.java +++ b/src/main/java/com/aolda/itda/config/WebConfig.java @@ -1,6 +1,9 @@ package com.aolda.itda.config; +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; +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.InterceptorRegistry; @@ -25,10 +28,15 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { - String[] excludeAuth = {"/error", "/api/auth/*", "/api/*" }; + String[] excludeAuth = {"/error", "/api/auth/*" }; registry.addInterceptor(authInterceptor) .addPathPatterns("/**") .excludePathPatterns(excludeAuth); } + + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager em) { + return new JPAQueryFactory(em); + } } 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/dto/PageResp.java b/src/main/java/com/aolda/itda/dto/PageResp.java index 49a2a6cdbacdee106e68588dc7b22cb43bb70fa7..7079600bbabe8b47e3e4c57f5267e8b287c8a473 100644 --- a/src/main/java/com/aolda/itda/dto/PageResp.java +++ b/src/main/java/com/aolda/itda/dto/PageResp.java @@ -15,7 +15,7 @@ import java.util.List; @JsonInclude(JsonInclude.Include.NON_NULL) public class PageResp<T> { private Integer totalPages; - private Integer totalElements; + private Long totalElements; private Integer size; private List<T> contents; private Boolean first; 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/LoginResponseDTO.java b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java index 33ba32fd358f5ab1fffecbe0965cc8921a88bbb2..a647b05835b9614cf21b1b8340a31b256fade9bc 100644 --- a/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java +++ b/src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java @@ -13,5 +13,5 @@ import java.util.List; @Builder public class LoginResponseDTO { private Boolean isAdmin; - private List<ProjectIdAndNameDTO> projects; + private List<IdAndNameDTO> projects; } diff --git a/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java b/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java deleted file mode 100644 index 1ca894d2bf9b9cd5f29244ac95e7c4950daec174..0000000000000000000000000000000000000000 --- a/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.aolda.itda.dto.auth; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -@Getter -@NoArgsConstructor -@AllArgsConstructor -@Builder -public class ProjectIdAndNameDTO { - - private String id; - private String name; -} 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..589545518431c85d093ca695b3cb97778fb9bac0 --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/log/LogDTO.java @@ -0,0 +1,38 @@ +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.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; + private String description; + 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/entity/BaseTimeEntity.java b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java index 4460eb7b0c8c1401e10dcc56a34a09e81beca59a..6cfa1fde46f85bae5cf6c3ae7b2e543002db0ad9 100644 --- a/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java +++ b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java @@ -1,5 +1,6 @@ package com.aolda.itda.entity; +import com.fasterxml.jackson.annotation.JsonFormat; import jakarta.persistence.Column; import jakarta.persistence.EntityListeners; import jakarta.persistence.MappedSuperclass; @@ -17,10 +18,12 @@ public abstract class BaseTimeEntity { @CreatedDate @Column(updatable = false) + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") private LocalDateTime createdAt; @LastModifiedDate @Column(name = "updated_at") + @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/forwarding/Forwarding.java b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java index b0c8d34715c26ce8334e6037ea97c65329761c72..86f46b767f85217fd224a55e4951ccafe08e2f4c 100644 --- a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java +++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java @@ -4,10 +4,7 @@ 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.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; @Entity @Table(name = "forwarding") @@ -15,6 +12,7 @@ import lombok.NoArgsConstructor; @Builder @NoArgsConstructor @AllArgsConstructor +@ToString public class Forwarding extends BaseTimeEntity { @Id @@ -40,6 +38,18 @@ public class Forwarding extends BaseTimeEntity { private String name; + public Forwarding(Forwarding forwarding) { + this.forwardingId = forwarding.getForwardingId(); + this.user = forwarding.getUser(); + 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) diff --git a/src/main/java/com/aolda/itda/entity/log/Log.java b/src/main/java/com/aolda/itda/entity/log/Log.java index fa78a6677f3cbb66322056a83dfd2a404eb5435c..373a836541823c018c770b131683760cd6a4b47a 100644 --- a/src/main/java/com/aolda/itda/entity/log/Log.java +++ b/src/main/java/com/aolda/itda/entity/log/Log.java @@ -1,5 +1,7 @@ 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.*; @@ -37,4 +39,16 @@ public class Log extends BaseTimeEntity { 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/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java index 6eea5fb80b24a23bab78c4c0ee5676feac05bae4..3e4b0be8af87fa6ad85462e6a86fb52d6c295b2d 100644 --- a/src/main/java/com/aolda/itda/exception/ErrorCode.java +++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java @@ -10,6 +10,7 @@ public enum ErrorCode { // User INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다"), + NOT_FOUND_USER(HttpStatus.BAD_REQUEST, "존재하지 않는 사용자입니다"), UNAUTHORIZED_USER(HttpStatus.BAD_REQUEST, "권한이 없는 사용자입니다"), // Token @@ -20,6 +21,9 @@ public enum ErrorCode { // Routing NOT_FOUND_ROUTING(HttpStatus.BAD_REQUEST, "라우팅 파일이 존재하지 않습니다"), + + // Routing + NOT_FOUND_LOG(HttpStatus.BAD_REQUEST, "로그가 존재하지 않습니다"), // Certificate NOT_FOUND_CERTIFICATE(HttpStatus.BAD_REQUEST, "SSL 인증서가 존재하지 않습니다"), 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..3ec962ba71fe611200dd83fa8fa2da04b0e38ef5 --- /dev/null +++ b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java @@ -0,0 +1,104 @@ +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)); + } + + /* 오브젝트 타입 조건 ( 기본 : ROUTING ) */ + if (type.equals("certificate")) { + builder.and(log.objectType.eq(ObjectType.CERTIFICATE)); + } + else if (type.equals("forwarding")) { + builder.and(log.objectType.eq(ObjectType.FORWARDING)); + } + else { + builder.and(log.objectType.eq(ObjectType.ROUTING)); + } + + /* 사용자 ID 조건 */ + if (username != null) { + builder.and(log.user.keystoneUsername.eq(username)); + } + + /* CUD 조건 */ + if (action.equals("create")) { + builder.and(log.action.eq(Action.CREATE)); + } else if (action.equals("update")) { + builder.and(log.action.eq(Action.UPDATE)); + } else if (action.equals("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/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 index cbc157f6b4b7f5cf9398c9095330fd4ab40c6184..be6d764295fe1cb3d913199f4da9de315fb6ec33 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -2,9 +2,11 @@ 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.ProjectIdAndNameDTO; +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; @@ -31,6 +33,7 @@ public class AuthService { 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 { @@ -44,6 +47,13 @@ public class AuthService { throw new CustomException(ErrorCode.INVALID_USER_INFO); } + + User entity = userRepository.findByKeystoneUsername(userId).orElse(null); + if (entity == null) { + userRepository.save(User.builder().keystoneId(validateTokenAndGetUserId(token)). + keystoneUsername(userId).build()); + } + response.addHeader("X-Subject-Token", systemToken != null ? systemToken : token); return LoginResponseDTO.builder() .isAdmin(systemToken != null) @@ -239,7 +249,7 @@ public class AuthService { } // 특정 사용자의 참여 프로젝트 반환 - public List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException { + 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) { @@ -257,12 +267,12 @@ public class AuthService { JsonNode node = objectMapper.readTree(res.getBody()); ArrayNode arrayNode = (ArrayNode) node.get("projects"); - List<ProjectIdAndNameDTO> lists = new ArrayList<>(); + List<IdAndNameDTO> lists = new ArrayList<>(); for (JsonNode assignment : arrayNode) { String projectId = assignment.path("id").asText(); String projectName = assignment.path("name").asText(); - lists.add(new ProjectIdAndNameDTO(projectId, projectName)); + lists.add(new IdAndNameDTO(projectId, projectName)); } return lists; } @@ -284,12 +294,12 @@ public class AuthService { } public void validateProjectAuth(List<String> projects, String projectId) { - if (!projects.contains(projectId)) { + if (projects != null && !projects.contains(projectId)) { throw new CustomException(ErrorCode.UNAUTHORIZED_USER); } } - private Boolean isAdmin(Map<String, String> user) throws JsonProcessingException { + 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")); diff --git a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java index 441090bb334842d0a0142511f87b7992f5919a9f..85c1ec3eb5450c7adae76f06f22724397f1e09f1 100644 --- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java +++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java @@ -1,7 +1,6 @@ package com.aolda.itda.service.forwarding; import com.aolda.itda.dto.PageResp; -import com.aolda.itda.dto.auth.ProjectIdAndNameDTO; import com.aolda.itda.dto.forwarding.ForwardingDTO; import com.aolda.itda.entity.forwarding.Forwarding; import com.aolda.itda.exception.CustomException; @@ -9,20 +8,14 @@ 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 com.fasterxml.jackson.databind.ObjectMapper; -import jakarta.servlet.http.HttpServletRequest; 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.http.HttpEntity; -import org.springframework.http.ResponseEntity; 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.RestClientException; import org.springframework.web.client.RestTemplate; import java.io.BufferedWriter; @@ -34,7 +27,6 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.List; -import java.util.Map; @Service @Transactional @@ -71,7 +63,7 @@ public class ForwardingService { } /* 포트포워딩 생성 */ - public void createForwarding(String projectId, ForwardingDTO dto) { + public ForwardingDTO createForwarding(String projectId, ForwardingDTO dto) { /* 입력 DTO 검증 */ validateDTO(dto); @@ -163,7 +155,7 @@ public class ForwardingService { } throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); } - + return forwarding.toForwardingDTO(); } /* 포트포워딩 정보 수정 */ 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/routing/RoutingService.java b/src/main/java/com/aolda/itda/service/routing/RoutingService.java index eb3777b6fc26fbbd7c822894b5fed5f640202634..7b30c9127796b6dda07d65207a0bc37abbdb9b92 100644 --- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java +++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java @@ -54,7 +54,6 @@ public class RoutingService { /* Routing 목록 조회 */ public PageResp<RoutingDTO> getRoutings(String projectId) { - // project id 확인 필요 return PageResp.<RoutingDTO>builder() .contents(routingRepository.findByProjectIdAndIsDeleted(projectId, false) .stream() @@ -63,7 +62,7 @@ public class RoutingService { } /* Routing 생성 */ - public void createRouting(String projectId, RoutingDTO dto) { + public RoutingDTO createRouting(String projectId, RoutingDTO dto) { /* 입력 DTO 검증 */ validateDTO(dto); @@ -157,6 +156,7 @@ public class RoutingService { throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); } + return routing.toRoutingDTO(); } /* Routing 수정 */ @@ -167,9 +167,6 @@ public class RoutingService { /* 프로젝트 권한 검증 */ authService.validateProjectAuth(projects, routing.getProjectId()); - /* 입력 DTO 검증 */ - validateDTO(dto); - /* 중복 검증 */ if (dto.getDomain() != null && routingRepository.existsByDomainAndIsDeleted(dto.getDomain(), false)) { throw new CustomException(ErrorCode.DUPLICATED_DOMAIN_NAME); @@ -182,7 +179,9 @@ public class RoutingService { /* 파일 수정 */ routing.edit(dto, certificate); - String content = routingTemplate.getRouting(routing.toRoutingDTO(), certificate == null ? null : certificate.formatDomain()); + 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()) {