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: 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..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,12 +3,35 @@ 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; +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 +@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){ @@ -19,4 +42,35 @@ public boolean insertDiscordUser(String userId, String userName, String guildId, } return false; } + + public Map refreshAccessToken(String refreshToken) { + String url = "https://oauth2.googleapis.com/token"; + log.info("{}", refreshToken); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("client_id", googleClientId); + body.add("client_secret", googleClientSecret); + 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..bee2ce0 100644 --- a/src/main/java/com/gcp/domain/gcp/service/GcpService.java +++ b/src/main/java/com/gcp/domain/gcp/service/GcpService.java @@ -4,12 +4,12 @@ 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; 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; @@ -18,18 +18,19 @@ 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; import java.io.IOException; +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(); @@ -39,6 +40,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 +132,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 +216,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.now().plusSeconds((Integer) reissued.get("expires_in"))); + } String accessToken = discordUserRepository.findAccessTokenByUserIdAndGuildId(userId, guildId).orElseThrow(); + HttpHeaders headers = new HttpHeaders(); headers.setBearerAuth(accessToken); headers.setContentType(MediaType.APPLICATION_JSON);