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