From aa3f93d94913af4d1ecbab4aed2733da0bb9205d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Mon, 3 Mar 2025 21:22:07 +0900 Subject: [PATCH 01/41] =?UTF-8?q?Feat/auth=20:=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=ED=96=89=20=EB=B0=8F?= =?UTF-8?q?=20=EC=B0=B8=EC=97=AC=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20?= =?UTF-8?q?=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + .../java/com/aolda/itda/config/WebConfig.java | 19 +++ .../aolda/itda/controller/AuthController.java | 27 +++ .../aolda/itda/dto/auth/LoginRequestDTO.java | 11 ++ .../aolda/itda/dto/auth/LoginResponseDTO.java | 17 ++ .../itda/dto/auth/ProjectIdAndNameDTO.java | 16 ++ .../aolda/itda/dto/auth/ProjectRoleDTO.java | 17 ++ .../itda/exception/ApiExceptionHandler.java | 17 ++ .../aolda/itda/exception/CustomException.java | 19 +++ .../com/aolda/itda/exception/ErrorCode.java | 16 ++ .../aolda/itda/exception/ErrorResponse.java | 29 ++++ .../com/aolda/itda/service/AuthService.java | 159 ++++++++++++++++++ 12 files changed, 349 insertions(+) create mode 100644 src/main/java/com/aolda/itda/config/WebConfig.java create mode 100644 src/main/java/com/aolda/itda/controller/AuthController.java create mode 100644 src/main/java/com/aolda/itda/dto/auth/LoginRequestDTO.java create mode 100644 src/main/java/com/aolda/itda/dto/auth/LoginResponseDTO.java create mode 100644 src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java create mode 100644 src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java create mode 100644 src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java create mode 100644 src/main/java/com/aolda/itda/exception/CustomException.java create mode 100644 src/main/java/com/aolda/itda/exception/ErrorCode.java create mode 100644 src/main/java/com/aolda/itda/exception/ErrorResponse.java create mode 100644 src/main/java/com/aolda/itda/service/AuthService.java diff --git a/build.gradle b/build.gradle index 9305e70..5cef9f6 100644 --- a/build.gradle +++ b/build.gradle @@ -28,6 +28,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' 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' 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 0000000..9eb992b --- /dev/null +++ b/src/main/java/com/aolda/itda/config/WebConfig.java @@ -0,0 +1,19 @@ +package com.aolda.itda.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + @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") + ; + } +} 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 0000000..274ad8f --- /dev/null +++ b/src/main/java/com/aolda/itda/controller/AuthController.java @@ -0,0 +1,27 @@ +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.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@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)); + } +} 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 0000000..b91b3af --- /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 0000000..a5aa14a --- /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<ProjectIdAndNameDTO> lists; +} diff --git a/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java b/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java new file mode 100644 index 0000000..1ca894d --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java @@ -0,0 +1,16 @@ +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/auth/ProjectRoleDTO.java b/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java new file mode 100644 index 0000000..d3b9b8e --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.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 ProjectRoleDTO { + private String projectName; + private String roleName; +} 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 0000000..4d1f360 --- /dev/null +++ b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java @@ -0,0 +1,17 @@ +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()); + return ErrorResponse.fromException(e); + } +} 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 0000000..d110063 --- /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 0000000..6bcb643 --- /dev/null +++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java @@ -0,0 +1,16 @@ +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, "잘못된 회원 정보입니다"); + + private final HttpStatus status; + private final String message; +} 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 0000000..1bec060 --- /dev/null +++ b/src/main/java/com/aolda/itda/exception/ErrorResponse.java @@ -0,0 +1,29 @@ +package com.aolda.itda.exception; + +import lombok.Builder; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +@Getter +@Builder +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/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java new file mode 100644 index 0000000..5f071ee --- /dev/null +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -0,0 +1,159 @@ +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.ProjectRoleDTO; +import com.aolda.itda.exception.CustomException; +import com.aolda.itda.exception.ErrorCode; +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.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(); + + // 사용자 로그인 후 토큰 발행 및 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"); + + if (userId == null || token == null) { + throw new CustomException(ErrorCode.INVALID_USER_INFO); + } + + response.addHeader("X-Subject-Token", token); + return LoginResponseDTO.builder() + .isAdmin(false) + .lists(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 = restTemplate.postForEntity(url, requestEntity, Map.class); + + 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); + } + + // 특정 사용자의 프로젝트별 Role 반환 + private List<ProjectRoleDTO> getRolesWithProjects(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 + "/role_assignments?user.id=" + userId + "&effective&include_names=true"; + + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Auth-Token", getAdminToken()); + + 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"); + + List<ProjectRoleDTO> lists = new ArrayList<>(); + + for (JsonNode assignment : arrayNode) { + + String projectName = assignment.path("scope").path("project").path("name").asText(); + String roleName = assignment.path("role").path("name").asText(); + + ProjectRoleDTO projectRoleDTO = new ProjectRoleDTO(projectName, roleName); + lists.add(projectRoleDTO); + + } + + return lists; + } + + // 관리자용 토큰 발행 + public String getAdminToken() { + Map<String, String> user = getToken(adminId, adminPassword); + return user.get("token"); + } + + private List<ProjectIdAndNameDTO> 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", getAdminToken()); + + 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<ProjectIdAndNameDTO> 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)); + } + return lists; + } +} -- GitLab From 0f0003d97c25c5725945ac08e460e2fdb289f78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Fri, 28 Feb 2025 14:47:36 +0900 Subject: [PATCH 02/41] =?UTF-8?q?feat:=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EC=9A=A9=20=ED=95=B8=EB=93=A4=EB=9F=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 - 1 file changed, 1 deletion(-) diff --git a/build.gradle b/build.gradle index 5cef9f6..8b2abc8 100644 --- a/build.gradle +++ b/build.gradle @@ -29,7 +29,6 @@ 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' -- GitLab From e76498c5df30f39d4383b6361958ffaa770c9563 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Fri, 28 Feb 2025 22:27:45 +0900 Subject: [PATCH 03/41] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EB=B0=9C?= =?UTF-8?q?=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + src/main/java/com/aolda/itda/service/AuthService.java | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 8b2abc8..5cef9f6 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ 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' diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index 5f071ee..e64baf7 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -65,11 +65,9 @@ public class AuthService { " ],\n" + " \"password\": {\n" + " \"user\": {\n" + - " \"name\": \""+ id + "\",\n" + " \"domain\": {\n" + " \"name\": \"Default\"\n" + " },\n" + - " \"password\": \"" + password + "\"\n" + " }\n" + " }\n" + " }\n" + -- GitLab From b1ea5b7cb79a4b7aa5aee99328c94e347cb2eb06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sun, 2 Mar 2025 23:35:40 +0900 Subject: [PATCH 04/41] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EB=B0=9C?= =?UTF-8?q?=ED=96=89=EC=8B=9C=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8?= =?UTF-8?q?=EB=B3=84=20Role=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aolda/itda/service/AuthService.java | 34 ++----------------- 1 file changed, 3 insertions(+), 31 deletions(-) diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index e64baf7..cbd20a1 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -2,7 +2,6 @@ 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.ProjectRoleDTO; import com.aolda.itda.exception.CustomException; import com.aolda.itda.exception.ErrorCode; @@ -45,8 +44,7 @@ public class AuthService { response.addHeader("X-Subject-Token", token); return LoginResponseDTO.builder() - .isAdmin(false) - .lists(getProjectsWithUser(user)) + .lists(getRolesWithProjects(user)) .build(); } @@ -65,9 +63,11 @@ public class AuthService { " ],\n" + " \"password\": {\n" + " \"user\": {\n" + + " \"name\": \""+ id + "\",\n" + " \"domain\": {\n" + " \"name\": \"Default\"\n" + " },\n" + + " \"password\": \"" + password + "\"\n" + " }\n" + " }\n" + " }\n" + @@ -126,32 +126,4 @@ public class AuthService { Map<String, String> user = getToken(adminId, adminPassword); return user.get("token"); } - - private List<ProjectIdAndNameDTO> 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", getAdminToken()); - - 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<ProjectIdAndNameDTO> 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)); - } - return lists; - } } -- GitLab From 74f69b1c388c06a0ae7555717b9070fd61675cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Mon, 3 Mar 2025 21:02:38 +0900 Subject: [PATCH 05/41] =?UTF-8?q?feat:=20=ED=86=A0=ED=81=B0=20=EB=B0=9C?= =?UTF-8?q?=ED=96=89=EC=8B=9C=20=EC=B0=B8=EC=97=AC=20=ED=94=84=EB=A1=9C?= =?UTF-8?q?=EC=A0=9D=ED=8A=B8=EB=A7=8C=20=EB=B0=98=ED=99=98=ED=95=98?= =?UTF-8?q?=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aolda/itda/service/AuthService.java | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index cbd20a1..5f071ee 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -2,6 +2,7 @@ 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.ProjectRoleDTO; import com.aolda.itda.exception.CustomException; import com.aolda.itda.exception.ErrorCode; @@ -44,7 +45,8 @@ public class AuthService { response.addHeader("X-Subject-Token", token); return LoginResponseDTO.builder() - .lists(getRolesWithProjects(user)) + .isAdmin(false) + .lists(getProjectsWithUser(user)) .build(); } @@ -126,4 +128,32 @@ public class AuthService { Map<String, String> user = getToken(adminId, adminPassword); return user.get("token"); } + + private List<ProjectIdAndNameDTO> 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", getAdminToken()); + + 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<ProjectIdAndNameDTO> 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)); + } + return lists; + } } -- GitLab From e5489649279e1bf7751e426784d1d368280d24af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Mon, 3 Mar 2025 22:56:29 +0900 Subject: [PATCH 06/41] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90?= =?UTF-8?q?=EC=9D=98=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20Role=20api?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aolda/itda/controller/AuthController.java | 12 +++-- .../com/aolda/itda/exception/ErrorCode.java | 5 +- .../com/aolda/itda/service/AuthService.java | 47 +++++++++++++++---- 3 files changed, 51 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/aolda/itda/controller/AuthController.java b/src/main/java/com/aolda/itda/controller/AuthController.java index 274ad8f..79046d2 100644 --- a/src/main/java/com/aolda/itda/controller/AuthController.java +++ b/src/main/java/com/aolda/itda/controller/AuthController.java @@ -6,10 +6,7 @@ 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.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/auth") @@ -24,4 +21,11 @@ public class AuthController { 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/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java index 6bcb643..0916e71 100644 --- a/src/main/java/com/aolda/itda/exception/ErrorCode.java +++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java @@ -9,7 +9,10 @@ import org.springframework.http.HttpStatus; public enum ErrorCode { // User - INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다"); + INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다"), + + // Token + INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"); private final HttpStatus status; private final String message; diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index 5f071ee..6fea01e 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -15,6 +15,9 @@ 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.HttpStatusCodeException; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import java.util.*; @@ -88,8 +91,15 @@ public class AuthService { "token", token); } - // 특정 사용자의 프로젝트별 Role 반환 - private List<ProjectRoleDTO> getRolesWithProjects(Map<String, String> user) throws JsonProcessingException { + // 특정 사용자의 특정 프로젝트 내 최고 권한 반환 + public Map<String, String> getBestRoleWithinProject(String token, String projectId) throws JsonProcessingException { + return getBestRoleWithinProject(Map.of( + "id", validateTokenAndGetUserId(token), + "token", token), + projectId); + } + + private Map<String, String> getBestRoleWithinProject(Map<String, String> user, String projectId) throws JsonProcessingException { String userId = user.get("id"); String token = user.get("token"); @@ -97,7 +107,7 @@ public class AuthService { throw new CustomException(ErrorCode.INVALID_USER_INFO); } - String url = keystone + "/role_assignments?user.id=" + userId + "&effective&include_names=true"; + 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", getAdminToken()); @@ -108,19 +118,23 @@ public class AuthService { JsonNode node = objectMapper.readTree(res.getBody()); ArrayNode arrayNode = (ArrayNode) node.get("role_assignments"); - List<ProjectRoleDTO> lists = new ArrayList<>(); + String bestRole = "reader"; for (JsonNode assignment : arrayNode) { - String projectName = assignment.path("scope").path("project").path("name").asText(); String roleName = assignment.path("role").path("name").asText(); - ProjectRoleDTO projectRoleDTO = new ProjectRoleDTO(projectName, roleName); - lists.add(projectRoleDTO); + 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 lists; + return Map.of("role", bestRole); } // 관리자용 토큰 발행 @@ -129,6 +143,7 @@ public class AuthService { return user.get("token"); } + // 특정 사용자의 참여 프로젝트 반환 private List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException { String userId = user.get("id"); String token = user.get("token"); @@ -156,4 +171,20 @@ public class AuthService { } return lists; } + + private String validateTokenAndGetUserId(String token) throws JsonProcessingException { + String url = keystone + "/auth/tokens"; + HttpHeaders headers = new HttpHeaders(); + headers.set("X-Auth-Token", getAdminToken()); + 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 (HttpClientErrorException.NotFound e) { + throw new CustomException(ErrorCode.INVALID_TOKEN); + } + return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText(); + + } } -- GitLab From cf23a24328437198177e86a2ad91b737db238120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Mon, 3 Mar 2025 23:11:53 +0900 Subject: [PATCH 07/41] =?UTF-8?q?chore:=20=ED=95=84=EC=9A=94=EC=97=86?= =?UTF-8?q?=EB=8A=94=20dto=20=EB=B0=8F=20import=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aolda/itda/dto/auth/ProjectRoleDTO.java | 17 ----------------- .../com/aolda/itda/service/AuthService.java | 3 --- 2 files changed, 20 deletions(-) delete mode 100644 src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java diff --git a/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java b/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java deleted file mode 100644 index d3b9b8e..0000000 --- a/src/main/java/com/aolda/itda/dto/auth/ProjectRoleDTO.java +++ /dev/null @@ -1,17 +0,0 @@ -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 ProjectRoleDTO { - private String projectName; - private String roleName; -} diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index 6fea01e..88062f7 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -3,7 +3,6 @@ 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.ProjectRoleDTO; import com.aolda.itda.exception.CustomException; import com.aolda.itda.exception.ErrorCode; import com.fasterxml.jackson.core.JsonProcessingException; @@ -16,8 +15,6 @@ 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.HttpStatusCodeException; -import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; import java.util.*; -- GitLab From caac306fd9c3c19012d948b3c7f6be6256a3aae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Mon, 10 Mar 2025 16:29:35 +0900 Subject: [PATCH 08/41] =?UTF-8?q?feat:=20=EC=96=B4=EB=93=9C=EB=AF=BC=20?= =?UTF-8?q?=EA=B6=8C=ED=95=9C=20=ED=99=95=EC=9D=B8=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aolda/itda/dto/auth/LoginResponseDTO.java | 2 +- .../com/aolda/itda/service/AuthService.java | 83 +++++++++++++++++-- 2 files changed, 78 insertions(+), 7 deletions(-) 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 a5aa14a..33ba32f 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> lists; + private List<ProjectIdAndNameDTO> projects; } diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index 88062f7..80f7196 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -38,15 +38,16 @@ public class AuthService { 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); } - response.addHeader("X-Subject-Token", token); + response.addHeader("X-Subject-Token", systemToken != null ? systemToken : token); return LoginResponseDTO.builder() - .isAdmin(false) - .lists(getProjectsWithUser(user)) + .isAdmin(systemToken != null) + .projects(getProjectsWithUser(user)) .build(); } @@ -88,6 +89,50 @@ public class AuthService { "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 (RuntimeException 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; + } + // 특정 사용자의 특정 프로젝트 내 최고 권한 반환 public Map<String, String> getBestRoleWithinProject(String token, String projectId) throws JsonProcessingException { return getBestRoleWithinProject(Map.of( @@ -107,7 +152,7 @@ public class AuthService { 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", getAdminToken()); + headers.set("X-Auth-Token", token); HttpEntity<String> requestEntity = new HttpEntity<>(headers); ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); @@ -151,7 +196,7 @@ public class AuthService { String url = keystone + "/users/" + userId + "/projects"; HttpHeaders headers = new HttpHeaders(); - headers.set("X-Auth-Token", getAdminToken()); + headers.set("X-Auth-Token", token); HttpEntity<String> requestEntity = new HttpEntity<>(headers); ResponseEntity<String> res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); @@ -172,7 +217,7 @@ public class AuthService { private String validateTokenAndGetUserId(String token) throws JsonProcessingException { String url = keystone + "/auth/tokens"; HttpHeaders headers = new HttpHeaders(); - headers.set("X-Auth-Token", getAdminToken()); + headers.set("X-Auth-Token", token); headers.set("X-Subject-Token", token); HttpEntity<String> requestEntity = new HttpEntity<>(headers); ResponseEntity<String> res; @@ -184,4 +229,30 @@ public class AuthService { return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText(); } + + private 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 (RuntimeException e) { + e.printStackTrace(); + System.out.println("runtime"); + 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(); + System.out.println("role: " + role); + if (system_all.equals("true") && role.equals("admin")) { + System.out.println(system_all); + return true; + } + System.out.println("hi"); + return false; + + } } -- GitLab From 7320a6e16e89547ad69925d35196b7d0d57a43b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Mon, 10 Mar 2025 17:31:35 +0900 Subject: [PATCH 09/41] =?UTF-8?q?fix:=20project=20token=EC=9C=BC=EB=A1=9C?= =?UTF-8?q?=20project=20role=20=ED=99=95=EC=9D=B8=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aolda/itda/service/AuthService.java | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index 80f7196..1dea9a2 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -15,6 +15,7 @@ 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.RestClientException; import org.springframework.web.client.RestTemplate; import java.util.*; @@ -133,11 +134,59 @@ public class AuthService { 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 (RuntimeException e) { + e.printStackTrace(); + 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", token), + "token", getProjectToken(token, projectId)), projectId); } @@ -224,6 +273,7 @@ public class AuthService { try { res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); } catch (HttpClientErrorException.NotFound e) { + System.out.println("validate"); throw new CustomException(ErrorCode.INVALID_TOKEN); } return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText(); -- GitLab From f06d3bce2771390e445c47b0d77f03baaa163653 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sun, 9 Mar 2025 19:20:50 +0900 Subject: [PATCH 10/41] =?UTF-8?q?feat:=20=EB=9D=BC=EC=9A=B0=ED=8C=85,=20?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=20=EB=93=B1=20=EC=97=94=ED=8B=B0=ED=8B=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aolda/itda/entity/BaseTimeEntity.java | 26 +++++++++++ .../itda/entity/certificate/Certificate.java | 46 +++++++++++++++++++ .../itda/entity/certificate/Challenge.java | 5 ++ .../itda/entity/forwarding/Forwarding.java | 39 ++++++++++++++++ .../com/aolda/itda/entity/log/Action.java | 5 ++ .../java/com/aolda/itda/entity/log/Log.java | 40 ++++++++++++++++ .../com/aolda/itda/entity/log/ObjectType.java | 5 ++ .../aolda/itda/entity/routing/Routing.java | 42 +++++++++++++++++ .../java/com/aolda/itda/entity/user/User.java | 24 ++++++++++ 9 files changed, 232 insertions(+) create mode 100644 src/main/java/com/aolda/itda/entity/BaseTimeEntity.java create mode 100644 src/main/java/com/aolda/itda/entity/certificate/Certificate.java create mode 100644 src/main/java/com/aolda/itda/entity/certificate/Challenge.java create mode 100644 src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java create mode 100644 src/main/java/com/aolda/itda/entity/log/Action.java create mode 100644 src/main/java/com/aolda/itda/entity/log/Log.java create mode 100644 src/main/java/com/aolda/itda/entity/log/ObjectType.java create mode 100644 src/main/java/com/aolda/itda/entity/routing/Routing.java create mode 100644 src/main/java/com/aolda/itda/entity/user/User.java 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 0000000..4460eb7 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java @@ -0,0 +1,26 @@ +package com.aolda.itda.entity; + +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) + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + 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 0000000..76253a1 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -0,0 +1,46 @@ +package com.aolda.itda.entity.certificate; + +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 java.time.LocalDateTime; + +@Entity +@Table(name = "routing") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Certificate extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long certificateId; + + @OneToOne + @JoinColumn(nullable = false, name = "user_id") + private User user; + + private String projectId; + + private String domain; + + private String email; + + private LocalDateTime expiredAt; + + @Enumerated(EnumType.STRING) + private Challenge challenge; + + private Boolean isDeleted; + + private String metadata; + + +} 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 0000000..14713bf --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java @@ -0,0 +1,5 @@ +package com.aolda.itda.entity.certificate; + +public enum Challenge { + HTTP, DNS_CLOUDFLARE +} 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 0000000..0288af8 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java @@ -0,0 +1,39 @@ +package com.aolda.itda.entity.forwarding; + +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 = "forwarding") +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Forwarding extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(nullable = false) + private Long forwardingId; + + @OneToOne + @JoinColumn(name = "user_id") + private User user; + + private String projectId; + + private String serverIp; + + private String serverPort; + + private String instanceIp; + + private String instancePort; + + private Boolean isDeleted; +} 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 0000000..a620f87 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/log/Action.java @@ -0,0 +1,5 @@ +package com.aolda.itda.entity.log; + +public enum Action { + CREATE, UPDATE, DELETE +} 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 0000000..fd8f460 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/log/Log.java @@ -0,0 +1,40 @@ +package com.aolda.itda.entity.log; + +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; + + @OneToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + private String projectId; + + @Enumerated(EnumType.STRING) + private ObjectType objectType; + + private Long objectId; + + @Enumerated(EnumType.STRING) + private Action action; + + private String metadata; + +} 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 0000000..5310315 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/log/ObjectType.java @@ -0,0 +1,5 @@ +package com.aolda.itda.entity.log; + +public enum ObjectType { + ROUTING, CERTIFICATE, FORWARDING +} 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 0000000..2eb9bd1 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java @@ -0,0 +1,42 @@ +package com.aolda.itda.entity.routing; + +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; + + @OneToOne + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @OneToOne + @JoinColumn(name = "certificate_id") + private Certificate certificate; + + private String projectId; + + private String domain; + + private String instanceIp; + + private Boolean isDeleted; + + +} 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 0000000..df006b2 --- /dev/null +++ b/src/main/java/com/aolda/itda/entity/user/User.java @@ -0,0 +1,24 @@ +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; +} -- GitLab From e92e5bd4765a609fe9ac63b6d8d59f72b5e7ddb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Mon, 10 Mar 2025 15:35:31 +0900 Subject: [PATCH 11/41] =?UTF-8?q?feat:=20=ED=8F=AC=ED=8A=B8=ED=8F=AC?= =?UTF-8?q?=EC=9B=8C=EB=94=A9,=20=EC=98=B5=EC=85=98=20=ED=85=9C=ED=94=8C?= =?UTF-8?q?=EB=A6=BF=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../itda/template/ForwardingTemplate.java | 13 ++++++++++ .../aolda/itda/template/OptionTemplate.java | 26 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/main/java/com/aolda/itda/template/ForwardingTemplate.java create mode 100644 src/main/java/com/aolda/itda/template/OptionTemplate.java 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 0000000..8031bff --- /dev/null +++ b/src/main/java/com/aolda/itda/template/ForwardingTemplate.java @@ -0,0 +1,13 @@ +package com.aolda.itda.template; + +import org.springframework.stereotype.Component; + +@Component +public class ForwardingTemplate { + + public String getPortForwardingWithTCP(String instanceIp, String serverPort) { + return "\nlisten " + serverPort + "; \n" + + "listen [::]:" + serverPort + "; \n" + + "proxy_pass " + instanceIp + ";\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 0000000..3e1e92c --- /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(Long certificateId) { + return "\nconf.d/include/letsencrypt-acme-challenge.conf;\n" + + "include conf.d/include/ssl-ciphers.conf;\n" + + "ssl_certificate /etc/letsencrypt/live/npm-" + certificateId + "/fullchain.pem;\n" + + "ssl_certificate_key /etc/letsencrypt/live/npm-" + certificateId + "/privkey.pem;\n"; + } + + public String getAssetCaching() { + return "include conf.d/include/assets.conf;\n"; + } + + public String getBlockExploits() { + return "include conf.d/include/block-exploits.conf;\n"; + } + + public String getForceSSL() { + return "include conf.d/include/force-ssl.conf;\n"; + } +} -- GitLab From b96e7708b01a98ca6ae80dfa905afe7014728d70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Fri, 14 Mar 2025 22:16:11 +0900 Subject: [PATCH 12/41] =?UTF-8?q?=ED=8F=AC=ED=8A=B8=ED=8F=AC=EC=9B=8C?= =?UTF-8?q?=EB=94=A9=20CRUD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 1 + .../aolda/itda/config/AuthInterceptor.java | 37 ++++ .../java/com/aolda/itda/config/WebConfig.java | 15 ++ .../forwarding/ForwardingController.java | 46 +++++ .../java/com/aolda/itda/dto/PageResp.java | 27 +++ .../itda/dto/forwarding/ForwardingDTO.java | 45 +++++ .../itda/entity/certificate/Certificate.java | 4 +- .../itda/entity/forwarding/Forwarding.java | 32 ++++ .../aolda/itda/entity/routing/Routing.java | 3 +- .../com/aolda/itda/exception/ErrorCode.java | 9 +- .../forwarding/ForwardingRepository.java | 14 ++ .../com/aolda/itda/service/AuthService.java | 17 +- .../service/forwarding/ForwardingService.java | 158 ++++++++++++++++++ .../itda/template/ForwardingTemplate.java | 11 +- 14 files changed, 402 insertions(+), 17 deletions(-) create mode 100644 src/main/java/com/aolda/itda/config/AuthInterceptor.java create mode 100644 src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java create mode 100644 src/main/java/com/aolda/itda/dto/PageResp.java create mode 100644 src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java create mode 100644 src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java create mode 100644 src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java diff --git a/build.gradle b/build.gradle index 5cef9f6..b88f454 100644 --- a/build.gradle +++ b/build.gradle @@ -26,6 +26,7 @@ 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' diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java new file mode 100644 index 0000000..63f2320 --- /dev/null +++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java @@ -0,0 +1,37 @@ +package com.aolda.itda.config; + +import com.aolda.itda.exception.CustomException; +import com.aolda.itda.exception.ErrorCode; +import com.aolda.itda.service.AuthService; +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.servlet.HandlerInterceptor; + +@RequiredArgsConstructor +@Component +@Slf4j +public class AuthInterceptor implements HandlerInterceptor { + + private final AuthService authService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + String token = request.getHeader("X-Subject-Token"); + if (token == null || token.isEmpty()) { + throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI()); + } + // 어드민과 일반 유저 구분 필요 + try { + if (authService.validateTokenAndGetUserId(token) != null) { + return true; + } + } catch (Exception e) { + log.error("Token validation failed for URI {}: {}", request.getRequestURI(), e.getMessage(), e); + throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI()); + } + throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI()); + } +} diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java index 9eb992b..fd00807 100644 --- a/src/main/java/com/aolda/itda/config/WebConfig.java +++ b/src/main/java/com/aolda/itda/config/WebConfig.java @@ -1,11 +1,17 @@ package com.aolda.itda.config; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration +@RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { + + private final AuthInterceptor authInterceptor; + @Override public void addCorsMappings(CorsRegistry registry) { // 스프링단에서 cors 설정 registry.addMapping("/**") @@ -16,4 +22,13 @@ public class WebConfig implements WebMvcConfigurer { .exposedHeaders("Authorization", "X-Refresh-Token", "Access-Control-Allow-Origin") ; } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + String[] excludeAuth = {"/error", "/api/auth/*" }; + + registry.addInterceptor(authInterceptor) + .addPathPatterns("/**") + .excludePathPatterns(excludeAuth); + } } 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 0000000..19786bd --- /dev/null +++ b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java @@ -0,0 +1,46 @@ +package com.aolda.itda.controller.forwarding; + +import com.aolda.itda.dto.forwarding.ForwardingDTO; +import com.aolda.itda.service.forwarding.ForwardingService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@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) { + return ResponseEntity.ok(forwardingService.getForwarding(forwardingId)); + } + + @GetMapping("/forwardings") + public ResponseEntity<Object> lists(@RequestParam String projectId) { + return ResponseEntity.ok(forwardingService.getForwardings(projectId)); + } + + @PatchMapping("/forwarding") + public ResponseEntity<Object> edit(@RequestParam Long forwardingId, + @RequestBody ForwardingDTO dto) { + forwardingService.editForwarding(forwardingId, dto); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/forwarding") + public ResponseEntity<Object> delete(@RequestParam Long forwardingId) { + forwardingService.deleteForwarding(forwardingId); + 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 0000000..49a2a6c --- /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 Integer 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/forwarding/ForwardingDTO.java b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java new file mode 100644 index 0000000..3302d0a --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java @@ -0,0 +1,45 @@ +package com.aolda.itda.dto.forwarding; + +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; + private LocalDateTime createdAt; + 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 index 76253a1..5ab56a9 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -11,7 +11,7 @@ import lombok.NoArgsConstructor; import java.time.LocalDateTime; @Entity -@Table(name = "routing") +@Table(name = "certificate") @Getter @Builder @NoArgsConstructor @@ -40,7 +40,7 @@ public class Certificate extends BaseTimeEntity { private Boolean isDeleted; - private String metadata; + private String description; } 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 0288af8..b0c8d34 100644 --- a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java +++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java @@ -1,5 +1,6 @@ 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.*; @@ -36,4 +37,35 @@ public class Forwarding extends BaseTimeEntity { private String instancePort; private Boolean isDeleted; + + private String name; + + 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/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java index 2eb9bd1..0c6b2aa 100644 --- a/src/main/java/com/aolda/itda/entity/routing/Routing.java +++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java @@ -27,7 +27,7 @@ public class Routing extends BaseTimeEntity { private User user; @OneToOne - @JoinColumn(name = "certificate_id") + @JoinColumn(name = "certificate_id", nullable = false) private Certificate certificate; private String projectId; @@ -38,5 +38,6 @@ public class Routing extends BaseTimeEntity { private Boolean isDeleted; + private String description; } diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java index 0916e71..c36e7f4 100644 --- a/src/main/java/com/aolda/itda/exception/ErrorCode.java +++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java @@ -12,7 +12,14 @@ public enum ErrorCode { INVALID_USER_INFO(HttpStatus.BAD_REQUEST, "잘못된 회원 정보입니다"), // Token - INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"); + INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"), + + //Forwarding + FAIL_CREATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 생성하지 못했습니다"), + NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"), + INVALID_CONF_INPUT(HttpStatus.BAD_REQUEST, "잘못된 입력이 존재합니다"), + DUPLICATED_INSTANCE_INFO(HttpStatus.BAD_REQUEST, "중복된 인스턴스 IP와 포트입니다"), + DUPLICATED_SERVER_PORT(HttpStatus.BAD_REQUEST, "중복된 서버 포트입니다"); private final HttpStatus status; private final String message; 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 0000000..c461762 --- /dev/null +++ b/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java @@ -0,0 +1,14 @@ +package com.aolda.itda.repository.forwarding; + +import com.aolda.itda.entity.forwarding.Forwarding; +import org.springframework.data.jpa.repository.JpaRepository; + +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); +} diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index 1dea9a2..cc523fa 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -15,7 +15,6 @@ 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.RestClientException; import org.springframework.web.client.RestTemplate; import java.util.*; @@ -79,8 +78,13 @@ public class AuthService { "}"; HttpEntity<String> requestEntity = new HttpEntity<>(requestBody, headers); - ResponseEntity<Map> res = restTemplate.postForEntity(url, requestEntity, Map.class); - + ResponseEntity<Map> res; + try { + res = restTemplate.postForEntity(url, requestEntity, Map.class); + } catch (Exception e) { + e.printStackTrace(); + 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"); @@ -263,7 +267,7 @@ public class AuthService { return lists; } - private String validateTokenAndGetUserId(String token) throws JsonProcessingException { + public String validateTokenAndGetUserId(String token) throws JsonProcessingException { String url = keystone + "/auth/tokens"; HttpHeaders headers = new HttpHeaders(); headers.set("X-Auth-Token", token); @@ -273,7 +277,6 @@ public class AuthService { try { res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); } catch (HttpClientErrorException.NotFound e) { - System.out.println("validate"); throw new CustomException(ErrorCode.INVALID_TOKEN); } return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText(); @@ -290,18 +293,14 @@ public class AuthService { res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); } catch (RuntimeException e) { e.printStackTrace(); - System.out.println("runtime"); 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(); - System.out.println("role: " + role); if (system_all.equals("true") && role.equals("admin")) { - System.out.println(system_all); return true; } - System.out.println("hi"); return false; } 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 0000000..22a1aee --- /dev/null +++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java @@ -0,0 +1,158 @@ +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.template.ForwardingTemplate; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +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; + +@Service +@Transactional +@RequiredArgsConstructor +public class ForwardingService { + + @Value("${spring.server.base-ip}") + private String serverBaseIp; + private final ForwardingTemplate forwardingTemplate; + private final ForwardingRepository forwardingRepository; + + /* 포트포워딩 정보 조회 */ + public ForwardingDTO getForwarding(Long forwardingId) { + Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + return forwarding.toForwardingDTO(); + } + + /* 포트포워딩 목록 조회 */ + public PageResp<ForwardingDTO> getForwardings(String projectId) { + + return PageResp.<ForwardingDTO>builder() + .contents(forwardingRepository.findByProjectIdAndIsDeleted(projectId, false) + .stream() + .map(Forwarding::toForwardingDTO) + .toList()).build(); + } + + /* 포트포워딩 생성 */ + public void 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) { + e.printStackTrace(); + throw new CustomException(ErrorCode.FAIL_CREATE_CONF); + } + + /* conf 파일 작성 및 예외 처리 */ + try { + BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); // 예외처리 필요 + bw.write(content); + bw.flush(); + bw.close(); + } catch (Exception e) { + e.printStackTrace(); + if (file.exists()) { + file.delete(); + } + throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다"); + } + + + } + + /* 포트포워딩 정보 수정 */ + public void editForwarding(Long forwardingId, ForwardingDTO dto) { + Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + + /* 중복 검증 */ + 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.edit(dto); + forwardingRepository.save(forwarding); + } + + /* 포트포워딩 삭제 (소프트) */ + public void deleteForwarding(Long forwardingId) { + Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + + /* 파일 삭제 */ + String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf"; + File file = new File(confPath); + if (!file.delete()) { + throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 삭제할 수 없습니다"); + } + + /* 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/template/ForwardingTemplate.java b/src/main/java/com/aolda/itda/template/ForwardingTemplate.java index 8031bff..6d8bf2c 100644 --- a/src/main/java/com/aolda/itda/template/ForwardingTemplate.java +++ b/src/main/java/com/aolda/itda/template/ForwardingTemplate.java @@ -5,9 +5,12 @@ import org.springframework.stereotype.Component; @Component public class ForwardingTemplate { - public String getPortForwardingWithTCP(String instanceIp, String serverPort) { - return "\nlisten " + serverPort + "; \n" + - "listen [::]:" + serverPort + "; \n" + - "proxy_pass " + instanceIp + ";\n"; + 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"; } } -- GitLab From e6f1aafb3915c7623458f0e9d43eeca3908ce317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Fri, 14 Mar 2025 23:27:46 +0900 Subject: [PATCH 13/41] =?UTF-8?q?fix:=20=ED=8F=AC=ED=8A=B8=ED=8F=AC?= =?UTF-8?q?=EC=9B=8C=EB=94=A9=20=ED=8C=8C=EC=9D=BC=EC=9D=84=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=ED=95=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8?= =?UTF-8?q?=EC=A0=9C,=20=EC=83=9D=EC=84=B1=EC=9D=BC/=EC=88=98=EC=A0=95?= =?UTF-8?q?=EC=9D=BC=20=ED=95=84=EB=93=9C=EA=B0=80=20=EC=A0=9C=EB=8C=80?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=81=EC=9A=A9=EB=90=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/aolda/itda/ItdaApplication.java | 2 ++ .../com/aolda/itda/exception/ErrorCode.java | 1 + .../service/forwarding/ForwardingService.java | 33 ++++++++++++++++--- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/aolda/itda/ItdaApplication.java b/src/main/java/com/aolda/itda/ItdaApplication.java index c7f4ef7..674e657 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/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java index c36e7f4..2fc313b 100644 --- a/src/main/java/com/aolda/itda/exception/ErrorCode.java +++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java @@ -16,6 +16,7 @@ public enum ErrorCode { //Forwarding FAIL_CREATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 생성하지 못했습니다"), + FAIL_UPDATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 수정하지 못했습니다"), NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"), INVALID_CONF_INPUT(HttpStatus.BAD_REQUEST, "잘못된 입력이 존재합니다"), DUPLICATED_INSTANCE_INFO(HttpStatus.BAD_REQUEST, "중복된 인스턴스 IP와 포트입니다"), 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 22a1aee..e47001d 100644 --- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java +++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java @@ -115,17 +115,42 @@ public class ForwardingService { Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false) .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + forwarding.edit(dto); + /* 중복 검증 */ - if (forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(dto.getInstanceIp(), dto.getInstancePort(), false)) { + if (!(dto.getInstanceIp() == null && dto.getInstancePort() == null) && + forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(forwarding.getInstanceIp() + , forwarding.getInstancePort() + , false)) { throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO); } - if (forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) { + if (dto.getServerPort() != null && forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) { throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT); } - /* 정보 수정 */ - 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 파일이 존재하지 않아 수정할 수 없습니다"); + } + + try { + BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); // 예외처리 필요 + bw.write(content); + bw.flush(); + bw.close(); + } catch (Exception e) { + e.printStackTrace(); + throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "포트포워딩 Conf 파일을 수정하지 못했습니다"); + } + + /* DB 정보 수정 */ forwardingRepository.save(forwarding); } -- GitLab From 5b1a3a59c2ef7436055ea4f46eb1ad14f3c034c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sat, 15 Mar 2025 11:31:04 +0900 Subject: [PATCH 14/41] =?UTF-8?q?feat:=20Dockerfile=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..16f6160 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,10 @@ +FROM gradle:jdk21 AS build +WORKDIR /home/gradle/project +COPY --chown=gradle:gradle . . +RUN gradle clean bootJar --no-daemon + +FROM openjdk:21-jdk-slim +WORKDIR /app +COPY --from=build /home/gradle/project/build/libs/*.jar app.jar +EXPOSE 8080 +ENTRYPOINT ["java", "-jar", "app.jar"] -- GitLab From c0d0a410f493f4bffe0c7d6cb4ca33e9f75a3258 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sat, 15 Mar 2025 12:28:51 +0900 Subject: [PATCH 15/41] =?UTF-8?q?fix:=20conf=20=EC=88=98=EC=A0=95=EC=9D=B4?= =?UTF-8?q?=20=EB=8D=AE=EC=96=B4=EC=93=B0=EA=B8=B0=20=EB=90=98=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aolda/itda/service/forwarding/ForwardingService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e47001d..f1d2e46 100644 --- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java +++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java @@ -95,7 +95,7 @@ public class ForwardingService { /* conf 파일 작성 및 예외 처리 */ try { - BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); // 예외처리 필요 + BufferedWriter bw = new BufferedWriter(new FileWriter(file, false)); // 예외처리 필요 bw.write(content); bw.flush(); bw.close(); -- GitLab From f78d7851ea18441b994dddc97b3f4acd31546865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sat, 15 Mar 2025 16:26:46 +0900 Subject: [PATCH 16/41] =?UTF-8?q?feat:=20Nginx=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8,=20=EB=A6=AC=EB=A1=9C=EB=93=9C=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80,=20conf=20=ED=8C=8C=EC=9D=BC=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EB=A1=A4=EB=B0=B1=20=EB=A1=9C=EC=A7=81=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aolda/itda/exception/ErrorCode.java | 11 +- .../service/forwarding/ForwardingService.java | 163 +++++++++++++++++- 2 files changed, 166 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java index 2fc313b..443f3a1 100644 --- a/src/main/java/com/aolda/itda/exception/ErrorCode.java +++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java @@ -14,13 +14,20 @@ public enum ErrorCode { // Token INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"), - //Forwarding + // Forwarding FAIL_CREATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 생성하지 못했습니다"), FAIL_UPDATE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 수정하지 못했습니다"), NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"), INVALID_CONF_INPUT(HttpStatus.BAD_REQUEST, "잘못된 입력이 존재합니다"), DUPLICATED_INSTANCE_INFO(HttpStatus.BAD_REQUEST, "중복된 인스턴스 IP와 포트입니다"), - DUPLICATED_SERVER_PORT(HttpStatus.BAD_REQUEST, "중복된 서버 포트입니다"); + DUPLICATED_SERVER_PORT(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, "롤백 실패"); private final HttpStatus status; private final String message; 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 f1d2e46..4bfbce4 100644 --- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java +++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java @@ -7,12 +7,20 @@ import com.aolda.itda.exception.CustomException; import com.aolda.itda.exception.ErrorCode; import com.aolda.itda.repository.forwarding.ForwardingRepository; import com.aolda.itda.template.ForwardingTemplate; +import com.fasterxml.jackson.databind.ObjectMapper; 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; import java.io.File; @@ -21,16 +29,20 @@ 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.Map; @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 RestTemplate restTemplate = new RestTemplate(); /* 포트포워딩 정보 조회 */ public ForwardingDTO getForwarding(Long forwardingId) { @@ -101,12 +113,47 @@ public class ForwardingService { bw.close(); } catch (Exception e) { e.printStackTrace(); - if (file.exists()) { - file.delete(); + 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) { + log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); + 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) { + log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); + if (file.delete()) { + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); + } } @@ -140,8 +187,13 @@ public class ForwardingService { throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "Conf 파일이 존재하지 않아 수정할 수 없습니다"); } + Path backup; try { - BufferedWriter bw = new BufferedWriter(new FileWriter(file, true)); // 예외처리 필요 + 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(); @@ -150,6 +202,59 @@ public class ForwardingService { 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 (HttpServerErrorException.InternalServerError e) { + log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); + 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); + } catch (RuntimeException e) { + log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); + 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 (HttpServerErrorException.InternalServerError e) { + log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); + 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); + } catch (RuntimeException e) { + log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); + 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); } @@ -161,9 +266,55 @@ public class ForwardingService { /* 파일 삭제 */ String confPath = "/data/nginx/stream/" + forwarding.getForwardingId() + ".conf"; - File file = new File(confPath); - if (!file.delete()) { - throw new CustomException(ErrorCode.NOT_FOUND_FORWARDING, "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 (HttpServerErrorException.InternalServerError e) { + log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); + 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); + } catch (Exception e) { + log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); + 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 (HttpServerErrorException.InternalServerError e) { + log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); + 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); + } catch (Exception e) { + log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); + 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 */ -- GitLab From 82fd3ae9115c9400f203c7be34d5cbb761823b64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sat, 15 Mar 2025 19:35:32 +0900 Subject: [PATCH 17/41] =?UTF-8?q?fix:=20conf=20=EC=A4=91=EB=B3=B5=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=EC=9D=B4=20=EC=95=88=EB=90=98=EB=8D=98=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/forwarding/ForwardingService.java | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) 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 4bfbce4..9ec9343 100644 --- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java +++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java @@ -162,21 +162,31 @@ public class ForwardingService { Forwarding forwarding = forwardingRepository.findByForwardingIdAndIsDeleted(forwardingId, false) .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); - forwarding.edit(dto); /* 중복 검증 */ + if (dto.getServerPort() != null && forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) { + System.out.println(dto.getServerPort()); + System.out.println(forwarding.getServerPort()); + forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false); + throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT); + } + if (!(dto.getInstanceIp() == null && dto.getInstancePort() == null) && - forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted(forwarding.getInstanceIp() - , forwarding.getInstancePort() + forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted( + dto.getInstanceIp() == null ? forwarding.getInstanceIp() : dto.getInstanceIp() + , dto.getInstancePort() == null ? forwarding.getInstancePort() : dto.getInstancePort() , false)) { + System.out.println(dto.getInstanceIp()); + System.out.println(forwarding.getInstanceIp()); + System.out.println(forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted( + dto.getInstanceIp() == null ? forwarding.getInstanceIp() : dto.getInstanceIp() + , dto.getInstancePort() == null ? forwarding.getInstancePort() : dto.getInstancePort() + , false)); throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO); } - if (dto.getServerPort() != null && forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) { - throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT); - } - /* 파일 수정 */ + forwarding.edit(dto); String content = forwardingTemplate.getPortForwardingWithTCP(forwarding.getServerPort(), forwarding.getInstanceIp(), forwarding.getInstancePort(), -- GitLab From 85d12a5613f1c1402a1d714c9b251d74b1163658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Mon, 17 Mar 2025 17:37:46 +0900 Subject: [PATCH 18/41] =?UTF-8?q?feat:=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20CRU?= =?UTF-8?q?D=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/aolda/itda/config/WebConfig.java | 2 +- .../forwarding/ForwardingController.java | 1 + .../controller/routing/RoutingController.java | 39 +++ .../aolda/itda/dto/routing/RoutingDTO.java | 37 ++ .../itda/entity/certificate/Certificate.java | 4 +- .../java/com/aolda/itda/entity/log/Log.java | 2 +- .../aolda/itda/entity/routing/Routing.java | 34 +- .../com/aolda/itda/exception/ErrorCode.java | 11 +- .../certificate/CertificateRepository.java | 7 + .../repository/routing/RoutingRepository.java | 14 + .../itda/service/routing/RoutingService.java | 325 ++++++++++++++++++ .../aolda/itda/template/OptionTemplate.java | 12 +- .../aolda/itda/template/RoutingTemplate.java | 44 +++ 13 files changed, 521 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/aolda/itda/controller/routing/RoutingController.java create mode 100644 src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java create mode 100644 src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java create mode 100644 src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java create mode 100644 src/main/java/com/aolda/itda/service/routing/RoutingService.java create mode 100644 src/main/java/com/aolda/itda/template/RoutingTemplate.java diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java index fd00807..71e134b 100644 --- a/src/main/java/com/aolda/itda/config/WebConfig.java +++ b/src/main/java/com/aolda/itda/config/WebConfig.java @@ -25,7 +25,7 @@ public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { - String[] excludeAuth = {"/error", "/api/auth/*" }; + String[] excludeAuth = {"/error", "/api/auth/*", "/api/*" }; registry.addInterceptor(authInterceptor) .addPathPatterns("/**") diff --git a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java index 19786bd..a6b67ec 100644 --- a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java +++ b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java @@ -2,6 +2,7 @@ package com.aolda.itda.controller.forwarding; import com.aolda.itda.dto.forwarding.ForwardingDTO; import com.aolda.itda.service.forwarding.ForwardingService; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; 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 0000000..8943571 --- /dev/null +++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java @@ -0,0 +1,39 @@ +package com.aolda.itda.controller.routing; + +import com.aolda.itda.dto.routing.RoutingDTO; +import com.aolda.itda.service.routing.RoutingService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api") +@RequiredArgsConstructor +public class RoutingController { + + private final RoutingService routingService; + + @GetMapping("/routing") + public ResponseEntity<Object> view(@RequestParam Long routingId) { + return ResponseEntity.ok(routingService.getRouting(routingId)); + } + + @GetMapping("/routings") + public ResponseEntity<Object> lists(@RequestParam String projectId) { + return ResponseEntity.ok(routingService.getRoutings(projectId)); + } + + @PatchMapping("/routing") + public ResponseEntity<Object> edit(@RequestParam Long routingId, + @RequestBody RoutingDTO dto) { + routingService.editRouting(routingId, dto); + return ResponseEntity.ok().build(); + } + + @DeleteMapping("/routing") + public ResponseEntity<Object> delete(@RequestParam Long routingId) { + routingService.deleteRouting(routingId); + return ResponseEntity.ok().build(); + } + +} 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 0000000..e95ab70 --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java @@ -0,0 +1,37 @@ +package com.aolda.itda.dto.routing; + +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; + @NotBlank + private Long certificateId; + + private LocalDateTime createdAt; + + private LocalDateTime updatedAt; + + @NotNull + private Boolean caching; +} diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java index 5ab56a9..188f05e 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -42,5 +42,7 @@ public class Certificate extends BaseTimeEntity { private String description; - + public String formatDomain() { + return domain == null ? null : domain.replace("*", "_"); + } } 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 fd8f460..fa78a66 100644 --- a/src/main/java/com/aolda/itda/entity/log/Log.java +++ b/src/main/java/com/aolda/itda/entity/log/Log.java @@ -35,6 +35,6 @@ public class Log extends BaseTimeEntity { @Enumerated(EnumType.STRING) private Action action; - private String metadata; + private String description; } diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java index 0c6b2aa..be03dcd 100644 --- a/src/main/java/com/aolda/itda/entity/routing/Routing.java +++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java @@ -1,5 +1,7 @@ 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; @@ -36,8 +38,38 @@ public class Routing extends BaseTimeEntity { private String instanceIp; + private String instancePort; + private Boolean isDeleted; - private String description; + private Boolean caching; + + 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/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java index 443f3a1..321bdad 100644 --- a/src/main/java/com/aolda/itda/exception/ErrorCode.java +++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java @@ -15,12 +15,21 @@ public enum ErrorCode { INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"), // Forwarding + NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"), + + // Routing + NOT_FOUND_ROUTING(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 파일을 수정하지 못했습니다"), - NOT_FOUND_FORWARDING(HttpStatus.BAD_REQUEST, "포트포워딩 파일이 존재하지 않습니다"), 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 파일 테스트에 실패했습니다"), 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 0000000..392c90b --- /dev/null +++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java @@ -0,0 +1,7 @@ +package com.aolda.itda.repository.certificate; + +import com.aolda.itda.entity.certificate.Certificate; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CertificateRepository extends JpaRepository<Certificate, 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 0000000..046296c --- /dev/null +++ b/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java @@ -0,0 +1,14 @@ +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 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); +} 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 0000000..9cfad7d --- /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.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.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; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class RoutingService { + + private final RoutingRepository routingRepository; + private final CertificateRepository certificateRepository; + private final RoutingTemplate routingTemplate; + private final RestTemplate restTemplate = new RestTemplate(); + + /* Routing 조회 */ + public RoutingDTO getRouting(Long routingId) { + // project id 확인 필요 + Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING)); + return routing.toRoutingDTO(); + } + + /* Routing 목록 조회 */ + public PageResp<RoutingDTO> getRoutings(String projectId) { + // project id 확인 필요 + return PageResp.<RoutingDTO>builder() + .contents(routingRepository.findByProjectIdAndIsDeleted(projectId, false) + .stream() + .map(Routing::toRoutingDTO) + .toList()).build(); + } + + /* Routing 생성 */ + public void createRouting(String projectId, RoutingDTO dto) { + /* 입력 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) { + e.printStackTrace(); + 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) { + e.printStackTrace(); + 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) { + log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); + 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) { + log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); + if (file.delete()) { + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); + } + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); + } + + } + + /* Routing 수정 */ + public void editRouting(Long routingId, RoutingDTO dto) { + Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING)); + + /* 입력 DTO 검증 */ + validateDTO(dto); + + /* 중복 검증 */ + if (dto.getDomain() != null && routingRepository.existsByDomainAndIsDeleted(dto.getDomain(), false)) { + throw new CustomException(ErrorCode.DUPLICATED_DOMAIN_NAME); + } + + /* SSL 인증서 조회 */ + Certificate certificate = (dto.getCertificateId() == null) || (dto.getCertificateId() == -1 ) ? null : + certificateRepository.findById(dto.getCertificateId()) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요 + + /* 파일 수정 */ + routing.edit(dto, certificate); + String content = routingTemplate.getRouting(routing.toRoutingDTO(), 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) { + e.printStackTrace(); + 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 (HttpServerErrorException.InternalServerError e) { + log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); + 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); + } catch (RuntimeException e) { + log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); + 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 (HttpServerErrorException.InternalServerError e) { + log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); + 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); + } catch (RuntimeException e) { + log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); + 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) { + Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING)); + + /* 파일 삭제 */ + 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 (HttpServerErrorException.InternalServerError e) { + log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); + 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); + } catch (Exception e) { + log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); + 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 (HttpServerErrorException.InternalServerError e) { + log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); + 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); + } catch (Exception e) { + log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); + 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/OptionTemplate.java b/src/main/java/com/aolda/itda/template/OptionTemplate.java index 3e1e92c..945b666 100644 --- a/src/main/java/com/aolda/itda/template/OptionTemplate.java +++ b/src/main/java/com/aolda/itda/template/OptionTemplate.java @@ -5,22 +5,22 @@ import org.springframework.stereotype.Component; @Component public class OptionTemplate { - public String getSSL(Long certificateId) { + public String getSSL(String certificateDomain) { return "\nconf.d/include/letsencrypt-acme-challenge.conf;\n" + "include conf.d/include/ssl-ciphers.conf;\n" + - "ssl_certificate /etc/letsencrypt/live/npm-" + certificateId + "/fullchain.pem;\n" + - "ssl_certificate_key /etc/letsencrypt/live/npm-" + certificateId + "/privkey.pem;\n"; + "ssl_certificate /data/lego/certificates/" + certificateDomain + ".crt;\n" + + "ssl_certificate_key /data/lego/certificates/" + certificateDomain + ".key;\n"; } public String getAssetCaching() { - return "include conf.d/include/assets.conf;\n"; + return "\ninclude conf.d/include/assets.conf;\n"; } public String getBlockExploits() { - return "include conf.d/include/block-exploits.conf;\n"; + return "\ninclude conf.d/include/block-exploits.conf;\n"; } public String getForceSSL() { - return "include conf.d/include/force-ssl.conf;\n"; + 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 0000000..46e6116 --- /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;\n listen [::]: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"; + } + +} -- GitLab From 783724adc2cf3f660a53565a576df6a4865b3b88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Mon, 17 Mar 2025 21:43:19 +0900 Subject: [PATCH 19/41] =?UTF-8?q?fix:=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20Crea?= =?UTF-8?q?te=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=B9=A0=EC=A0=B8?= =?UTF-8?q?=EC=9E=88=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aolda/itda/controller/routing/RoutingController.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java index 8943571..aec9927 100644 --- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java +++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java @@ -1,5 +1,6 @@ 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 lombok.RequiredArgsConstructor; @@ -13,6 +14,13 @@ public class RoutingController { private final RoutingService routingService; + @PostMapping("/forwarding") + public ResponseEntity<Object> create(@RequestParam String projectId, + @RequestBody RoutingDTO dto) { + routingService.createRouting(projectId, dto); + return ResponseEntity.ok().build(); + } + @GetMapping("/routing") public ResponseEntity<Object> view(@RequestParam Long routingId) { return ResponseEntity.ok(routingService.getRouting(routingId)); -- GitLab From 585f5bb27620012fa5f77904532a98adf4b96ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Tue, 18 Mar 2025 13:42:46 +0900 Subject: [PATCH 20/41] =?UTF-8?q?fix:=20API=20=EA=B2=BD=EB=A1=9C,=20DB=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EA=B0=80=20=EC=9E=98=EB=AA=BB=EB=90=98?= =?UTF-8?q?=EC=96=B4=EC=9E=88=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aolda/itda/controller/routing/RoutingController.java | 2 +- src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java | 2 +- src/main/java/com/aolda/itda/entity/routing/Routing.java | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java index aec9927..dc4d0db 100644 --- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java +++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java @@ -14,7 +14,7 @@ public class RoutingController { private final RoutingService routingService; - @PostMapping("/forwarding") + @PostMapping("/routing") public ResponseEntity<Object> create(@RequestParam String projectId, @RequestBody RoutingDTO dto) { routingService.createRouting(projectId, dto); diff --git a/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java index e95ab70..6bc48d8 100644 --- a/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java +++ b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java @@ -25,7 +25,7 @@ public class RoutingDTO { @NotBlank private String port; private Long id; - @NotBlank + @NotNull private Long certificateId; private LocalDateTime createdAt; diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java index be03dcd..39b7716 100644 --- a/src/main/java/com/aolda/itda/entity/routing/Routing.java +++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java @@ -25,11 +25,11 @@ public class Routing extends BaseTimeEntity { private Long routingId; @OneToOne - @JoinColumn(name = "user_id", nullable = false) + @JoinColumn(name = "user_id") private User user; @OneToOne - @JoinColumn(name = "certificate_id", nullable = false) + @JoinColumn(name = "certificate_id") private Certificate certificate; private String projectId; -- GitLab From 3f8e2f278bdcf3fe0e9988cf58e3d74d19741289 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Tue, 18 Mar 2025 16:23:39 +0900 Subject: [PATCH 21/41] =?UTF-8?q?feat:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=EB=B3=84=20=EA=B6=8C=ED=95=9C=20=EA=B2=80=EC=A6=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aolda/itda/config/AuthInterceptor.java | 47 +++++++++++++++---- .../forwarding/ForwardingController.java | 17 ++++--- .../controller/routing/RoutingController.java | 18 ++++--- .../com/aolda/itda/exception/ErrorCode.java | 1 + .../com/aolda/itda/service/AuthService.java | 8 +++- .../service/forwarding/ForwardingService.java | 20 ++++++-- .../itda/service/routing/RoutingService.java | 20 ++++++-- 7 files changed, 103 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java index 63f2320..83c3821 100644 --- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java +++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java @@ -1,8 +1,10 @@ package com.aolda.itda.config; +import com.aolda.itda.dto.auth.ProjectIdAndNameDTO; 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; @@ -10,6 +12,10 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + @RequiredArgsConstructor @Component @Slf4j @@ -20,18 +26,43 @@ public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("X-Subject-Token"); + + /* 토큰 헤더 검증 */ if (token == null || token.isEmpty()) { throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI()); } - // 어드민과 일반 유저 구분 필요 - try { - if (authService.validateTokenAndGetUserId(token) != null) { - return true; - } - } catch (Exception e) { - log.error("Token validation failed for URI {}: {}", request.getRequestURI(), e.getMessage(), e); + + /* 유효 토큰 검증 */ + 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()); } - throw new CustomException(ErrorCode.INVALID_TOKEN, request.getRequestURI()); + + /* 프로젝트 권한 검증 */ + String projectId = request.getParameter("projectId"); + if (projectId != null) { + + try { + String role = authService.getBestRoleWithinProject(token, projectId).get("role"); + if (!role.equals("admin")) { + log.error("Unauthorized Token for URI {}: {}", request.getRequestURI(), request.getRemoteAddr()); + throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI()); + } + } catch (Exception e) { + throw new CustomException(ErrorCode.UNAUTHORIZED_USER, request.getRequestURI()); + } + + + + } + + /* 프로젝트 리스트 조회 */ + List<String> projects = authService.getProjectsWithUser(Map.of("id", userId, "token", token)) + .stream().map(ProjectIdAndNameDTO::getId) + .toList(); + request.setAttribute("projects", projects); + return true; + } } diff --git a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java index a6b67ec..36e6f9a 100644 --- a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java +++ b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java @@ -2,11 +2,14 @@ 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 @@ -22,8 +25,8 @@ public class ForwardingController { } @GetMapping("/forwarding") - public ResponseEntity<Object> view(@RequestParam Long forwardingId) { - return ResponseEntity.ok(forwardingService.getForwarding(forwardingId)); + public ResponseEntity<Object> view(@RequestParam Long forwardingId, HttpServletRequest request) { + return ResponseEntity.ok(forwardingService.getForwarding(forwardingId, (List<String>) request.getAttribute("projects"))); } @GetMapping("/forwardings") @@ -33,14 +36,16 @@ public class ForwardingController { @PatchMapping("/forwarding") public ResponseEntity<Object> edit(@RequestParam Long forwardingId, - @RequestBody ForwardingDTO dto) { - forwardingService.editForwarding(forwardingId, dto); + @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) { - forwardingService.deleteForwarding(forwardingId); + 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/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java index dc4d0db..4b9d551 100644 --- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java +++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java @@ -3,10 +3,13 @@ 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.util.List; + @RestController @RequestMapping("/api") @RequiredArgsConstructor @@ -22,8 +25,9 @@ public class RoutingController { } @GetMapping("/routing") - public ResponseEntity<Object> view(@RequestParam Long routingId) { - return ResponseEntity.ok(routingService.getRouting(routingId)); + public ResponseEntity<Object> view(@RequestParam Long routingId, + HttpServletRequest request) { + return ResponseEntity.ok(routingService.getRouting(routingId, (List<String>) request.getAttribute("projects"))); } @GetMapping("/routings") @@ -33,14 +37,16 @@ public class RoutingController { @PatchMapping("/routing") public ResponseEntity<Object> edit(@RequestParam Long routingId, - @RequestBody RoutingDTO dto) { - routingService.editRouting(routingId, dto); + @RequestBody RoutingDTO dto, + HttpServletRequest request) { + routingService.editRouting(routingId, dto, (List<String>) request.getAttribute("projects")); return ResponseEntity.ok().build(); } @DeleteMapping("/routing") - public ResponseEntity<Object> delete(@RequestParam Long routingId) { - routingService.deleteRouting(routingId); + 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/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java index 321bdad..6eea5fb 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, "잘못된 회원 정보입니다"), + UNAUTHORIZED_USER(HttpStatus.BAD_REQUEST, "권한이 없는 사용자입니다"), // Token INVALID_TOKEN(HttpStatus.BAD_REQUEST, "잘못된 토큰입니다"), diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index cc523fa..cbc157f 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -239,7 +239,7 @@ public class AuthService { } // 특정 사용자의 참여 프로젝트 반환 - private List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException { + public List<ProjectIdAndNameDTO> getProjectsWithUser(Map<String, String> user) throws JsonProcessingException { String userId = user.get("id"); String token = user.get("token"); if (userId == null || token == null) { @@ -283,6 +283,12 @@ public class AuthService { } + public void validateProjectAuth(List<String> projects, String projectId) { + if (!projects.contains(projectId)) { + throw new CustomException(ErrorCode.UNAUTHORIZED_USER); + } + } + private 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(); 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 9ec9343..441090b 100644 --- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java +++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java @@ -1,13 +1,16 @@ 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; 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; @@ -30,6 +33,7 @@ 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.Map; @Service @@ -42,12 +46,17 @@ public class ForwardingService { 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) { + 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(); } @@ -158,10 +167,12 @@ public class ForwardingService { } /* 포트포워딩 정보 수정 */ - public void editForwarding(Long forwardingId, ForwardingDTO dto) { + 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)) { @@ -270,10 +281,13 @@ public class ForwardingService { } /* 포트포워딩 삭제 (소프트) */ - public void deleteForwarding(Long forwardingId) { + 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"; 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 9cfad7d..eb3777b 100644 --- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java +++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java @@ -8,6 +8,7 @@ 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; @@ -26,6 +27,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; +import java.util.List; @Service @Transactional @@ -35,14 +37,18 @@ 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) { - // project id 확인 필요 + 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(); } @@ -154,10 +160,13 @@ public class RoutingService { } /* Routing 수정 */ - public void editRouting(Long routingId, RoutingDTO dto) { + public void editRouting(Long routingId, RoutingDTO dto, List<String> projects) { Routing routing = routingRepository.findByRoutingIdAndIsDeleted(routingId, false) .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_ROUTING)); + /* 프로젝트 권한 검증 */ + authService.validateProjectAuth(projects, routing.getProjectId()); + /* 입력 DTO 검증 */ validateDTO(dto); @@ -253,10 +262,13 @@ public class RoutingService { } /* Routing 삭제 */ - public void deleteRouting(Long routingId) { + 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"; -- GitLab From ad7d22e661ac3b0f41f779c4f6ae023466791501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sat, 22 Mar 2025 22:28:17 +0900 Subject: [PATCH 22/41] =?UTF-8?q?feat:=20=EC=82=AC=EC=9A=A9=EC=9E=90=20CUD?= =?UTF-8?q?=20=ED=99=9C=EB=8F=99=20=EB=A1=9C=EA=B7=B8=20=EC=83=9D=EC=84=B1?= =?UTF-8?q?=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C=20=EB=A1=9C=EC=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 9 +- .../itda/aspect/ForwardingLogAspect.java | 154 +++++++++++++++++ .../aolda/itda/aspect/RoutingLogAspect.java | 157 ++++++++++++++++++ .../aolda/itda/config/AuthInterceptor.java | 7 +- .../java/com/aolda/itda/config/WebConfig.java | 10 +- .../itda/controller/log/LogController.java | 41 +++++ .../java/com/aolda/itda/dto/PageResp.java | 2 +- .../com/aolda/itda/dto/auth/IdAndNameDTO.java | 24 +++ .../aolda/itda/dto/auth/LoginResponseDTO.java | 2 +- .../itda/dto/auth/ProjectIdAndNameDTO.java | 16 -- .../java/com/aolda/itda/dto/log/LogDTO.java | 38 +++++ .../com/aolda/itda/entity/BaseTimeEntity.java | 3 + .../itda/entity/forwarding/Forwarding.java | 18 +- .../java/com/aolda/itda/entity/log/Log.java | 14 ++ .../com/aolda/itda/exception/ErrorCode.java | 4 + .../itda/repository/log/LogQueryDSL.java | 104 ++++++++++++ .../itda/repository/log/LogRepository.java | 10 ++ .../itda/repository/user/UserRepository.java | 11 ++ .../com/aolda/itda/service/AuthService.java | 22 ++- .../service/forwarding/ForwardingService.java | 12 +- .../aolda/itda/service/log/LogService.java | 57 +++++++ .../itda/service/routing/RoutingService.java | 11 +- 22 files changed, 676 insertions(+), 50 deletions(-) create mode 100644 src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java create mode 100644 src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java create mode 100644 src/main/java/com/aolda/itda/controller/log/LogController.java create mode 100644 src/main/java/com/aolda/itda/dto/auth/IdAndNameDTO.java delete mode 100644 src/main/java/com/aolda/itda/dto/auth/ProjectIdAndNameDTO.java create mode 100644 src/main/java/com/aolda/itda/dto/log/LogDTO.java create mode 100644 src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java create mode 100644 src/main/java/com/aolda/itda/repository/log/LogRepository.java create mode 100644 src/main/java/com/aolda/itda/repository/user/UserRepository.java create mode 100644 src/main/java/com/aolda/itda/service/log/LogService.java diff --git a/build.gradle b/build.gradle index b88f454..1b29e4a 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 0000000..f515ab6 --- /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 0000000..c8b052a --- /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 83c3821..3a95f8a 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 71e134b..e3893ae 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 0000000..0d4bfcf --- /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 49a2a6c..7079600 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 0000000..5918105 --- /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 33ba32f..a647b05 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 1ca894d..0000000 --- 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 0000000..5895455 --- /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 4460eb7..6cfa1fd 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 b0c8d34..86f46b7 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 fa78a66..373a836 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 6eea5fb..3e4b0be 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 0000000..3ec962b --- /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 0000000..de0a769 --- /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 0000000..44cae68 --- /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 cbc157f..be6d764 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 441090b..85c1ec3 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 0000000..b33cc2e --- /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 eb3777b..7b30c91 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()) { -- GitLab From 8207b91fbc687d2f13658ca6a7904d93855eea59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Mon, 24 Mar 2025 19:00:48 +0900 Subject: [PATCH 23/41] =?UTF-8?q?fix:=20DB=20=EC=97=B0=EA=B4=80=EA=B4=80?= =?UTF-8?q?=EA=B3=84=20=EC=88=98=EC=A0=95,=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EB=A9=94=EC=84=B8=EC=A7=80=EA=B0=80=20=EC=9E=98=EB=AA=BB?= =?UTF-8?q?=EB=90=98=EC=96=B4=EC=9E=88=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../itda/aspect/ForwardingLogAspect.java | 15 +++--- .../aolda/itda/aspect/RoutingLogAspect.java | 51 ++++++++++++------- .../itda/entity/certificate/Certificate.java | 2 +- .../itda/entity/forwarding/Forwarding.java | 5 -- .../java/com/aolda/itda/entity/log/Log.java | 2 +- .../aolda/itda/entity/routing/Routing.java | 6 +-- .../itda/repository/log/LogQueryDSL.java | 30 +++++------ .../com/aolda/itda/service/AuthService.java | 2 +- 8 files changed, 58 insertions(+), 55 deletions(-) diff --git a/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java index f515ab6..d50c2fe 100644 --- a/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java +++ b/src/main/java/com/aolda/itda/aspect/ForwardingLogAspect.java @@ -23,6 +23,7 @@ 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; @@ -46,7 +47,7 @@ public class ForwardingLogAspect { /* 사용자 조회 */ HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); - Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user"); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( () -> new CustomException(ErrorCode.NOT_FOUND_USER) ); @@ -65,7 +66,7 @@ public class ForwardingLogAspect { .user(user) .objectType(ObjectType.FORWARDING) .objectId(forwarding.getForwardingId()) - .action(Action.UPDATE) + .action(Action.CREATE) .projectId(forwarding.getProjectId()) .description(description) .build()); @@ -77,7 +78,7 @@ public class ForwardingLogAspect { /* 사용자 조회 */ HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); - Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user"); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( () -> new CustomException(ErrorCode.NOT_FOUND_USER) ); @@ -99,7 +100,7 @@ public class ForwardingLogAspect { .user(user) .objectType(ObjectType.FORWARDING) .objectId(forwarding.getForwardingId()) - .action(Action.UPDATE) + .action(Action.DELETE) .projectId(forwarding.getProjectId()) .description(description) .build()); @@ -111,7 +112,7 @@ public class ForwardingLogAspect { /* 사용자 조회 */ HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); - Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user"); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( () -> new CustomException(ErrorCode.NOT_FOUND_USER) ); @@ -134,8 +135,8 @@ public class ForwardingLogAspect { /* 로그 메세지 작성 */ 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())); + + "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() diff --git a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java index c8b052a..a3a67d2 100644 --- a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java +++ b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java @@ -23,6 +23,7 @@ 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; @@ -46,7 +47,7 @@ public class RoutingLogAspect { /* 사용자 조회 */ HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); - Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user"); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( () -> new CustomException(ErrorCode.NOT_FOUND_USER) ); @@ -58,16 +59,16 @@ public class RoutingLogAspect { String description = "name: " + routing.getName() + "\n" + "domain: " + routing.getDomain() + "\n" + "ip: " + routing.getInstanceIp() + "\n" - + "ip: " + routing.getInstancePort() + "\n" - + "certificateId: " + routing.getCertificate().getCertificateId() + "\n" + + "port: " + routing.getInstancePort() + "\n" + + (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "") + "caching: " + routing.getCaching() + "\n"; /* 로그 엔티티 저장 */ logRepository.save(Log.builder() .user(user) - .objectType(ObjectType.FORWARDING) + .objectType(ObjectType.ROUTING) .objectId(routing.getRoutingId()) - .action(Action.UPDATE) + .action(Action.CREATE) .projectId(routing.getProjectId()) .description(description) .build()); @@ -79,7 +80,7 @@ public class RoutingLogAspect { /* 사용자 조회 */ HttpServletRequest request = ((ServletRequestAttributes) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getRequest(); - Map<String, String> tmp = (Map<String, String>) request.getSession().getAttribute("user"); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( () -> new CustomException(ErrorCode.NOT_FOUND_USER) ); @@ -94,28 +95,28 @@ public class RoutingLogAspect { String description = "name: " + routing.getName() + "\n" + "domain: " + routing.getDomain() + "\n" + "ip: " + routing.getInstanceIp() + "\n" - + "ip: " + routing.getInstancePort() + "\n" - + "certificateId: " + routing.getCertificate().getCertificateId() + "\n" + + "port: " + routing.getInstancePort() + "\n" + + (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "") + "caching: " + routing.getCaching() + "\n"; /* 로그 엔티티 저장 */ logRepository.save(Log.builder() .user(user) - .objectType(ObjectType.FORWARDING) + .objectType(ObjectType.ROUTING) .objectId(routing.getRoutingId()) - .action(Action.UPDATE) + .action(Action.DELETE) .projectId(routing.getProjectId()) .description(description) .build()); } /* Update(edit) 로깅 */ - @Around("execution(* com.aolda.itda.service.forwarding.*Service.*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.getSession().getAttribute("user"); + Map<String, String> tmp = (Map<String, String>) request.getAttribute("user"); User user = userRepository.findByKeystoneId(tmp.get("id")).orElseThrow( () -> new CustomException(ErrorCode.NOT_FOUND_USER) ); @@ -136,17 +137,29 @@ public class RoutingLogAspect { 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())); + 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 (old.getCertificate() == null) { + if (newObj.getCertificate() != null) { + description = description + "certificateId: null -> " + newObj.getCertificate().getCertificateId() + "\n"; + } + } + else { + if (newObj.getCertificate() == null) { + description = description + "certificateId: " + old.getCertificate().getCertificateId() + " -> null\n"; + } + else { + description = description + "certificateId: " + old.getCertificate().getCertificateId() + " -> " + newObj.getCertificate().getCertificateId() + "\n"; + } + } + description = description + "caching: " + (old.getCaching() == newObj.getCaching() ? newObj.getCaching() : (" -> " + newObj.getCaching())); /* 로그 엔티티 저장 */ logRepository.save(Log.builder() .user(user) - .objectType(ObjectType.FORWARDING) + .objectType(ObjectType.ROUTING) .objectId(newObj.getRoutingId()) .action(Action.UPDATE) .projectId(newObj.getProjectId()) diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java index 188f05e..a23cf85 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -23,7 +23,7 @@ public class Certificate extends BaseTimeEntity { @Column(nullable = false) private Long certificateId; - @OneToOne + @ManyToOne @JoinColumn(nullable = false, name = "user_id") private User user; 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 86f46b7..0e5e505 100644 --- a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java +++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java @@ -20,10 +20,6 @@ public class Forwarding extends BaseTimeEntity { @Column(nullable = false) private Long forwardingId; - @OneToOne - @JoinColumn(name = "user_id") - private User user; - private String projectId; private String serverIp; @@ -40,7 +36,6 @@ public class Forwarding extends BaseTimeEntity { public Forwarding(Forwarding forwarding) { this.forwardingId = forwarding.getForwardingId(); - this.user = forwarding.getUser(); this.projectId = forwarding.getProjectId(); this.serverIp = forwarding.getServerIp(); this.serverPort = forwarding.getServerPort(); 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 373a836..9832ab2 100644 --- a/src/main/java/com/aolda/itda/entity/log/Log.java +++ b/src/main/java/com/aolda/itda/entity/log/Log.java @@ -23,7 +23,7 @@ public class Log extends BaseTimeEntity { @Column(nullable = false) private Long logId; - @OneToOne + @ManyToOne @JoinColumn(name = "user_id", nullable = false) private User user; diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java index 39b7716..de13d82 100644 --- a/src/main/java/com/aolda/itda/entity/routing/Routing.java +++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java @@ -24,11 +24,7 @@ public class Routing extends BaseTimeEntity { @Column(nullable = false) private Long routingId; - @OneToOne - @JoinColumn(name = "user_id") - private User user; - - @OneToOne + @ManyToOne @JoinColumn(name = "certificate_id") private Certificate certificate; diff --git a/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java index 3ec962b..ceba4b2 100644 --- a/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java +++ b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java @@ -74,31 +74,29 @@ public class LogQueryDSL { 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)); + /* 오브젝트 타입 조건 */ + 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.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)); + 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/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index be6d764..80c53d3 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -51,7 +51,7 @@ public class AuthService { User entity = userRepository.findByKeystoneUsername(userId).orElse(null); if (entity == null) { userRepository.save(User.builder().keystoneId(validateTokenAndGetUserId(token)). - keystoneUsername(userId).build()); + keystoneUsername(loginRequestDTO.getId()).build()); } response.addHeader("X-Subject-Token", systemToken != null ? systemToken : token); -- GitLab From c516dbbf1dfd07b8df7ec027170688d9262e9074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=ED=95=9C=EB=8F=99=ED=98=84?= <hando1220@ajou.ac.kr> Date: Tue, 25 Mar 2025 14:15:47 +0900 Subject: [PATCH 24/41] =?UTF-8?q?chore:=20lego=20=EB=B0=94=EC=9D=B4?= =?UTF-8?q?=EB=84=88=EB=A6=AC=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20=EC=9D=B4?= =?UTF-8?q?=EB=AF=B8=EC=A7=80=20=EC=9A=A9=EB=9F=89=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Dockerfile | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 16f6160..3742c4d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,10 +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 . . + +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 openjdk:21-jdk-slim +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"] -- GitLab From 1c4faf35c8da14efff5a198ce993dcc526751ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Wed, 26 Mar 2025 17:57:28 +0900 Subject: [PATCH 25/41] =?UTF-8?q?feat:=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=20=EC=B6=9C=EB=A0=A5=20=EB=B0=8F=20=EC=A0=80?= =?UTF-8?q?=EC=9E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aolda/itda/config/LoggingFilter.java | 51 +++++++++++ .../java/com/aolda/itda/entity/user/User.java | 4 + .../itda/exception/ApiExceptionHandler.java | 2 +- .../com/aolda/itda/service/AuthService.java | 8 +- .../service/forwarding/ForwardingService.java | 55 +----------- .../itda/service/routing/RoutingService.java | 58 ------------ src/main/resources/logback-spring.xml | 90 +++++++++++++++++++ 7 files changed, 153 insertions(+), 115 deletions(-) create mode 100644 src/main/java/com/aolda/itda/config/LoggingFilter.java create mode 100644 src/main/resources/logback-spring.xml 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 0000000..0dd73b1 --- /dev/null +++ b/src/main/java/com/aolda/itda/config/LoggingFilter.java @@ -0,0 +1,51 @@ +package com.aolda.itda.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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; + +@Component +public class LoggingFilter extends OncePerRequestFilter { + + private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + // Request Body를 읽을 수 있도록 래핑 + ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request); + System.out.println("필터 적용"); + filterChain.doFilter(cachingRequest, response); + + // 로그 기록 + logRequest(cachingRequest); + System.out.println("왜 안돼ㅐ"); + } + + private void logRequest(ContentCachingRequestWrapper request) { + System.out.println("되는거 맞아?"); + String ip = request.getRemoteAddr(); + String method = request.getMethod(); + String uri = request.getRequestURI(); + String queryString = request.getQueryString(); + String body = getRequestBody(request); + + logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, Request Body: {}", + ip, method, uri, (queryString != null ? queryString : "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/entity/user/User.java b/src/main/java/com/aolda/itda/entity/user/User.java index df006b2..682a50e 100644 --- a/src/main/java/com/aolda/itda/entity/user/User.java +++ b/src/main/java/com/aolda/itda/entity/user/User.java @@ -21,4 +21,8 @@ public class User { 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 index 4d1f360..8dbb993 100644 --- a/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java +++ b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java @@ -11,7 +11,7 @@ public class ApiExceptionHandler { @ExceptionHandler(value = CustomException.class) public ResponseEntity<ErrorResponse> handleCustomException(CustomException e) { - log.error("[handleCustomException] {} : {}", e.getErrorCode().name(), e.getErrorCode().getMessage()); + log.error("[handleCustomException] {} : {}, {}", e.getErrorCode().name(), e.getErrorCode().getMessage(), e.getStackTrace()); return ErrorResponse.fromException(e); } } diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index 80c53d3..6a25ebc 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -48,11 +48,15 @@ public class AuthService { } - User entity = userRepository.findByKeystoneUsername(userId).orElse(null); + User entity = userRepository.findByKeystoneId(userId).orElse(null); if (entity == null) { - userRepository.save(User.builder().keystoneId(validateTokenAndGetUserId(token)). + 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() 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 85c1ec3..cae38c3 100644 --- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java +++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java @@ -102,7 +102,6 @@ public class ForwardingService { throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "중복된 포트포워딩 Conf 파일이 존재합니다"); } } catch (IOException e) { - e.printStackTrace(); throw new CustomException(ErrorCode.FAIL_CREATE_CONF); } @@ -113,7 +112,6 @@ public class ForwardingService { bw.flush(); bw.close(); } catch (Exception e) { - e.printStackTrace(); if (file.delete()) { throw new CustomException(ErrorCode.FAIL_DELETE_CONF); } @@ -131,7 +129,6 @@ public class ForwardingService { } throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST); } catch (Exception e) { - log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); if (file.delete()) { throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); } @@ -149,7 +146,6 @@ public class ForwardingService { } throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); } catch (Exception e) { - log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); if (file.delete()) { throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); } @@ -168,8 +164,6 @@ public class ForwardingService { /* 중복 검증 */ if (dto.getServerPort() != null && forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false)) { - System.out.println(dto.getServerPort()); - System.out.println(forwarding.getServerPort()); forwardingRepository.existsByServerPortAndIsDeleted(dto.getServerPort(), false); throw new CustomException(ErrorCode.DUPLICATED_SERVER_PORT); } @@ -179,12 +173,6 @@ public class ForwardingService { dto.getInstanceIp() == null ? forwarding.getInstanceIp() : dto.getInstanceIp() , dto.getInstancePort() == null ? forwarding.getInstancePort() : dto.getInstancePort() , false)) { - System.out.println(dto.getInstanceIp()); - System.out.println(forwarding.getInstanceIp()); - System.out.println(forwardingRepository.existsByInstanceIpAndInstancePortAndIsDeleted( - dto.getInstanceIp() == null ? forwarding.getInstanceIp() : dto.getInstanceIp() - , dto.getInstancePort() == null ? forwarding.getInstancePort() : dto.getInstancePort() - , false)); throw new CustomException(ErrorCode.DUPLICATED_INSTANCE_INFO); } @@ -211,7 +199,6 @@ public class ForwardingService { bw.flush(); bw.close(); } catch (Exception e) { - e.printStackTrace(); throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "포트포워딩 Conf 파일을 수정하지 못했습니다"); } @@ -219,19 +206,8 @@ public class ForwardingService { 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()); - 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); } catch (RuntimeException e) { - log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); + try { Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING , StandardCopyOption.COPY_ATTRIBUTES); @@ -246,18 +222,7 @@ public class ForwardingService { url = "http://nginx:8081/nginx-api/reload"; try { restTemplate.getForEntity(url, String.class); - } catch (HttpServerErrorException.InternalServerError e) { - log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); - 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); } catch (RuntimeException e) { - log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); try { Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING , StandardCopyOption.COPY_ATTRIBUTES); @@ -293,16 +258,7 @@ public class ForwardingService { 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()); - 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); } catch (Exception e) { - log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); try { Files.move(Paths.get(deletePath), Paths.get(confPath)); } catch (IOException e1) { @@ -315,16 +271,7 @@ public class ForwardingService { url = "http://nginx:8081/nginx-api/reload"; try { restTemplate.getForEntity(url, String.class); - } catch (HttpServerErrorException.InternalServerError e) { - log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); - 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); } catch (Exception e) { - log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); try { Files.move(Paths.get(deletePath), Paths.get(confPath)); } catch (IOException e1) { 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 7b30c91..769278d 100644 --- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java +++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java @@ -102,7 +102,6 @@ public class RoutingService { throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "중복된 라우팅 Conf 파일이 존재합니다"); } } catch (IOException e) { - e.printStackTrace(); throw new CustomException(ErrorCode.FAIL_CREATE_CONF); } @@ -113,7 +112,6 @@ public class RoutingService { bw.flush(); bw.close(); } catch (Exception e) { - e.printStackTrace(); if (file.delete()) { throw new CustomException(ErrorCode.FAIL_DELETE_CONF); } @@ -124,14 +122,7 @@ public class RoutingService { 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) { - log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); if (file.delete()) { throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); } @@ -142,14 +133,7 @@ public class RoutingService { 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) { - log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); if (file.delete()) { throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); } @@ -199,7 +183,6 @@ public class RoutingService { bw.flush(); bw.close(); } catch (Exception e) { - e.printStackTrace(); throw new CustomException(ErrorCode.FAIL_UPDATE_CONF, "라우팅 Conf 파일을 수정하지 못했습니다"); } @@ -207,19 +190,7 @@ public class RoutingService { 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()); - 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); } catch (RuntimeException e) { - log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); try { Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING , StandardCopyOption.COPY_ATTRIBUTES); @@ -234,18 +205,7 @@ public class RoutingService { url = "http://nginx:8081/nginx-api/reload"; try { restTemplate.getForEntity(url, String.class); - } catch (HttpServerErrorException.InternalServerError e) { - log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); - 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); } catch (RuntimeException e) { - log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); try { Files.copy(backup, Paths.get(confPath), StandardCopyOption.REPLACE_EXISTING , StandardCopyOption.COPY_ATTRIBUTES); @@ -281,16 +241,7 @@ public class RoutingService { 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()); - 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); } catch (Exception e) { - log.error("[RestClientException] {} : {}", "Nginx Conf Test (forwarding)", e.getMessage()); try { Files.move(Paths.get(deletePath), Paths.get(confPath)); } catch (IOException e1) { @@ -303,16 +254,7 @@ public class RoutingService { url = "http://nginx:8081/nginx-api/reload"; try { restTemplate.getForEntity(url, String.class); - } catch (HttpServerErrorException.InternalServerError e) { - log.error("[nginxApiException] {} : {}", e.getResponseBodyAsString(), e.getMessage()); - 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); } catch (Exception e) { - log.error("[RestClientException] {} : {}", "Nginx Conf Reload (forwarding)", e.getMessage()); try { Files.move(Paths.get(deletePath), Paths.get(confPath)); } catch (IOException e1) { diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..a8acbee --- /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 -- GitLab From a09cf30c2b6077352b617d462a5fee0aaa5fb368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Wed, 26 Mar 2025 21:46:11 +0900 Subject: [PATCH 26/41] =?UTF-8?q?feat:=20DB=20=ED=95=84=EB=93=9C=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=EC=84=A4=EC=A0=95=20=EB=B0=8F=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=96=91=EC=8B=9D=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/aolda/itda/dto/forwarding/ForwardingDTO.java | 5 +++++ src/main/java/com/aolda/itda/dto/log/LogDTO.java | 5 +++++ src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java | 3 +++ src/main/java/com/aolda/itda/entity/BaseTimeEntity.java | 4 ++-- .../java/com/aolda/itda/entity/certificate/Certificate.java | 4 ++++ .../java/com/aolda/itda/entity/forwarding/Forwarding.java | 6 ++++++ src/main/java/com/aolda/itda/entity/log/Log.java | 4 ++++ src/main/java/com/aolda/itda/entity/routing/Routing.java | 5 +++++ 8 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java index 3302d0a..be003b6 100644 --- a/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java +++ b/src/main/java/com/aolda/itda/dto/forwarding/ForwardingDTO.java @@ -1,5 +1,6 @@ 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; @@ -40,6 +41,10 @@ public class ForwardingDTO { @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 index 5895455..9946028 100644 --- a/src/main/java/com/aolda/itda/dto/log/LogDTO.java +++ b/src/main/java/com/aolda/itda/dto/log/LogDTO.java @@ -3,6 +3,7 @@ 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; @@ -22,7 +23,11 @@ public class LogDTO { 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 diff --git a/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java index 6bc48d8..985056e 100644 --- a/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java +++ b/src/main/java/com/aolda/itda/dto/routing/RoutingDTO.java @@ -1,5 +1,6 @@ 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; @@ -28,8 +29,10 @@ public class RoutingDTO { @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 diff --git a/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java index 6cfa1fd..b62af84 100644 --- a/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java +++ b/src/main/java/com/aolda/itda/entity/BaseTimeEntity.java @@ -17,12 +17,12 @@ import java.time.LocalDateTime; public abstract class BaseTimeEntity { @CreatedDate - @Column(updatable = false) + @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") + @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 index a23cf85..85b1f29 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -27,10 +27,13 @@ public class Certificate extends BaseTimeEntity { @JoinColumn(nullable = false, name = "user_id") private User user; + @Column(length = 64) private String projectId; + @Column(length = 64) private String domain; + @Column(length = 64) private String email; private LocalDateTime expiredAt; @@ -40,6 +43,7 @@ public class Certificate extends BaseTimeEntity { private Boolean isDeleted; + @Column(length = 256) private String description; public String formatDomain() { 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 0e5e505..0ba96d6 100644 --- a/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java +++ b/src/main/java/com/aolda/itda/entity/forwarding/Forwarding.java @@ -20,18 +20,24 @@ public class Forwarding extends BaseTimeEntity { @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) { 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 9832ab2..6bf6074 100644 --- a/src/main/java/com/aolda/itda/entity/log/Log.java +++ b/src/main/java/com/aolda/itda/entity/log/Log.java @@ -27,16 +27,20 @@ public class Log extends BaseTimeEntity { @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() { diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java index de13d82..94ecb55 100644 --- a/src/main/java/com/aolda/itda/entity/routing/Routing.java +++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java @@ -28,18 +28,23 @@ public class Routing extends BaseTimeEntity { @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() { -- GitLab From 62f400dffe4db2c570926c9a831617ed14a560e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Wed, 26 Mar 2025 22:39:01 +0900 Subject: [PATCH 27/41] =?UTF-8?q?feat:=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20?= =?UTF-8?q?=EC=96=B4=EB=93=9C=EB=AF=BC=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=A0=91=EA=B7=BC=20=EA=B6=8C=ED=95=9C=20=EB=B6=80?= =?UTF-8?q?=EC=97=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aolda/itda/config/AuthInterceptor.java | 15 ++++++++--- .../com/aolda/itda/config/LoggingFilter.java | 3 --- .../com/aolda/itda/service/AuthService.java | 25 +++++++++++++++++++ 3 files changed, 37 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java index 3a95f8a..b0767e3 100644 --- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java +++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java @@ -11,6 +11,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; +import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -56,9 +57,17 @@ public class AuthInterceptor implements HandlerInterceptor { } /* 프로젝트 리스트 조회 */ - List<String> projects = authService.getProjectsWithUser(Map.of("id", userId, "token", token)) - .stream().map(IdAndNameDTO::getId) - .toList(); + List<String> projects; + if (authService.isAdmin(Map.of("id", userId, "token", token))) { + projects = authService.getAllProjects(token); + } + + 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)); return true; diff --git a/src/main/java/com/aolda/itda/config/LoggingFilter.java b/src/main/java/com/aolda/itda/config/LoggingFilter.java index 0dd73b1..49264ec 100644 --- a/src/main/java/com/aolda/itda/config/LoggingFilter.java +++ b/src/main/java/com/aolda/itda/config/LoggingFilter.java @@ -23,16 +23,13 @@ public class LoggingFilter extends OncePerRequestFilter { // Request Body를 읽을 수 있도록 래핑 ContentCachingRequestWrapper cachingRequest = new ContentCachingRequestWrapper(request); - System.out.println("필터 적용"); filterChain.doFilter(cachingRequest, response); // 로그 기록 logRequest(cachingRequest); - System.out.println("왜 안돼ㅐ"); } private void logRequest(ContentCachingRequestWrapper request) { - System.out.println("되는거 맞아?"); String ip = request.getRemoteAddr(); String method = request.getMethod(); String uri = request.getRequestURI(); diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index 6a25ebc..8bc6836 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -297,6 +297,31 @@ public class AuthService { } + public List<String> 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 (HttpClientErrorException.NotFound e) { + throw new CustomException(ErrorCode.INVALID_TOKEN); + } + + JsonNode node = objectMapper.readTree(res.getBody()); + ArrayNode arrayNode = (ArrayNode) node.get("projects"); + + List<String> lists = new ArrayList<>(); + + for (JsonNode assignment : arrayNode) { + lists.add(assignment.path("id").asText()); + } + + return lists; + + } + public void validateProjectAuth(List<String> projects, String projectId) { if (projects != null && !projects.contains(projectId)) { throw new CustomException(ErrorCode.UNAUTHORIZED_USER); -- GitLab From 1b407531c1208ef0f09867426b7cc75eafd20486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Wed, 26 Mar 2025 23:39:18 +0900 Subject: [PATCH 28/41] =?UTF-8?q?feat:=20=EB=A9=94=EC=9D=B8=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EB=B0=8F=20=EC=A0=91=EA=B7=BC=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=20=ED=94=84=EB=A1=9C=EC=A0=9D=ED=8A=B8=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aolda/itda/config/AuthInterceptor.java | 3 +- .../itda/controller/main/MainController.java | 33 ++++++++++ .../com/aolda/itda/dto/main/MainInfoDTO.java | 20 ++++++ .../com/aolda/itda/service/AuthService.java | 8 ++- .../aolda/itda/service/main/MainService.java | 61 +++++++++++++++++++ 5 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/aolda/itda/controller/main/MainController.java create mode 100644 src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java create mode 100644 src/main/java/com/aolda/itda/service/main/MainService.java diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java index b0767e3..7c9f87f 100644 --- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java +++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java @@ -59,7 +59,8 @@ public class AuthInterceptor implements HandlerInterceptor { /* 프로젝트 리스트 조회 */ List<String> projects; if (authService.isAdmin(Map.of("id", userId, "token", token))) { - projects = authService.getAllProjects(token); + projects = authService.getAllProjects(token).stream().map(IdAndNameDTO::getId) + .toList(); } else { 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 0000000..7b22923 --- /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/dto/main/MainInfoDTO.java b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java new file mode 100644 index 0000000..6de5fdf --- /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/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index 8bc6836..a222e28 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -297,7 +297,7 @@ public class AuthService { } - public List<String> getAllProjects(String token) throws JsonProcessingException { + public List<IdAndNameDTO> getAllProjects(String token) throws JsonProcessingException { String url = keystone + "/projects"; HttpHeaders headers = new HttpHeaders(); headers.set("X-Auth-Token", token); @@ -312,10 +312,12 @@ public class AuthService { JsonNode node = objectMapper.readTree(res.getBody()); ArrayNode arrayNode = (ArrayNode) node.get("projects"); - List<String> lists = new ArrayList<>(); + List<IdAndNameDTO> lists = new ArrayList<>(); for (JsonNode assignment : arrayNode) { - lists.add(assignment.path("id").asText()); + String projectId = assignment.path("id").asText(); + String projectName = assignment.path("name").asText(); + lists.add(new IdAndNameDTO(projectId, projectName)); } return lists; 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 0000000..2c36ac8 --- /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(); + } +} -- GitLab From 017ea5c5ad239d906bdb7f85f9290a01dd99e8d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sat, 29 Mar 2025 11:08:48 +0900 Subject: [PATCH 29/41] =?UTF-8?q?fix:=20=ED=94=84=EB=A1=9C=EC=A0=9D?= =?UTF-8?q?=ED=8A=B8=20=EC=97=AD=ED=95=A0=EA=B3=BC=20=EA=B6=8C=ED=95=9C?= =?UTF-8?q?=EC=9D=B4=20=EC=9D=BC=EC=B9=98=ED=95=98=EC=A7=80=20=EC=95=8A?= =?UTF-8?q?=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/aolda/itda/config/AuthInterceptor.java | 6 +++--- .../java/com/aolda/itda/service/AuthService.java | 13 +++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthInterceptor.java index 7c9f87f..fd78fb6 100644 --- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java +++ b/src/main/java/com/aolda/itda/config/AuthInterceptor.java @@ -43,10 +43,10 @@ public class AuthInterceptor implements HandlerInterceptor { if (projectId != null) { try { - String role = authService.getBestRoleWithinProject(token, projectId).get("role"); - if (!role.equals("admin")) { - log.error("Unauthorized Token for URI {}: {}", request.getRequestURI(), request.getRemoteAddr()); + 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()); diff --git a/src/main/java/com/aolda/itda/service/AuthService.java b/src/main/java/com/aolda/itda/service/AuthService.java index a222e28..4c1740e 100644 --- a/src/main/java/com/aolda/itda/service/AuthService.java +++ b/src/main/java/com/aolda/itda/service/AuthService.java @@ -96,7 +96,6 @@ public class AuthService { try { res = restTemplate.postForEntity(url, requestEntity, Map.class); } catch (Exception e) { - e.printStackTrace(); throw new CustomException(ErrorCode.INVALID_USER_INFO); } Map<String, Object> resToken = (Map<String, Object>) res.getBody().get("token"); @@ -140,7 +139,7 @@ public class AuthService { try { requestEntity = new HttpEntity<>(requestBody, headers); res = restTemplate.postForEntity(url, requestEntity, Map.class); - } catch (RuntimeException e) { + } catch (Exception e) { return null; } @@ -185,8 +184,7 @@ public class AuthService { } catch (HttpClientErrorException.Forbidden e) { return unscopedToken; } - catch (RuntimeException e) { - e.printStackTrace(); + catch (Exception e) { throw new CustomException(ErrorCode.INVALID_TOKEN); } @@ -290,7 +288,7 @@ public class AuthService { ResponseEntity<String> res; try { res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); - } catch (HttpClientErrorException.NotFound e) { + } catch (Exception e) { throw new CustomException(ErrorCode.INVALID_TOKEN); } return objectMapper.readTree(res.getBody()).path("token").path("user").path("id").asText(); @@ -305,7 +303,7 @@ public class AuthService { ResponseEntity<String> res; try { res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); - } catch (HttpClientErrorException.NotFound e) { + } catch (Exception e) { throw new CustomException(ErrorCode.INVALID_TOKEN); } @@ -338,8 +336,7 @@ public class AuthService { ResponseEntity<String> res; try { res = restTemplate.exchange(url, HttpMethod.GET, requestEntity, String.class); - } catch (RuntimeException e) { - e.printStackTrace(); + } catch (Exception e) { return false; } JsonNode node = objectMapper.readTree(res.getBody()).path("role_assignments"); -- GitLab From 1b0c96231a33afa111a7016e5289cfbea0930104 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sat, 29 Mar 2025 11:09:28 +0900 Subject: [PATCH 30/41] =?UTF-8?q?fix:=20=EB=9D=BC=EC=9A=B0=ED=8C=85=20?= =?UTF-8?q?=EC=9E=91=EC=97=85=20=EB=A1=9C=EA=B7=B8=EC=97=90=20=EA=B0=9C?= =?UTF-8?q?=ED=96=89=EC=9D=B4=20=ED=95=9C=20=EB=B2=88=20=EB=8D=94=20?= =?UTF-8?q?=EB=93=A4=EC=96=B4=EA=B0=80=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java index a3a67d2..97f7de4 100644 --- a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java +++ b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java @@ -61,7 +61,7 @@ public class RoutingLogAspect { + "ip: " + routing.getInstanceIp() + "\n" + "port: " + routing.getInstancePort() + "\n" + (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "") - + "caching: " + routing.getCaching() + "\n"; + + "caching: " + routing.getCaching(); /* 로그 엔티티 저장 */ logRepository.save(Log.builder() @@ -97,7 +97,7 @@ public class RoutingLogAspect { + "ip: " + routing.getInstanceIp() + "\n" + "port: " + routing.getInstancePort() + "\n" + (routing.getCertificate() != null ? ("certificateId: " + routing.getCertificate().getCertificateId() + "\n") : "") - + "caching: " + routing.getCaching() + "\n"; + + "caching: " + routing.getCaching(); /* 로그 엔티티 저장 */ logRepository.save(Log.builder() -- GitLab From aa911ace917181294680c8cf68ce89ef7b26fb3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sat, 29 Mar 2025 11:56:18 +0900 Subject: [PATCH 31/41] =?UTF-8?q?fix:=20enum=20type=20=EC=86=8C=EB=AC=B8?= =?UTF-8?q?=EC=9E=90=20=EC=A7=81=EB=A0=AC=ED=99=94=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/aolda/itda/config/LoggingFilter.java | 96 +++++----- .../itda/controller/main/MainController.java | 66 +++---- .../com/aolda/itda/dto/main/MainInfoDTO.java | 40 ++-- .../itda/entity/certificate/Challenge.java | 12 +- .../com/aolda/itda/entity/log/Action.java | 12 +- .../com/aolda/itda/entity/log/ObjectType.java | 12 +- .../aolda/itda/service/main/MainService.java | 122 ++++++------ src/main/resources/logback-spring.xml | 178 +++++++++--------- 8 files changed, 284 insertions(+), 254 deletions(-) diff --git a/src/main/java/com/aolda/itda/config/LoggingFilter.java b/src/main/java/com/aolda/itda/config/LoggingFilter.java index 49264ec..f2330bf 100644 --- a/src/main/java/com/aolda/itda/config/LoggingFilter.java +++ b/src/main/java/com/aolda/itda/config/LoggingFilter.java @@ -1,48 +1,48 @@ -package com.aolda.itda.config; - -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -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; - -@Component -public class LoggingFilter extends OncePerRequestFilter { - - private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); - - @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) { - String ip = request.getRemoteAddr(); - String method = request.getMethod(); - String uri = request.getRequestURI(); - String queryString = request.getQueryString(); - String body = getRequestBody(request); - - logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, Request Body: {}", - ip, method, uri, (queryString != null ? queryString : "None"), - (!body.isEmpty() ? body : "None")); - } - - private String getRequestBody(ContentCachingRequestWrapper request) { - byte[] buf = request.getContentAsByteArray(); - return (buf.length > 0) ? new String(buf) : ""; - } -} +package com.aolda.itda.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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; + +@Component +public class LoggingFilter extends OncePerRequestFilter { + + private static final Logger logger = LoggerFactory.getLogger(LoggingFilter.class); + + @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) { + String ip = request.getRemoteAddr(); + String method = request.getMethod(); + String uri = request.getRequestURI(); + String queryString = request.getQueryString(); + String body = getRequestBody(request); + + logger.info("IP: {}, Method: {}, URI: {}, Query Params: {}, Request Body: {}", + ip, method, uri, (queryString != null ? queryString : "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/controller/main/MainController.java b/src/main/java/com/aolda/itda/controller/main/MainController.java index 7b22923..371cce5 100644 --- a/src/main/java/com/aolda/itda/controller/main/MainController.java +++ b/src/main/java/com/aolda/itda/controller/main/MainController.java @@ -1,33 +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"))); - } - -} +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/dto/main/MainInfoDTO.java b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java index 6de5fdf..cbcdcd5 100644 --- a/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java +++ b/src/main/java/com/aolda/itda/dto/main/MainInfoDTO.java @@ -1,20 +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; - -} +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/entity/certificate/Challenge.java b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java index 14713bf..41f607e 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Challenge.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Challenge.java @@ -1,5 +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 + HTTP, DNS_CLOUDFLARE; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } } diff --git a/src/main/java/com/aolda/itda/entity/log/Action.java b/src/main/java/com/aolda/itda/entity/log/Action.java index a620f87..448a65e 100644 --- a/src/main/java/com/aolda/itda/entity/log/Action.java +++ b/src/main/java/com/aolda/itda/entity/log/Action.java @@ -1,5 +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 + CREATE, UPDATE, DELETE; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } } diff --git a/src/main/java/com/aolda/itda/entity/log/ObjectType.java b/src/main/java/com/aolda/itda/entity/log/ObjectType.java index 5310315..c91f279 100644 --- a/src/main/java/com/aolda/itda/entity/log/ObjectType.java +++ b/src/main/java/com/aolda/itda/entity/log/ObjectType.java @@ -1,5 +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 + ROUTING, CERTIFICATE, FORWARDING; + + @JsonValue + @Override + public String toString() { + return name().toLowerCase(); + } } diff --git a/src/main/java/com/aolda/itda/service/main/MainService.java b/src/main/java/com/aolda/itda/service/main/MainService.java index 2c36ac8..8a4345e 100644 --- a/src/main/java/com/aolda/itda/service/main/MainService.java +++ b/src/main/java/com/aolda/itda/service/main/MainService.java @@ -1,61 +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(); - } -} +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/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index a8acbee..39e9337 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -1,90 +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> - +<?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 -- GitLab From 331b11478b712ac7c3fa1dc2794c3881b53d198d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sat, 29 Mar 2025 12:04:32 +0900 Subject: [PATCH 32/41] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EC=9E=90=20=EA=B2=80=EC=83=89=EC=9D=84=20=EC=9D=BC?= =?UTF-8?q?=EB=B6=80=20=EA=B2=80=EC=83=89=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java index ceba4b2..19feb19 100644 --- a/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java +++ b/src/main/java/com/aolda/itda/repository/log/LogQueryDSL.java @@ -86,7 +86,7 @@ public class LogQueryDSL { /* 사용자 ID 조건 */ if (username != null) { - builder.and(log.user.keystoneUsername.eq(username)); + builder.and(log.user.keystoneUsername.contains(username)); } /* CUD 조건 */ -- GitLab From 88e8e972827daa6d3b8e7fe1330641e42e8e7a89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sat, 29 Mar 2025 13:20:16 +0900 Subject: [PATCH 33/41] =?UTF-8?q?feat:=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=EC=97=90=20=EA=B2=80=EC=83=89=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../forwarding/ForwardingController.java | 5 ++-- .../controller/routing/RoutingController.java | 5 ++-- .../forwarding/ForwardingRepository.java | 4 ++++ .../repository/routing/RoutingRepository.java | 4 ++++ .../service/forwarding/ForwardingService.java | 16 ++++++++++--- .../itda/service/routing/RoutingService.java | 23 ++++++++++++++++--- 6 files changed, 47 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java index 36e6f9a..999707a 100644 --- a/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java +++ b/src/main/java/com/aolda/itda/controller/forwarding/ForwardingController.java @@ -30,8 +30,9 @@ public class ForwardingController { } @GetMapping("/forwardings") - public ResponseEntity<Object> lists(@RequestParam String projectId) { - return ResponseEntity.ok(forwardingService.getForwardings(projectId)); + public ResponseEntity<Object> lists(@RequestParam String projectId, + @RequestParam(required = false) String query) { + return ResponseEntity.ok(forwardingService.getForwardingsWithSearch(projectId, query)); } @PatchMapping("/forwarding") diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java index 4b9d551..c96e910 100644 --- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java +++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java @@ -31,8 +31,9 @@ public class RoutingController { } @GetMapping("/routings") - public ResponseEntity<Object> lists(@RequestParam String projectId) { - return ResponseEntity.ok(routingService.getRoutings(projectId)); + public ResponseEntity<Object> lists(@RequestParam String projectId, + @RequestParam(required = false) String query) { + return ResponseEntity.ok(routingService.getRoutingsWithSearch(projectId, query)); } @PatchMapping("/routing") diff --git a/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java b/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java index c461762..9a330f0 100644 --- a/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java +++ b/src/main/java/com/aolda/itda/repository/forwarding/ForwardingRepository.java @@ -2,6 +2,7 @@ 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; @@ -11,4 +12,7 @@ public interface ForwardingRepository extends JpaRepository<Forwarding, Long> { 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/routing/RoutingRepository.java b/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java index 046296c..44b88ae 100644 --- a/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java +++ b/src/main/java/com/aolda/itda/repository/routing/RoutingRepository.java @@ -3,6 +3,7 @@ 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; @@ -11,4 +12,7 @@ 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/service/forwarding/ForwardingService.java b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java index cae38c3..e6fc9d4 100644 --- a/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java +++ b/src/main/java/com/aolda/itda/service/forwarding/ForwardingService.java @@ -27,6 +27,7 @@ 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 @@ -52,11 +53,20 @@ public class ForwardingService { return forwarding.toForwardingDTO(); } - /* 포트포워딩 목록 조회 */ - public PageResp<ForwardingDTO> getForwardings(String projectId) { + /* 포트포워딩 목록 조회 + 검색 */ + 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.findByProjectIdAndIsDeleted(projectId, false) + .contents(forwardingRepository.findWithSearch(projectId, query, false) .stream() .map(Forwarding::toForwardingDTO) .toList()).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 index 769278d..d01eb02 100644 --- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java +++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java @@ -28,6 +28,7 @@ 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 @@ -52,10 +53,26 @@ public class RoutingService { return routing.toRoutingDTO(); } - /* Routing 목록 조회 */ - public PageResp<RoutingDTO> getRoutings(String projectId) { + /* 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.findByProjectIdAndIsDeleted(projectId, false) + .contents(routingRepository.findWithSearch(projectId, query, false) .stream() .map(Routing::toRoutingDTO) .toList()).build(); -- GitLab From 9662eda4bf9e66027a5cbe31f55ab3721aa676ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sat, 29 Mar 2025 13:54:43 +0900 Subject: [PATCH 34/41] =?UTF-8?q?feat:=20=EC=9D=B8=EC=A6=9D=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=EC=85=89=ED=84=B0=EC=97=90=EC=84=9C=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20=EB=B0=8F=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=97=90=20=EC=82=AC=EC=9A=A9=EC=9E=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{AuthInterceptor.java => AuthFilter.java} | 153 +++++++++--------- .../com/aolda/itda/config/LoggingFilter.java | 15 +- .../java/com/aolda/itda/config/WebConfig.java | 27 +++- 3 files changed, 107 insertions(+), 88 deletions(-) rename src/main/java/com/aolda/itda/config/{AuthInterceptor.java => AuthFilter.java} (71%) diff --git a/src/main/java/com/aolda/itda/config/AuthInterceptor.java b/src/main/java/com/aolda/itda/config/AuthFilter.java similarity index 71% rename from src/main/java/com/aolda/itda/config/AuthInterceptor.java rename to src/main/java/com/aolda/itda/config/AuthFilter.java index fd78fb6..f810c2c 100644 --- a/src/main/java/com/aolda/itda/config/AuthInterceptor.java +++ b/src/main/java/com/aolda/itda/config/AuthFilter.java @@ -1,77 +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.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -@RequiredArgsConstructor -@Component -@Slf4j -public class AuthInterceptor implements HandlerInterceptor { - - private final AuthService authService; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { - 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)); - return true; - - } -} +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 index f2330bf..8c5c2f0 100644 --- a/src/main/java/com/aolda/itda/config/LoggingFilter.java +++ b/src/main/java/com/aolda/itda/config/LoggingFilter.java @@ -1,9 +1,14 @@ 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; @@ -11,11 +16,14 @@ 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) @@ -29,15 +37,16 @@ public class LoggingFilter extends OncePerRequestFilter { logRequest(cachingRequest); } - private void logRequest(ContentCachingRequestWrapper request) { + 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: {}, Request Body: {}", - ip, method, uri, (queryString != null ? queryString : "None"), + 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")); } diff --git a/src/main/java/com/aolda/itda/config/WebConfig.java b/src/main/java/com/aolda/itda/config/WebConfig.java index e3893ae..a3559c4 100644 --- a/src/main/java/com/aolda/itda/config/WebConfig.java +++ b/src/main/java/com/aolda/itda/config/WebConfig.java @@ -3,17 +3,18 @@ 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.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { - private final AuthInterceptor authInterceptor; + private final AuthFilter authFilter; + private final LoggingFilter loggingFilter; @Override public void addCorsMappings(CorsRegistry registry) { // 스프링단에서 cors 설정 @@ -26,13 +27,23 @@ public class WebConfig implements WebMvcConfigurer { ; } - @Override - public void addInterceptors(InterceptorRegistry registry) { - String[] excludeAuth = {"/error", "/api/auth/*" }; - registry.addInterceptor(authInterceptor) - .addPathPatterns("/**") - .excludePathPatterns(excludeAuth); + @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 -- GitLab From 2fedb7cec27a20cd830f8fef5ec6b4f0d07fe7ea Mon Sep 17 00:00:00 2001 From: NaHyun22 <nhle0217@ajou.ac.kr> Date: Mon, 14 Apr 2025 19:30:39 +0900 Subject: [PATCH 35/41] =?UTF-8?q?fix:=20conflict=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../certificate/CertificateController.java | 93 ++++++++++++ .../itda/dto/certificate/CertificateDTO.java | 21 +++ .../itda/entity/certificate/Certificate.java | 9 ++ .../certificate/CertificateRepository.java | 11 ++ .../certificate/CertificateService.java | 137 ++++++++++++++++++ 5 files changed, 271 insertions(+) create mode 100644 src/main/java/com/aolda/itda/controller/certificate/CertificateController.java create mode 100644 src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java create mode 100644 src/main/java/com/aolda/itda/service/certificate/CertificateService.java 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 0000000..b6384c9 --- /dev/null +++ b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java @@ -0,0 +1,93 @@ +package com.aolda.itda.controller.certificate; + +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 + * Body: CertificateDTO + */ + @PostMapping("/certificate") + public ResponseEntity<Object> create(@RequestParam String projectId, + @RequestBody CertificateDTO dto, + HttpServletRequest request) { + certificateService.createCertificate( + projectId, + dto, + (List<String>) request.getAttribute("projects") // Forwarding과 동일하게 + ); + return ResponseEntity.ok().build(); + } + + /** + * 인증서 단건 조회 + * GET /api/certificate?certificateId=xxx + */ + @GetMapping("/certificate") + public ResponseEntity<Object> view(@RequestParam Long certificateId, + HttpServletRequest request) { + return ResponseEntity.ok( + certificateService.getCertificate( + certificateId, + (List<String>) request.getAttribute("projects") + ) + ); + } + + /** + * 인증서 목록 조회 + * GET /api/certificates?projectId=xxx + */ + @GetMapping("/certificates") + public ResponseEntity<Object> lists(@RequestParam String projectId) { + return ResponseEntity.ok( + certificateService.getCertificates(projectId) + ); + } + + /** + * 인증서 수정 + * PATCH /api/certificate?certificateId=xxx + * Body: CertificateDTO + */ + @PatchMapping("/certificate") + public ResponseEntity<Object> 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<Object> 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/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java new file mode 100644 index 0000000..35aad94 --- /dev/null +++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java @@ -0,0 +1,21 @@ +package com.aolda.itda.dto.certificate; + +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.*; + +import java.time.LocalDateTime; + +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public class CertificateDTO { + + private Long certificateId; + private String projectId; + private String domain; + private LocalDateTime expiredAt; // 필요 시 + private Boolean isDeleted; + private String description; // 추가 설명 +} diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java index 85b1f29..469ea6a 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -49,4 +49,13 @@ public class Certificate extends BaseTimeEntity { public String formatDomain() { return domain == null ? null : domain.replace("*", "_"); } + + public void setIsDeleted(boolean b) { + } + + public void setDomain(String domain) { + } + + public void setDescription(String description) { + } } diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java index 392c90b..1ce5c1f 100644 --- a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java +++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java @@ -1,7 +1,18 @@ package com.aolda.itda.repository.certificate; import com.aolda.itda.entity.certificate.Certificate; +import com.aolda.itda.entity.forwarding.Forwarding; import org.springframework.data.jpa.repository.JpaRepository; +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); + } 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 0000000..5c0dbaf --- /dev/null +++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java @@ -0,0 +1,137 @@ +package com.aolda.itda.service.certificate; + +import com.aolda.itda.dto.PageResp; +import com.aolda.itda.dto.certificate.CertificateDTO; +import com.aolda.itda.entity.certificate.Certificate; +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.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@Transactional +@RequiredArgsConstructor +@Slf4j +public class CertificateService { + + private final CertificateRepository certificateRepository; + private final AuthService authService; + + /* 인증서 하나 조회 */ + public CertificateDTO getCertificate(Long certificateId, List<String> projects) { + Certificate certificate = certificateRepository + .findByCertificateIdAndIsDeleted(certificateId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + // 여기서는 ErrorCode.NOT_FOUND_CERTIFICATE 등 새로운 코드로 교체 가능 + + // 프로젝트 권한 검증 + authService.validateProjectAuth(projects, certificate.getProjectId()); + + return toDTO(certificate); + } + + /* 인증서 목록 조회 */ + public PageResp<CertificateDTO> getCertificates(String projectId) { + List<CertificateDTO> list = certificateRepository + .findByProjectIdAndIsDeleted(projectId, false) + .stream() + .map(this::toDTO) + .toList(); + + return PageResp.<CertificateDTO>builder() + .contents(list) + .build(); + } + + /* 인증서 생성 */ + public CertificateDTO createCertificate(String projectId, CertificateDTO dto, List<String> projects) { + // 프로젝트 권한 검증 + authService.validateProjectAuth(projects, projectId); + + // DTO 유효성 검사 + validateDTO(dto); + + Certificate certificate = Certificate.builder() + .projectId(projectId) + .domain(dto.getDomain()) + .description(dto.getDescription()) + .isDeleted(false) + .build(); + + certificateRepository.save(certificate); + + // 생성 로직 (certbot 호출 등) 필요 시 추가 + + return toDTO(certificate); + } + + /* 인증서 수정 */ + public CertificateDTO editCertificate(Long certificateId, CertificateDTO dto, List<String> projects) { + Certificate certificate = certificateRepository + .findByCertificateIdAndIsDeleted(certificateId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + + // 프로젝트 권한 검증 + authService.validateProjectAuth(projects, certificate.getProjectId()); + + // 필요한 필드만 수정 + if (dto.getDomain() != null) { + certificate.setDomain(dto.getDomain()); + } + if (dto.getDescription() != null) { + certificate.setDescription(dto.getDescription()); + } + // 기타 수정할 필드가 있다면 추가 + + // DB 저장 + certificateRepository.save(certificate); + + // 수정 로직(certbot 재발급 등) 필요 시 추가 + + return toDTO(certificate); + } + + /* 인증서 삭제 (soft delete) */ + public void deleteCertificate(Long certificateId, List<String> projects) { + Certificate certificate = certificateRepository + .findByCertificateIdAndIsDeleted(certificateId, false) + .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + + // 권한 검증 + authService.validateProjectAuth(projects, certificate.getProjectId()); + + // soft delete + certificate.setIsDeleted(true); + certificateRepository.save(certificate); + + // (추가) 파일 제거 / certbot revoke 등 로직 필요 시 + } + + /* DTO 유효성 검사 */ + private void validateDTO(CertificateDTO dto) { + for (ConstraintViolation<CertificateDTO> violation + : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) { + throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage()); + } + } + + /* Entity -> DTO 변환 */ + private CertificateDTO toDTO(Certificate certificate) { + return CertificateDTO.builder() + .certificateId(certificate.getCertificateId()) + .projectId(certificate.getProjectId()) + .domain(certificate.getDomain()) + .description(certificate.getDescription()) + .isDeleted(certificate.getIsDeleted()) + .expiredAt(certificate.getExpiredAt()) + .build(); + } +} -- GitLab From fee78971e94dc1854499e6d1fed2a2dc5f456768 Mon Sep 17 00:00:00 2001 From: NaHyun22 <nhle0217@ajou.ac.kr> Date: Wed, 30 Apr 2025 19:10:55 +0900 Subject: [PATCH 36/41] =?UTF-8?q?feat:=20certificate=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=EC=84=9C=20=EB=B0=9C=EA=B8=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../certificate/CertificateController.java | 2 + .../itda/dto/certificate/CertificateDTO.java | 18 ++-- .../itda/entity/certificate/Certificate.java | 5 +- .../certificate/CertificateService.java | 88 ++++++++++++++++++- 4 files changed, 100 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java index b6384c9..cf5e160 100644 --- a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java +++ b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java @@ -25,6 +25,7 @@ public class CertificateController { public ResponseEntity<Object> create(@RequestParam String projectId, @RequestBody CertificateDTO dto, HttpServletRequest request) { + System.out.println("1"); certificateService.createCertificate( projectId, dto, @@ -89,5 +90,6 @@ public class CertificateController { ); return ResponseEntity.ok().build(); } + /*인증서 검색*/ } diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java index 35aad94..e37b75e 100644 --- a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java +++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java @@ -1,5 +1,6 @@ package com.aolda.itda.dto.certificate; +import com.aolda.itda.entity.certificate.Challenge; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.*; @@ -12,10 +13,15 @@ import java.time.LocalDateTime; @JsonInclude(JsonInclude.Include.NON_NULL) public class CertificateDTO { - private Long certificateId; - private String projectId; - private String domain; - private LocalDateTime expiredAt; // 필요 시 - private Boolean isDeleted; - private String description; // 추가 설명 + private Long certificateId; // 인증서 고유 ID + private String projectId; // 프로젝트 식별자 + private String domain; // SSL 인증받을 도메인 주소 + private String email; // 도메인 소유자의 이메일 + private LocalDateTime expiredAt; // 인증서 만료일 + private Challenge challenge; // 챌린지 방식 (HTTP, DNS_CLOUDFLARE) + private Boolean isDeleted; // 삭제 여부 (soft delete) + private String description; // 설명 (필요시 자유롭게 작성) } +/* 이메일, 챌린지 방식, http인지 dns인지... "*/ +//도메인, 소유자 이메일, 챌린지 방식 확실하게 들어가야함!! +/*erd 보고 만들기*/ diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java index 469ea6a..489138f 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -23,10 +23,6 @@ public class Certificate extends BaseTimeEntity { @Column(nullable = false) private Long certificateId; - @ManyToOne - @JoinColumn(nullable = false, name = "user_id") - private User user; - @Column(length = 64) private String projectId; @@ -57,5 +53,6 @@ public class Certificate extends BaseTimeEntity { } public void setDescription(String description) { + } } diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java index 5c0dbaf..f6f83bb 100644 --- a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java +++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java @@ -3,6 +3,7 @@ package com.aolda.itda.service.certificate; import com.aolda.itda.dto.PageResp; import com.aolda.itda.dto.certificate.CertificateDTO; import com.aolda.itda.entity.certificate.Certificate; +import com.aolda.itda.entity.certificate.Challenge; import com.aolda.itda.exception.CustomException; import com.aolda.itda.exception.ErrorCode; import com.aolda.itda.repository.certificate.CertificateRepository; @@ -14,6 +15,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; @Service @@ -52,10 +54,12 @@ public class CertificateService { } /* 인증서 생성 */ - public CertificateDTO createCertificate(String projectId, CertificateDTO dto, List<String> projects) { + /*public CertificateDTO createCertificate(String projectId, + CertificateDTO dto, + List<String> projects) { // 프로젝트 권한 검증 authService.validateProjectAuth(projects, projectId); - + System.out.println("2"); // DTO 유효성 검사 validateDTO(dto); @@ -67,9 +71,52 @@ public class CertificateService { .build(); certificateRepository.save(certificate); - + System.out.println("3"); // 생성 로직 (certbot 호출 등) 필요 시 추가 + return toDTO(certificate); + }*/ + /* 인증서 생성 + lego 호출 */ + public CertificateDTO createCertificate(String projectId, CertificateDTO dto, List<String> projects) { + + // 1) 권한 체크 + authService.validateProjectAuth(projects, projectId); + + // 2) DTO 검증 + validateDTO(dto); + + // 3) lego 명령어 구성 + ProcessBuilder pb = buildLegoProcess(dto); + + // 4) lego 실행 + int exitCode; + try { + Process process = pb.start(); + exitCode = process.waitFor(); + if (exitCode != 0) { + String err = new String(process.getErrorStream().readAllBytes()); + log.error("[lego-error] {}", err); + throw new CustomException(ErrorCode.FAIL_CREATE_CONF, + "lego 오류: " + err); + } + } catch (Exception e) { + log.error("[lego-exec] {}", e.getMessage()); + throw new CustomException(ErrorCode.FAIL_CREATE_CONF, + "lego 실행 실패"); + } + + // 5) 엔티티 저장 + Certificate certificate = Certificate.builder() + .projectId(projectId) + .domain(dto.getDomain()) + .email(dto.getEmail()) + .challenge(dto.getChallenge()) + .expiredAt(dto.getExpiredAt()) // 필요 시 lego 출력 파싱 + .isDeleted(false) + .description(dto.getDescription()) + .build(); + + certificateRepository.save(certificate); return toDTO(certificate); } @@ -134,4 +181,39 @@ public class CertificateService { .expiredAt(certificate.getExpiredAt()) .build(); } + + + + /* lego ProcessBuilder 생성 */ + private ProcessBuilder buildLegoProcess(CertificateDTO dto) { + + String basePath = "/data/lego"; // 인증서 저장 루트(볼륨) + 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(basePath); + + if (dto.getChallenge() == Challenge.HTTP) { + cmd.add("--http"); + cmd.add("--http.webroot"); + cmd.add("/data/letsencrypt-acme-challenge"); + } else if (dto.getChallenge() == Challenge.DNS_CLOUDFLARE) { + cmd.add("--dns"); + cmd.add("cloudflare"); + // CLOUDFLARE_API_TOKEN 환경변수를 컨테이너에 세팅했다고 가정 + } + + cmd.add("--domains"); cmd.add(dto.getDomain()); + cmd.add("run"); // 최초 발급(run) / renew(갱신) + + return new ProcessBuilder(cmd) + .redirectErrorStream(true); + } } + + + +// 여기서 매소드를 create로 해서 lego --accept-tos --email "email@example.com" --http --http.webroot data/letsencrypt-acme-challenge --path /data/lego --domains www.example.com run +// 이거 이메일 . 도메인 으로 넣어서 실제 인증서 연동하기!!! +// 그리고 Dto에 관리자 이메일, 인증서 완료일, 챌린지 방식 추가하기 \ No newline at end of file -- GitLab From 0b97807cf4cfa16a5e971d01efb26f41401e49de Mon Sep 17 00:00:00 2001 From: NaHyun22 <nhle0217@ajou.ac.kr> Date: Wed, 7 May 2025 05:57:24 +0900 Subject: [PATCH 37/41] feat: lego --- .../certificate/CertificateController.java | 47 ++-- .../itda/dto/certificate/CertificateDTO.java | 7 +- .../itda/entity/certificate/Certificate.java | 18 +- .../certificate/CertificateRepository.java | 11 + .../certificate/CertificateService.java | 262 +++++++++--------- 5 files changed, 189 insertions(+), 156 deletions(-) diff --git a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java index cf5e160..d08968b 100644 --- a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java +++ b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java @@ -1,5 +1,6 @@ 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; @@ -19,17 +20,17 @@ public class CertificateController { /** * 인증서 생성 * POST /api/certificate?projectId=xxx - * Body: CertificateDTO */ @PostMapping("/certificate") - public ResponseEntity<Object> create(@RequestParam String projectId, - @RequestBody CertificateDTO dto, - HttpServletRequest request) { - System.out.println("1"); + public ResponseEntity<Void> create( + @RequestParam String projectId, + @RequestBody CertificateDTO dto, + HttpServletRequest request + ) { certificateService.createCertificate( projectId, dto, - (List<String>) request.getAttribute("projects") // Forwarding과 동일하게 + (List<String>) request.getAttribute("projects") ); return ResponseEntity.ok().build(); } @@ -39,8 +40,10 @@ public class CertificateController { * GET /api/certificate?certificateId=xxx */ @GetMapping("/certificate") - public ResponseEntity<Object> view(@RequestParam Long certificateId, - HttpServletRequest request) { + public ResponseEntity<CertificateDTO> view( + @RequestParam Long certificateId, + HttpServletRequest request + ) { return ResponseEntity.ok( certificateService.getCertificate( certificateId, @@ -50,25 +53,29 @@ public class CertificateController { } /** - * 인증서 목록 조회 - * GET /api/certificates?projectId=xxx + * 인증서 목록 조회 (domain 필터링 optional) + * GET /api/certificates?projectId=xxx&domain=foo */ @GetMapping("/certificates") - public ResponseEntity<Object> lists(@RequestParam String projectId) { + public ResponseEntity<PageResp<CertificateDTO>> lists( + @RequestParam String projectId, + @RequestParam(required = false) String domain + ) { return ResponseEntity.ok( - certificateService.getCertificates(projectId) + certificateService.getCertificates(projectId, domain) ); } /** * 인증서 수정 * PATCH /api/certificate?certificateId=xxx - * Body: CertificateDTO */ @PatchMapping("/certificate") - public ResponseEntity<Object> edit(@RequestParam Long certificateId, - @RequestBody CertificateDTO dto, - HttpServletRequest request) { + public ResponseEntity<Void> edit( + @RequestParam Long certificateId, + @RequestBody CertificateDTO dto, + HttpServletRequest request + ) { certificateService.editCertificate( certificateId, dto, @@ -82,14 +89,14 @@ public class CertificateController { * DELETE /api/certificate?certificateId=xxx */ @DeleteMapping("/certificate") - public ResponseEntity<Object> delete(@RequestParam Long certificateId, - HttpServletRequest request) { + 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/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java index e37b75e..dcdefcf 100644 --- a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java +++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java @@ -14,14 +14,17 @@ import java.time.LocalDateTime; public class CertificateDTO { private Long certificateId; // 인증서 고유 ID - private String projectId; // 프로젝트 식별자 +// private String projectId; // 프로젝트 식별자 private String domain; // SSL 인증받을 도메인 주소 private String email; // 도메인 소유자의 이메일 private LocalDateTime expiredAt; // 인증서 만료일 + private LocalDateTime createdAt; // 인증서 생성일 + private LocalDateTime updatedAt; // 인증서 업데이트일 private Challenge challenge; // 챌린지 방식 (HTTP, DNS_CLOUDFLARE) private Boolean isDeleted; // 삭제 여부 (soft delete) - private String description; // 설명 (필요시 자유롭게 작성) + private String apiToken; } /* 이메일, 챌린지 방식, http인지 dns인지... "*/ //도메인, 소유자 이메일, 챌린지 방식 확실하게 들어가야함!! /*erd 보고 만들기*/ +//Challenge 키는 따로 (private으로 api 키 받기) \ No newline at end of file diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java index 489138f..6f52656 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -32,15 +32,15 @@ public class Certificate extends BaseTimeEntity { @Column(length = 64) private String email; - private LocalDateTime expiredAt; + private LocalDateTime expiredAt; //인증서 만료일 + private LocalDateTime createdAt; + private LocalDateTime updatedAt; @Enumerated(EnumType.STRING) private Challenge challenge; private Boolean isDeleted; - @Column(length = 256) - private String description; public String formatDomain() { return domain == null ? null : domain.replace("*", "_"); @@ -52,7 +52,17 @@ public class Certificate extends BaseTimeEntity { public void setDomain(String domain) { } - public void setDescription(String description) { + // public void setDescription(String description) { +// +// } + @Transient + private String apiToken; + public void setExpiredAt(LocalDateTime localDateTime) { + + } + + public void setEmail(String email) { } } + diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java index 1ce5c1f..b8cc148 100644 --- a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java +++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java @@ -4,6 +4,7 @@ import com.aolda.itda.entity.certificate.Certificate; import com.aolda.itda.entity.forwarding.Forwarding; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.LocalDateTime; import java.util.List; import java.util.Optional; @@ -15,4 +16,14 @@ public interface CertificateRepository extends JpaRepository<Certificate, Long> // 프로젝트별 목록 조회 (Soft Delete 고려) List<Certificate> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted); + // 만료일이 주어진 날짜 이전인(=만료 30일 이내) 인증서 조회 + //List<Certificate> findByExpiredAtBeforeAndIsDeleted(LocalDateTime date, Boolean isDeleted); + + // 1) domain 필터링용 메서드 + List<Certificate> findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted( + String projectId, String domain, Boolean isDeleted); + + // 3) 만료 30일 이내 대상 조회 + List<Certificate> findByExpiredAtBeforeAndIsDeleted( + LocalDateTime date, Boolean isDeleted); } diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java index f6f83bb..92d4c83 100644 --- a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java +++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java @@ -12,9 +12,14 @@ import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +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.List; @@ -27,193 +32,190 @@ public class CertificateService { private final CertificateRepository certificateRepository; private final AuthService authService; - /* 인증서 하나 조회 */ + /** 1) 단건 조회 **/ public CertificateDTO getCertificate(Long certificateId, List<String> projects) { - Certificate certificate = certificateRepository + Certificate cert = certificateRepository .findByCertificateIdAndIsDeleted(certificateId, false) .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); - // 여기서는 ErrorCode.NOT_FOUND_CERTIFICATE 등 새로운 코드로 교체 가능 - - // 프로젝트 권한 검증 - authService.validateProjectAuth(projects, certificate.getProjectId()); - - return toDTO(certificate); + authService.validateProjectAuth(projects, cert.getProjectId()); + return toDTO(cert); } - /* 인증서 목록 조회 */ - public PageResp<CertificateDTO> getCertificates(String projectId) { - List<CertificateDTO> list = certificateRepository - .findByProjectIdAndIsDeleted(projectId, false) - .stream() + /** 1) 목록 조회 (domain 필터 optional) **/ + public PageResp<CertificateDTO> getCertificates(String projectId, String domain) { + List<Certificate> list; + if (domain != null && !domain.isBlank()) { + list = certificateRepository + .findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted( + projectId, domain, false); + } else { + list = certificateRepository + .findByProjectIdAndIsDeleted(projectId, false); + } + List<CertificateDTO> dtos = list.stream() .map(this::toDTO) .toList(); - return PageResp.<CertificateDTO>builder() - .contents(list) + .contents(dtos) .build(); } - /* 인증서 생성 */ - /*public CertificateDTO createCertificate(String projectId, + /** 2) 생성: expiredAt 자동 90일 설정 + 로깅 **/ + public CertificateDTO createCertificate(String projectId, CertificateDTO dto, List<String> projects) { - // 프로젝트 권한 검증 - authService.validateProjectAuth(projects, projectId); - System.out.println("2"); - // DTO 유효성 검사 - validateDTO(dto); - - Certificate certificate = Certificate.builder() - .projectId(projectId) - .domain(dto.getDomain()) - .description(dto.getDescription()) - .isDeleted(false) - .build(); - - certificateRepository.save(certificate); - System.out.println("3"); - // 생성 로직 (certbot 호출 등) 필요 시 추가 - - return toDTO(certificate); - }*/ - /* 인증서 생성 + lego 호출 */ - public CertificateDTO createCertificate(String projectId, CertificateDTO dto, List<String> projects) { - - // 1) 권한 체크 + log.info("createCertificate start (project={})", projectId); authService.validateProjectAuth(projects, projectId); - - // 2) DTO 검증 validateDTO(dto); - // 3) lego 명령어 구성 - ProcessBuilder pb = buildLegoProcess(dto); + // 발급 + executeLego(dto); + log.info("certificate issued for domain={}", dto.getDomain()); - // 4) lego 실행 - int exitCode; - try { - Process process = pb.start(); - exitCode = process.waitFor(); - if (exitCode != 0) { - String err = new String(process.getErrorStream().readAllBytes()); - log.error("[lego-error] {}", err); - throw new CustomException(ErrorCode.FAIL_CREATE_CONF, - "lego 오류: " + err); - } - } catch (Exception e) { - log.error("[lego-exec] {}", e.getMessage()); - throw new CustomException(ErrorCode.FAIL_CREATE_CONF, - "lego 실행 실패"); - } - - // 5) 엔티티 저장 - Certificate certificate = Certificate.builder() + // 엔티티 저장 (expiredAt 기본 90일 뒤) + Certificate cert = Certificate.builder() .projectId(projectId) .domain(dto.getDomain()) .email(dto.getEmail()) .challenge(dto.getChallenge()) - .expiredAt(dto.getExpiredAt()) // 필요 시 lego 출력 파싱 + .expiredAt(LocalDateTime.now().plusDays(90)) .isDeleted(false) - .description(dto.getDescription()) + .apiToken(dto.getApiToken()) .build(); + certificateRepository.save(cert); + log.info("certificate saved (id={}, domain={})", + cert.getCertificateId(), cert.getDomain()); - certificateRepository.save(certificate); - return toDTO(certificate); + return toDTO(cert); } - /* 인증서 수정 */ - public CertificateDTO editCertificate(Long certificateId, CertificateDTO dto, List<String> projects) { - Certificate certificate = certificateRepository + /** 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()); - // 프로젝트 권한 검증 - authService.validateProjectAuth(projects, certificate.getProjectId()); - - // 필요한 필드만 수정 - if (dto.getDomain() != null) { - certificate.setDomain(dto.getDomain()); - } - if (dto.getDescription() != null) { - certificate.setDescription(dto.getDescription()); - } - // 기타 수정할 필드가 있다면 추가 - - // DB 저장 - certificateRepository.save(certificate); + if (dto.getDomain() != null) cert.setDomain(dto.getDomain()); + if (dto.getEmail() != null) cert.setEmail(dto.getEmail()); - // 수정 로직(certbot 재발급 등) 필요 시 추가 - - return toDTO(certificate); + certificateRepository.save(cert); + log.info("certificate edited (id={}, domain={})", + cert.getCertificateId(), cert.getDomain()); + return toDTO(cert); } - /* 인증서 삭제 (soft delete) */ + /** 4) 삭제 + 로깅 **/ public void deleteCertificate(Long certificateId, List<String> projects) { - Certificate certificate = certificateRepository + Certificate cert = certificateRepository .findByCertificateIdAndIsDeleted(certificateId, false) .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_FORWARDING)); + authService.validateProjectAuth(projects, cert.getProjectId()); - // 권한 검증 - authService.validateProjectAuth(projects, certificate.getProjectId()); - - // soft delete - certificate.setIsDeleted(true); - certificateRepository.save(certificate); + cert.setIsDeleted(true); + certificateRepository.save(cert); + log.info("certificate deleted (id={}, domain={})", + cert.getCertificateId(), cert.getDomain()); + } - // (추가) 파일 제거 / certbot revoke 등 로직 필요 시 + /** 3) 만료 30일 전 자동 갱신 배치 **/ + @Scheduled(cron = "0 0 3 * * *", zone = "Asia/Seoul") + public void renewExpiringCertificates() { + LocalDateTime threshold = LocalDateTime.now().plusDays(30); + List<Certificate> expiring = certificateRepository + .findByExpiredAtBeforeAndIsDeleted(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.setExpiredAt(LocalDateTime.now().plusDays(90)); + certificateRepository.save(cert); + log.info("renewed (id={}, newExpiry={})", + cert.getCertificateId(), cert.getExpiredAt()); + } catch (Exception e) { + log.error("failed to renew (id={}, domain={}): {}", + cert.getCertificateId(), cert.getDomain(), e.getMessage()); + } + } } - /* DTO 유효성 검사 */ + /** DTO 유효성 검사 **/ private void validateDTO(CertificateDTO dto) { - for (ConstraintViolation<CertificateDTO> violation - : Validation.buildDefaultValidatorFactory().getValidator().validate(dto)) { - throw new CustomException(ErrorCode.INVALID_CONF_INPUT, violation.getMessage()); + for (ConstraintViolation<CertificateDTO> v : + Validation.buildDefaultValidatorFactory() + .getValidator().validate(dto)) { + throw new CustomException(ErrorCode.INVALID_CONF_INPUT, v.getMessage()); } } - /* Entity -> DTO 변환 */ - private CertificateDTO toDTO(Certificate certificate) { + /** Entity→DTO 변환 **/ + private CertificateDTO toDTO(Certificate cert) { return CertificateDTO.builder() - .certificateId(certificate.getCertificateId()) - .projectId(certificate.getProjectId()) - .domain(certificate.getDomain()) - .description(certificate.getDescription()) - .isDeleted(certificate.getIsDeleted()) - .expiredAt(certificate.getExpiredAt()) + .certificateId(cert.getCertificateId()) + + .domain(cert.getDomain()) + .email(cert.getEmail()) + .challenge(cert.getChallenge()) + .expiredAt(cert.getExpiredAt()) + .createdAt(cert.getCreatedAt()) + .updatedAt(cert.getUpdatedAt()) + .isDeleted(cert.getIsDeleted()) + .apiToken(cert.getApiToken()) .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 필요"); + } - - /* lego ProcessBuilder 생성 */ - private ProcessBuilder buildLegoProcess(CertificateDTO dto) { - - String basePath = "/data/lego"; // 인증서 저장 루트(볼륨) 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(basePath); + 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 if (dto.getChallenge() == Challenge.DNS_CLOUDFLARE) { - cmd.add("--dns"); - cmd.add("cloudflare"); - // CLOUDFLARE_API_TOKEN 환경변수를 컨테이너에 세팅했다고 가정 + 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"); // 최초 발급(run) / renew(갱신) + cmd.add("--domains"); cmd.add(dto.getDomain()); + cmd.add("run"); - return new ProcessBuilder(cmd) + 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()); + } + } -// 여기서 매소드를 create로 해서 lego --accept-tos --email "email@example.com" --http --http.webroot data/letsencrypt-acme-challenge --path /data/lego --domains www.example.com run -// 이거 이메일 . 도메인 으로 넣어서 실제 인증서 연동하기!!! -// 그리고 Dto에 관리자 이메일, 인증서 완료일, 챌린지 방식 추가하기 \ No newline at end of file +} -- GitLab From 790875fc36b0a21c37dd959a0463c8ca6b142713 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Wed, 7 May 2025 18:14:51 +0900 Subject: [PATCH 38/41] =?UTF-8?q?fix:=20conf=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EC=9E=91=EC=84=B1=EC=9D=B4=20=EC=A0=9C=EB=8C=80=EB=A1=9C=20?= =?UTF-8?q?=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8D=98=20=EB=AC=B8=EC=A0=9C=20?= =?UTF-8?q?=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/routing/RoutingController.java | 3 ++- .../com/aolda/itda/exception/ErrorCode.java | 4 ++- .../itda/service/routing/RoutingService.java | 25 ++++++++++++++----- .../aolda/itda/template/OptionTemplate.java | 2 +- .../aolda/itda/template/RoutingTemplate.java | 4 +-- 5 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java index c96e910..d15805a 100644 --- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java +++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java @@ -8,6 +8,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import java.io.IOException; import java.util.List; @RestController @@ -19,7 +20,7 @@ public class RoutingController { @PostMapping("/routing") public ResponseEntity<Object> create(@RequestParam String projectId, - @RequestBody RoutingDTO dto) { + @RequestBody RoutingDTO dto) throws IOException { routingService.createRouting(projectId, dto); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/aolda/itda/exception/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java index 3e4b0be..d75235d 100644 --- a/src/main/java/com/aolda/itda/exception/ErrorCode.java +++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java @@ -41,7 +41,9 @@ public enum ErrorCode { FAIL_NGINX_CONF_RELOAD(HttpStatus.BAD_REQUEST, "Nginx 재시작에 실패했습니다"), FAIL_DELETE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 삭제하지 못했습니다"), - FAIL_ROLL_BACK(HttpStatus.BAD_REQUEST, "롤백 실패"); + FAIL_ROLL_BACK(HttpStatus.BAD_REQUEST, "롤백 실패"), + + FAIL_CREATE_CERT(HttpStatus.BAD_REQUEST, "인증서 생성에 실패했습니다"); private final HttpStatus status; private final String message; 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 d01eb02..90424a3 100644 --- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java +++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java @@ -16,6 +16,7 @@ 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; @@ -79,7 +80,7 @@ public class RoutingService { } /* Routing 생성 */ - public RoutingDTO createRouting(String projectId, RoutingDTO dto) { + public RoutingDTO createRouting(String projectId, RoutingDTO dto) throws IOException { /* 입력 DTO 검증 */ validateDTO(dto); @@ -129,7 +130,7 @@ public class RoutingService { bw.flush(); bw.close(); } catch (Exception e) { - if (file.delete()) { + if (!file.delete()) { throw new CustomException(ErrorCode.FAIL_DELETE_CONF); } throw new CustomException(ErrorCode.FAIL_CREATE_CONF, "포트포워딩 Conf 파일을 작성하지 못했습니다"); @@ -139,8 +140,20 @@ public class RoutingService { String url = "http://nginx:8081/nginx-api/test"; try { restTemplate.getForEntity(url, String.class); - } catch (Exception e) { - if (file.delete()) { + + } 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); @@ -151,8 +164,8 @@ public class RoutingService { try { restTemplate.getForEntity(url, String.class); } catch (Exception e) { - if (file.delete()) { - throw new CustomException(ErrorCode.FAIL_NGINX_CONF_TEST, "(롤백 실패)"); + if (!file.delete()) { + throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD, "(롤백 실패)"); } throw new CustomException(ErrorCode.FAIL_NGINX_CONF_RELOAD); } diff --git a/src/main/java/com/aolda/itda/template/OptionTemplate.java b/src/main/java/com/aolda/itda/template/OptionTemplate.java index 945b666..add4dfc 100644 --- a/src/main/java/com/aolda/itda/template/OptionTemplate.java +++ b/src/main/java/com/aolda/itda/template/OptionTemplate.java @@ -6,7 +6,7 @@ import org.springframework.stereotype.Component; public class OptionTemplate { public String getSSL(String certificateDomain) { - return "\nconf.d/include/letsencrypt-acme-challenge.conf;\n" + + 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"; diff --git a/src/main/java/com/aolda/itda/template/RoutingTemplate.java b/src/main/java/com/aolda/itda/template/RoutingTemplate.java index 46e6116..8a53785 100644 --- a/src/main/java/com/aolda/itda/template/RoutingTemplate.java +++ b/src/main/java/com/aolda/itda/template/RoutingTemplate.java @@ -19,11 +19,11 @@ public class RoutingTemplate { + "listen 80;\n" + "listen [::]:80;\n" + (dto.getCertificateId() == -1 ? "" : - "listen 443 ssl;\n listen [::]:443 ssl;\n") + "listen 443 ssl;\nlisten [::]:443 ssl;\n") + "server_name " + dto.getDomain() + ";\n" + (dto.getCertificateId() == -1 ? "" : optionTemplate.getSSL(certificateDomain)) - + (dto.getCaching() ? "" : + + (!dto.getCaching() ? "" : optionTemplate.getAssetCaching() ) + "proxy_set_header Upgrade $http_upgrade;\n" -- GitLab From f9aae944798944fb064a90f5d2e127b6aa1f1475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Wed, 7 May 2025 19:02:39 +0900 Subject: [PATCH 39/41] =?UTF-8?q?fix:=20expiredAt=EC=97=90=EC=84=9C=20expi?= =?UTF-8?q?resAt=EC=9C=BC=EB=A1=9C=20=ED=95=84=EB=93=9C=EB=AA=85=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../itda/dto/certificate/CertificateDTO.java | 4 ++-- .../itda/entity/certificate/Certificate.java | 23 +++++-------------- .../certificate/CertificateService.java | 10 ++++---- 3 files changed, 13 insertions(+), 24 deletions(-) diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java index dcdefcf..e2350cc 100644 --- a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java +++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java @@ -13,11 +13,11 @@ import java.time.LocalDateTime; @JsonInclude(JsonInclude.Include.NON_NULL) public class CertificateDTO { - private Long certificateId; // 인증서 고유 ID + private Long id; // 인증서 고유 ID // private String projectId; // 프로젝트 식별자 private String domain; // SSL 인증받을 도메인 주소 private String email; // 도메인 소유자의 이메일 - private LocalDateTime expiredAt; // 인증서 만료일 + private LocalDateTime expiresAt; // 인증서 만료일 private LocalDateTime createdAt; // 인증서 생성일 private LocalDateTime updatedAt; // 인증서 업데이트일 private Challenge challenge; // 챌린지 방식 (HTTP, DNS_CLOUDFLARE) diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java index 6f52656..eaab407 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -3,10 +3,7 @@ package com.aolda.itda.entity.certificate; 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.*; import java.time.LocalDateTime; @@ -26,13 +23,16 @@ public class Certificate extends BaseTimeEntity { @Column(length = 64) private String projectId; + @Setter @Column(length = 64) private String domain; @Column(length = 64) + @Setter private String email; - private LocalDateTime expiredAt; //인증서 만료일 + @Setter + private LocalDateTime expiresAt; //인증서 만료일 private LocalDateTime createdAt; private LocalDateTime updatedAt; @@ -47,22 +47,11 @@ public class Certificate extends BaseTimeEntity { } public void setIsDeleted(boolean b) { + this.isDeleted = b; } - public void setDomain(String domain) { - } - - // public void setDescription(String description) { -// -// } @Transient private String apiToken; - public void setExpiredAt(LocalDateTime localDateTime) { - - } - - public void setEmail(String email) { - } } diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java index 92d4c83..250e4d2 100644 --- a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java +++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java @@ -78,7 +78,7 @@ public class CertificateService { .domain(dto.getDomain()) .email(dto.getEmail()) .challenge(dto.getChallenge()) - .expiredAt(LocalDateTime.now().plusDays(90)) + .expiresAt(LocalDateTime.now().plusDays(90)) .isDeleted(false) .apiToken(dto.getApiToken()) .build(); @@ -138,10 +138,10 @@ public class CertificateService { .build(); executeLego(dto); - cert.setExpiredAt(LocalDateTime.now().plusDays(90)); + cert.setExpiresAt(LocalDateTime.now().plusDays(90)); certificateRepository.save(cert); log.info("renewed (id={}, newExpiry={})", - cert.getCertificateId(), cert.getExpiredAt()); + cert.getCertificateId(), cert.getExpiresAt()); } catch (Exception e) { log.error("failed to renew (id={}, domain={}): {}", cert.getCertificateId(), cert.getDomain(), e.getMessage()); @@ -161,12 +161,12 @@ public class CertificateService { /** Entity→DTO 변환 **/ private CertificateDTO toDTO(Certificate cert) { return CertificateDTO.builder() - .certificateId(cert.getCertificateId()) + .id(cert.getCertificateId()) .domain(cert.getDomain()) .email(cert.getEmail()) .challenge(cert.getChallenge()) - .expiredAt(cert.getExpiredAt()) + .expiresAt(cert.getExpiresAt()) .createdAt(cert.getCreatedAt()) .updatedAt(cert.getUpdatedAt()) .isDeleted(cert.getIsDeleted()) -- GitLab From 55d6360ccddd710fa4e5eb8126c614782fa92bcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sun, 11 May 2025 21:47:26 +0900 Subject: [PATCH 40/41] =?UTF-8?q?feat:=20Certificate=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=20=ED=99=9C=EB=8F=99=20Log=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../itda/aspect/CertificateLogAspect.java | 148 ++++++++++++++++++ .../aolda/itda/aspect/RoutingLogAspect.java | 29 ++-- .../controller/routing/RoutingController.java | 2 +- .../itda/dto/certificate/CertificateDTO.java | 11 +- .../itda/entity/certificate/Certificate.java | 4 +- .../aolda/itda/entity/routing/Routing.java | 1 - .../itda/exception/ApiExceptionHandler.java | 11 ++ .../com/aolda/itda/exception/ErrorCode.java | 9 +- .../aolda/itda/exception/ErrorResponse.java | 2 + .../certificate/CertificateRepository.java | 5 +- .../certificate/CertificateService.java | 2 +- .../itda/service/routing/RoutingService.java | 27 +++- 12 files changed, 222 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java 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 0000000..61998c8 --- /dev/null +++ b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java @@ -0,0 +1,148 @@ +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.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; + + /* 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(); + + /* 로그 엔티티 저장 */ + 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(); + + /* 로그 엔티티 저장 */ + 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() + (old.getExpiresAt().equals(newObj.getExpiresAt()) ? "" : " -> " + newObj.getExpiresAt()); + + /* 로그 엔티티 저장 */ + 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/RoutingLogAspect.java b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java index 97f7de4..8b9d5b9 100644 --- a/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java +++ b/src/main/java/com/aolda/itda/aspect/RoutingLogAspect.java @@ -141,19 +141,14 @@ public class RoutingLogAspect { + "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 (old.getCertificate() == null) { - if (newObj.getCertificate() != null) { - description = description + "certificateId: null -> " + newObj.getCertificate().getCertificateId() + "\n"; - } - } - else { - if (newObj.getCertificate() == null) { - description = description + "certificateId: " + old.getCertificate().getCertificateId() + " -> null\n"; - } - else { - description = description + "certificateId: " + old.getCertificate().getCertificateId() + " -> " + newObj.getCertificate().getCertificateId() + "\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())); /* 로그 엔티티 저장 */ @@ -167,4 +162,14 @@ public class RoutingLogAspect { .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/controller/routing/RoutingController.java b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java index d15805a..7ca1aa8 100644 --- a/src/main/java/com/aolda/itda/controller/routing/RoutingController.java +++ b/src/main/java/com/aolda/itda/controller/routing/RoutingController.java @@ -40,7 +40,7 @@ public class RoutingController { @PatchMapping("/routing") public ResponseEntity<Object> edit(@RequestParam Long routingId, @RequestBody RoutingDTO dto, - HttpServletRequest request) { + HttpServletRequest request) throws IOException { routingService.editRouting(routingId, dto, (List<String>) request.getAttribute("projects")); return ResponseEntity.ok().build(); } diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java index e2350cc..23b9e95 100644 --- a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java +++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java @@ -1,7 +1,9 @@ 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; @@ -16,9 +18,12 @@ public class CertificateDTO { private Long id; // 인증서 고유 ID // private String projectId; // 프로젝트 식별자 private String domain; // SSL 인증받을 도메인 주소 - private String email; // 도메인 소유자의 이메일 - private LocalDateTime expiresAt; // 인증서 만료일 - private LocalDateTime createdAt; // 인증서 생성일 + 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) diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java index eaab407..8ebc454 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -2,6 +2,7 @@ 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.*; @@ -32,9 +33,8 @@ public class Certificate extends BaseTimeEntity { private String email; @Setter + @Column(columnDefinition = "DATETIME") private LocalDateTime expiresAt; //인증서 만료일 - private LocalDateTime createdAt; - private LocalDateTime updatedAt; @Enumerated(EnumType.STRING) private Challenge challenge; diff --git a/src/main/java/com/aolda/itda/entity/routing/Routing.java b/src/main/java/com/aolda/itda/entity/routing/Routing.java index 94ecb55..9412368 100644 --- a/src/main/java/com/aolda/itda/entity/routing/Routing.java +++ b/src/main/java/com/aolda/itda/entity/routing/Routing.java @@ -69,7 +69,6 @@ public class Routing extends BaseTimeEntity { 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/exception/ApiExceptionHandler.java b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java index 8dbb993..20ef9ec 100644 --- a/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java +++ b/src/main/java/com/aolda/itda/exception/ApiExceptionHandler.java @@ -14,4 +14,15 @@ public class ApiExceptionHandler { 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/ErrorCode.java b/src/main/java/com/aolda/itda/exception/ErrorCode.java index d75235d..5adaa1f 100644 --- a/src/main/java/com/aolda/itda/exception/ErrorCode.java +++ b/src/main/java/com/aolda/itda/exception/ErrorCode.java @@ -43,8 +43,15 @@ public enum ErrorCode { FAIL_DELETE_CONF(HttpStatus.BAD_REQUEST, "Conf 파일을 삭제하지 못했습니다"), FAIL_ROLL_BACK(HttpStatus.BAD_REQUEST, "롤백 실패"), - FAIL_CREATE_CERT(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 index 1bec060..aa02002 100644 --- a/src/main/java/com/aolda/itda/exception/ErrorResponse.java +++ b/src/main/java/com/aolda/itda/exception/ErrorResponse.java @@ -1,5 +1,6 @@ package com.aolda.itda.exception; +import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import org.springframework.http.HttpStatus; @@ -7,6 +8,7 @@ import org.springframework.http.ResponseEntity; @Getter @Builder +@AllArgsConstructor public class ErrorResponse { private final HttpStatus status; // HTTP 상태 코드 diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java index b8cc148..6e0996b 100644 --- a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java +++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java @@ -1,7 +1,6 @@ package com.aolda.itda.repository.certificate; import com.aolda.itda.entity.certificate.Certificate; -import com.aolda.itda.entity.forwarding.Forwarding; import org.springframework.data.jpa.repository.JpaRepository; import java.time.LocalDateTime; @@ -17,13 +16,13 @@ public interface CertificateRepository extends JpaRepository<Certificate, Long> List<Certificate> findByProjectIdAndIsDeleted(String projectId, Boolean isDeleted); // 만료일이 주어진 날짜 이전인(=만료 30일 이내) 인증서 조회 - //List<Certificate> findByExpiredAtBeforeAndIsDeleted(LocalDateTime date, Boolean isDeleted); + //List<Certificate> findByExpiresAtBeforeAndIsDeleted(LocalDateTime date, Boolean isDeleted); // 1) domain 필터링용 메서드 List<Certificate> findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted( String projectId, String domain, Boolean isDeleted); // 3) 만료 30일 이내 대상 조회 - List<Certificate> findByExpiredAtBeforeAndIsDeleted( + List<Certificate> findByExpiresAtBeforeAndIsDeleted( LocalDateTime date, Boolean isDeleted); } diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java index 250e4d2..f8d8cfe 100644 --- a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java +++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java @@ -125,7 +125,7 @@ public class CertificateService { public void renewExpiringCertificates() { LocalDateTime threshold = LocalDateTime.now().plusDays(30); List<Certificate> expiring = certificateRepository - .findByExpiredAtBeforeAndIsDeleted(threshold, false); + .findByExpiresAtBeforeAndIsDeleted(threshold, false); for (Certificate cert : expiring) { try { 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 90424a3..8a50622 100644 --- a/src/main/java/com/aolda/itda/service/routing/RoutingService.java +++ b/src/main/java/com/aolda/itda/service/routing/RoutingService.java @@ -174,7 +174,7 @@ public class RoutingService { } /* Routing 수정 */ - public void editRouting(Long routingId, RoutingDTO dto, List<String> projects) { + 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)); @@ -187,9 +187,16 @@ public class RoutingService { } /* SSL 인증서 조회 */ - Certificate certificate = (dto.getCertificateId() == null) || (dto.getCertificateId() == -1 ) ? null : - certificateRepository.findById(dto.getCertificateId()) - .orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_CERTIFICATE)); // isDeleted 확인 필요 + 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); @@ -220,7 +227,17 @@ public class RoutingService { String url = "http://nginx:8081/nginx-api/test"; try { restTemplate.getForEntity(url, String.class); - } catch (RuntimeException e) { + } 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); -- GitLab From 7a143ba4d32523eddd06c204e873baf86f9c75b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=B2=9C=20=EC=A7=84=EA=B0=95?= <jjjjjk12@ajou.ac.kr> Date: Sun, 18 May 2025 22:32:26 +0900 Subject: [PATCH 41/41] =?UTF-8?q?feat:=20Certificate=20=EA=B2=80=EC=83=89?= =?UTF-8?q?=EC=97=90=20=EC=99=80=EC=9D=BC=EB=93=9C=EC=B9=B4=EB=93=9C=20?= =?UTF-8?q?=EC=A1=B0=EA=B1=B4,=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EA=B2=80=EC=83=89=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../itda/aspect/CertificateLogAspect.java | 10 ++- .../certificate/CertificateController.java | 12 +++- .../itda/dto/certificate/CertificateDTO.java | 2 +- .../itda/entity/certificate/Certificate.java | 1 + .../certificate/CertificateRepository.java | 7 +- .../certificate/CertificateService.java | 72 +++++++++++++++++-- 6 files changed, 91 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java index 61998c8..30df250 100644 --- a/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java +++ b/src/main/java/com/aolda/itda/aspect/CertificateLogAspect.java @@ -23,6 +23,7 @@ import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; +import java.time.format.DateTimeFormatter; import java.util.Map; import java.util.Objects; @@ -36,6 +37,8 @@ public class CertificateLogAspect { private final LogRepository logRepository; private final EntityManager entityManager; + private static final DateTimeFormatter LOG_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + /* Create 로깅 */ @AfterReturning(pointcut = "execution(* com.aolda.itda.service.certificate.*Service.*create*(..))" , returning = "result") @@ -55,7 +58,7 @@ public class CertificateLogAspect { String description = "domain: " + certificate.getDomain() + "\n" + "email: " + certificate.getEmail() + "\n" + "challenge: " + certificate.getChallenge() + "\n" - + "expiresAt: " + certificate.getExpiresAt(); + + "expiresAt: " + (certificate.getExpiresAt() != null ? certificate.getExpiresAt().format(LOG_DATE_FORMATTER) : "null"); /* 로그 엔티티 저장 */ logRepository.save(Log.builder() @@ -89,7 +92,7 @@ public class CertificateLogAspect { String description = "domain: " + certificate.getDomain() + "\n" + "email: " + certificate.getEmail() + "\n" + "challenge: " + certificate.getChallenge() + "\n" - + "expiresAt: " + certificate.getExpiresAt(); + + "expiresAt: " + (certificate.getExpiresAt() != null ? certificate.getExpiresAt().format(LOG_DATE_FORMATTER) : "null"); /* 로그 엔티티 저장 */ logRepository.save(Log.builder() @@ -132,7 +135,8 @@ public class CertificateLogAspect { String description = "domain: " + old.getDomain() + (old.getDomain().equals(newObj.getDomain()) ? "" : " -> " + newObj.getDomain()) + "\n" + "email: " + old.getEmail() + (old.getEmail().equals(newObj.getEmail()) ? "" : " -> " + newObj.getEmail()) + "\n" + "challenge: " + old.getChallenge() + (old.getChallenge() == newObj.getChallenge() ? "" : " -> " + newObj.getChallenge()) + "\n" - + "expiresAt: " + old.getExpiresAt() + (old.getExpiresAt().equals(newObj.getExpiresAt()) ? "" : " -> " + newObj.getExpiresAt()); + + "expiresAt: " + (old.getExpiresAt() != null ? old.getExpiresAt().format(LOG_DATE_FORMATTER) : "null") + + (old.getExpiresAt() != null && newObj.getExpiresAt() != null && !old.getExpiresAt().equals(newObj.getExpiresAt()) ? " -> " + newObj.getExpiresAt().format(LOG_DATE_FORMATTER) : ""); /* 로그 엔티티 저장 */ logRepository.save(Log.builder() diff --git a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java index d08968b..26a0322 100644 --- a/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java +++ b/src/main/java/com/aolda/itda/controller/certificate/CertificateController.java @@ -59,8 +59,18 @@ public class CertificateController { @GetMapping("/certificates") public ResponseEntity<PageResp<CertificateDTO>> lists( @RequestParam String projectId, - @RequestParam(required = false) String domain + @RequestParam(required = false) String domain, + @RequestParam(required = false) String query ) { + + if (query != null) { + return ResponseEntity.ok( + certificateService.getCertificatesWithSearch( + projectId, + query + ) + ); + } return ResponseEntity.ok( certificateService.getCertificates(projectId, domain) ); diff --git a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java index 23b9e95..058fdd7 100644 --- a/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java +++ b/src/main/java/com/aolda/itda/dto/certificate/CertificateDTO.java @@ -16,7 +16,7 @@ import java.time.LocalDateTime; public class CertificateDTO { private Long id; // 인증서 고유 ID -// private String projectId; // 프로젝트 식별자 + private String projectId; // 프로젝트 식별자 private String domain; // SSL 인증받을 도메인 주소 private String email; @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")// 도메인 소유자의 이메일 diff --git a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java index 8ebc454..9161b45 100644 --- a/src/main/java/com/aolda/itda/entity/certificate/Certificate.java +++ b/src/main/java/com/aolda/itda/entity/certificate/Certificate.java @@ -34,6 +34,7 @@ public class Certificate extends BaseTimeEntity { @Setter @Column(columnDefinition = "DATETIME") + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") private LocalDateTime expiresAt; //인증서 만료일 @Enumerated(EnumType.STRING) diff --git a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java index 6e0996b..0f3c137 100644 --- a/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java +++ b/src/main/java/com/aolda/itda/repository/certificate/CertificateRepository.java @@ -1,7 +1,9 @@ package com.aolda.itda.repository.certificate; import com.aolda.itda.entity.certificate.Certificate; +import com.aolda.itda.entity.routing.Routing; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.time.LocalDateTime; import java.util.List; @@ -19,10 +21,13 @@ public interface CertificateRepository extends JpaRepository<Certificate, Long> //List<Certificate> findByExpiresAtBeforeAndIsDeleted(LocalDateTime date, Boolean isDeleted); // 1) domain 필터링용 메서드 - List<Certificate> findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted( + List<Certificate> findByProjectIdAndDomainContainingAndIsDeleted( String projectId, String domain, Boolean isDeleted); // 3) 만료 30일 이내 대상 조회 List<Certificate> findByExpiresAtBeforeAndIsDeleted( LocalDateTime date, Boolean isDeleted); + + @Query("SELECT r FROM Routing r WHERE r.projectId = ?1 AND r.isDeleted = ?3 AND (r.domain LIKE %?2% OR r.instanceIp LIKE %?2% OR r.name LIKE %?2%)") + List<Certificate> findWithSearch(String projectId, String query, Boolean isDeleted); } diff --git a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java index f8d8cfe..7f42670 100644 --- a/src/main/java/com/aolda/itda/service/certificate/CertificateService.java +++ b/src/main/java/com/aolda/itda/service/certificate/CertificateService.java @@ -2,8 +2,10 @@ package com.aolda.itda.service.certificate; import com.aolda.itda.dto.PageResp; import com.aolda.itda.dto.certificate.CertificateDTO; +import com.aolda.itda.dto.routing.RoutingDTO; import com.aolda.itda.entity.certificate.Certificate; import com.aolda.itda.entity.certificate.Challenge; +import com.aolda.itda.entity.routing.Routing; import com.aolda.itda.exception.CustomException; import com.aolda.itda.exception.ErrorCode; import com.aolda.itda.repository.certificate.CertificateRepository; @@ -12,6 +14,7 @@ import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -21,7 +24,10 @@ import java.io.IOException; import java.io.InputStreamReader; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.regex.Pattern; @Service @Transactional @@ -29,6 +35,8 @@ import java.util.List; @Slf4j public class CertificateService { + @Value("${spring.server.admin-project}") + private String adminProject; private final CertificateRepository certificateRepository; private final AuthService authService; @@ -41,18 +49,67 @@ public class CertificateService { return toDTO(cert); } + /* Certificate 목록 조회 + 검색 */ + public PageResp<CertificateDTO> getCertificatesWithSearch(String projectId, String query) { + + /* 입력 검증 */ + if (query == null || query.isBlank()) { + return PageResp.<CertificateDTO>builder() + .contents(certificateRepository.findByProjectIdAndIsDeleted(projectId, false) + .stream() + .map(this::toDTO) + .toList()).build(); + } + + /* 도메인 패턴 검증 */ + String domainPattern = "^(\\*\\.)?([a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}$"; + if (Pattern.matches(domainPattern, query) && query.startsWith("*.")) { + query = query.substring(2); + } + + return PageResp.<CertificateDTO>builder() + .contents(certificateRepository.findWithSearch(projectId, query, false) + .stream() + .map(this::toDTO) + .toList()).build(); + } + /** 1) 목록 조회 (domain 필터 optional) **/ public PageResp<CertificateDTO> getCertificates(String projectId, String domain) { - List<Certificate> list; + Set<Certificate> set = new HashSet<>(); + // 도메인이 입력된 경우 처리 if (domain != null && !domain.isBlank()) { - list = certificateRepository - .findByProjectIdAndDomainContainingIgnoreCaseAndIsDeleted( - projectId, domain, false); + // 서브도메인이 있는 경우 + if (domain.indexOf('.') != domain.lastIndexOf('.')) { + String wildcardDomain = "*." + domain.substring(domain.indexOf('.') + 1); + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + projectId, wildcardDomain, false)); + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + projectId, domain, false)); + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + adminProject, wildcardDomain, false)); + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + adminProject, domain, false)); + } else { + // 서브도메인이 없는 경우 일반 검색 + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + projectId, domain, false)); + set.addAll(certificateRepository + .findByProjectIdAndDomainContainingAndIsDeleted( + adminProject, domain, false)); + } } else { - list = certificateRepository - .findByProjectIdAndIsDeleted(projectId, false); + set.addAll(certificateRepository + .findByProjectIdAndIsDeleted(projectId, false)); + set.addAll(certificateRepository + .findByProjectIdAndIsDeleted(adminProject, false)); } - List<CertificateDTO> dtos = list.stream() + List<CertificateDTO> dtos = set.stream() .map(this::toDTO) .toList(); return PageResp.<CertificateDTO>builder() @@ -171,6 +228,7 @@ public class CertificateService { .updatedAt(cert.getUpdatedAt()) .isDeleted(cert.getIsDeleted()) .apiToken(cert.getApiToken()) + .projectId(cert.getProjectId()) .build(); } -- GitLab