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/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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/12] =?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