From 8283d8235e86087007bed4b75f0dd1cc5323fb66 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Fri, 15 Aug 2025 21:47:26 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=EC=95=A1=EC=84=B8=EC=8A=A4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=9E=AC=EB=B0=9C=EA=B8=89=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repository/DiscordUserRepository.java | 2 - .../discord/service/DiscordUserService.java | 44 +++++++++++++++++++ .../gcp/domain/gcp/service/GcpService.java | 14 +++++- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java b/src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java index fa35fb1..53f6f3a 100644 --- a/src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java +++ b/src/main/java/com/gcp/domain/discord/repository/DiscordUserRepository.java @@ -32,6 +32,4 @@ Optional findAccessTokenByUserIdAndGuildId(@Param("userId") String userI @Query("SELECT u.accessTokenExpiration FROM DiscordUser u WHERE u.userId = :userId AND u.guildId = :guildId") Optional findAccessTokenExpByUserIdAndGuildId(@Param("userId") String userId, @Param("guildId") String guildId); - - } diff --git a/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java b/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java index 06ec7d7..d925a05 100644 --- a/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java +++ b/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java @@ -3,12 +3,26 @@ import com.gcp.domain.discord.entity.DiscordUser; import com.gcp.domain.discord.repository.DiscordUserRepository; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.HashMap; +import java.util.Map; + +import static org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames.CLIENT_ID; +import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.CLIENT_SECRET; @Service @RequiredArgsConstructor public class DiscordUserService { private final DiscordUserRepository discordUserRepository; + private final RestTemplate restTemplate; public boolean insertDiscordUser(String userId, String userName, String guildId, String guildName){ @@ -19,4 +33,34 @@ public boolean insertDiscordUser(String userId, String userName, String guildId, } return false; } + + public Map refreshAccessToken(String refreshToken) { + String url = "https://oauth2.googleapis.com/token"; + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("client_id", CLIENT_ID); + body.add("client_secret", CLIENT_SECRET); + body.add("refresh_token", refreshToken); + body.add("grant_type", "refresh_token"); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + + HttpEntity> request = new HttpEntity<>(body, headers); + ResponseEntity response = restTemplate.postForEntity(url, request, Map.class); + + if (response.getStatusCode().is2xxSuccessful()) { + Map result = new HashMap<>(); + Map bodyMap = response.getBody(); + + result.put("access_token", bodyMap.get("access_token")); + result.put("expires_in", bodyMap.get("expires_in")); // 보통 초 단위 (ex: 3599) + result.put("scope", bodyMap.get("scope")); + result.put("token_type", bodyMap.get("token_type")); + + return result; + } else { + throw new RuntimeException("리프레시 토큰으로 액세스 토큰 재발급 실패"); + } + } } diff --git a/src/main/java/com/gcp/domain/gcp/service/GcpService.java b/src/main/java/com/gcp/domain/gcp/service/GcpService.java index 50fdf2b..6e4f23c 100644 --- a/src/main/java/com/gcp/domain/gcp/service/GcpService.java +++ b/src/main/java/com/gcp/domain/gcp/service/GcpService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.gcp.domain.discord.entity.DiscordUser; import com.gcp.domain.discord.repository.DiscordUserRepository; +import com.gcp.domain.discord.service.DiscordUserService; import com.gcp.domain.gcp.dto.ProjectZoneDto; import com.gcp.domain.gcp.repository.GcpProjectRepository; @@ -23,6 +24,7 @@ import java.io.IOException; +import java.time.LocalDateTime; import java.util.*; import static org.json.XMLTokener.entity; @@ -39,6 +41,8 @@ public class GcpService { private static final String PROJECT_ID = "sincere-elixir-464606-j1"; private final GcpImageUtil gcpImageUtil; + private final DiscordUserService discordUserService; + public String startVM(String userId, String guildId, String vmName) { try { @@ -129,7 +133,7 @@ public List getVmLogs(String userId, String guildId, String vmName) { "resource.type=\"gce_instance\" AND resource.labels.instance_id=\"%s\" AND severity>=ERROR", vmId ); - + Map body = Map.of( "resourceNames", List.of("projects/sincere-elixir-464606-j1"), "pageSize", 50, @@ -213,8 +217,16 @@ public List> getVmList(String userId, String guildId) { public List getProjectIds(String userId, String guildId) { try { String url = "https://cloudresourcemanager.googleapis.com/v1/projects"; + LocalDateTime tokenExp = discordUserRepository.findAccessTokenExpByUserIdAndGuildId(userId, guildId).orElseThrow(); + if(tokenExp.isBefore(LocalDateTime.now())){ + DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow(); + Map reissued = discordUserService.refreshAccessToken(discordUser.getGoogleRefreshToken()); + discordUser.updateAccessToken((String) reissued.get("access_token")); + discordUser.updateAccessTokenExpiration((LocalDateTime) reissued.get("expires_in")); + } String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow(); + HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(accessToken); headers.setContentType(MediaType.APPLICATION_JSON); From 4174a3120b714efe63800dfec5fff0a690d13ecd Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Fri, 15 Aug 2025 21:47:53 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=EB=B0=B0=ED=8F=AC=20=EB=B8=8C?= =?UTF-8?q?=EB=9E=9C=EC=B9=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1d81a01..45e070b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -2,7 +2,7 @@ name: CI / CD on: push: - branches: [main] + branches: [feat/#19-access-token-reissue] jobs: CI: From 6e1c023a3fe6ac066779dc4c3ddc16041aa004d3 Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Sat, 16 Aug 2025 23:12:00 +0900 Subject: [PATCH 3/4] =?UTF-8?q?fix:=20=ED=86=A0=ED=81=B0=20=EB=A7=8C?= =?UTF-8?q?=EB=A3=8C=EA=B8=B0=EA=B0=84=20=EC=97=85=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=8A=B8=20=EB=B0=A9=EC=8B=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/gcp/domain/gcp/service/GcpService.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/gcp/domain/gcp/service/GcpService.java b/src/main/java/com/gcp/domain/gcp/service/GcpService.java index 6e4f23c..bee2ce0 100644 --- a/src/main/java/com/gcp/domain/gcp/service/GcpService.java +++ b/src/main/java/com/gcp/domain/gcp/service/GcpService.java @@ -9,8 +9,7 @@ import com.gcp.domain.gcp.repository.GcpProjectRepository; import com.gcp.domain.gcp.util.GcpImageUtil; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.cloud.compute.v1.Project; +import com.gcp.domain.oauth2.util.TokenEncryptConverter; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -19,6 +18,7 @@ import org.json.JSONObject; import org.springframework.http.*; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestTemplate; @@ -27,11 +27,10 @@ import java.time.LocalDateTime; import java.util.*; -import static org.json.XMLTokener.entity; - @Service @RequiredArgsConstructor @Slf4j +@Transactional public class GcpService { private final RestTemplate restTemplate = new RestTemplate(); @@ -222,7 +221,7 @@ public List getProjectIds(String userId, String guildId) { DiscordUser discordUser = discordUserRepository.findByUserIdAndGuildId(userId, guildId).orElseThrow(); Map reissued = discordUserService.refreshAccessToken(discordUser.getGoogleRefreshToken()); discordUser.updateAccessToken((String) reissued.get("access_token")); - discordUser.updateAccessTokenExpiration((LocalDateTime) reissued.get("expires_in")); + discordUser.updateAccessTokenExpiration(LocalDateTime.now().plusSeconds((Integer) reissued.get("expires_in"))); } String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow(); From d7c7f1b788bba423343115377deda6fc770da00c Mon Sep 17 00:00:00 2001 From: Jinyoung Date: Sat, 16 Aug 2025 23:12:15 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20client=5Fid,=20client=5Fsecret=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EB=B3=80=EC=88=98=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/discord/service/DiscordUserService.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java b/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java index d925a05..5dfa5f3 100644 --- a/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java +++ b/src/main/java/com/gcp/domain/discord/service/DiscordUserService.java @@ -3,6 +3,8 @@ import com.gcp.domain.discord.entity.DiscordUser; import com.gcp.domain.discord.repository.DiscordUserRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; @@ -20,10 +22,17 @@ @Service @RequiredArgsConstructor +@Slf4j public class DiscordUserService { private final DiscordUserRepository discordUserRepository; private final RestTemplate restTemplate; + @Value("${GOOGLE_CLIENT_ID}") + private String googleClientId; + + @Value("${GOOGLE_CLIENT_SECRET}") + private String googleClientSecret; + public boolean insertDiscordUser(String userId, String userName, String guildId, String guildName){ if (!discordUserRepository.existsByUserIdAndGuildId(userId, guildId)) { @@ -36,10 +45,11 @@ public boolean insertDiscordUser(String userId, String userName, String guildId, public Map refreshAccessToken(String refreshToken) { String url = "https://oauth2.googleapis.com/token"; + log.info("{}", refreshToken); MultiValueMap body = new LinkedMultiValueMap<>(); - body.add("client_id", CLIENT_ID); - body.add("client_secret", CLIENT_SECRET); + body.add("client_id", googleClientId); + body.add("client_secret", googleClientSecret); body.add("refresh_token", refreshToken); body.add("grant_type", "refresh_token");