Skip to content

Commit

Permalink
[FIX/#15] - 구글 미트 API 연동 충돌 해결
Browse files Browse the repository at this point in the history
  • Loading branch information
seokbeom00 authored Jul 8, 2024
2 parents 1cbb9f8 + 9ed8a60 commit 13fb7f4
Show file tree
Hide file tree
Showing 15 changed files with 217 additions and 16 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/DOCKER-CD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ jobs:
cat ./application.yml
working-directory: ${{ env.working-directory }}

- name: credentials.json 생성
run: |
mkdir -p ./src/main/resources && cd $_
touch ./credentials.json
echo "${{ secrets.GOOGLE_CREDENTIALS }}" > ./credentials.json
cat ./credentials.json
working-directory: ${{ env.working-directory }}

- name: 빌드
run: |
chmod +x gradlew
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,8 @@ out/
### Config ###
application-secret.properties
application.yaml
application.yml
application.yml
credentials.json

### 구글미트 액세스 토큰 ###
/tokens
14 changes: 7 additions & 7 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ plugins {
id 'org.springframework.boot' version '3.3.1'
id 'io.spring.dependency-management' version '1.1.5'
}

group = 'org.sopt'
version = '0.0.1-SNAPSHOT'

Expand All @@ -21,6 +20,7 @@ configurations {

repositories {
mavenCentral()
maven { url 'https://maven.google.com' }
}

dependencies {
Expand Down Expand Up @@ -68,18 +68,18 @@ dependencies {
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' // 쿼리 파라미터 로그 남기기

// GoogleMeet
implementation platform('com.google.cloud:libraries-bom:26.42.0')
implementation 'com.google.cloud:google-cloud-meet:0.3.0'
implementation 'com.google.auth:google-auth-library-oauth2-http:1.19.0'
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
}

ext {
set('springCloudVersion', "2023.0.2")
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}

tasks.named('test') {
useJUnitPlatform()
}
Empty file.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import org.sopt.seonyakServer.global.auth.jwt.JwtTokenProvider;
import org.sopt.seonyakServer.global.common.external.client.dto.MemberInfoResponse;
import org.sopt.seonyakServer.global.common.external.client.dto.MemberLoginRequest;
import org.sopt.seonyakServer.global.common.external.client.google.GoogleSocialService;
import org.sopt.seonyakServer.global.common.external.client.service.GoogleSocialService;
import org.sopt.seonyakServer.global.exception.enums.ErrorType;
import org.sopt.seonyakServer.global.exception.model.CustomException;
import org.springframework.dao.DataIntegrityViolationException;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@ public class SecurityConfig {
private final CustomAccessDeniedHandler customAccessDeniedHandler;

private static final String[] AUTH_WHITE_LIST = {
"/api/v1/**",
"/api/v1/auth/**",
"/actuator/health",
"/v3/api-docs/**",
"/swagger-ui/**",
"/swagger-resources/**"
"/swagger-resources/**",
};

@Bean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.sopt.seonyakServer.global.common.external.client.google;
package org.sopt.seonyakServer.global.common.external.client.service;

import lombok.Getter;
import lombok.RequiredArgsConstructor;
Expand All @@ -7,7 +7,10 @@
import org.sopt.seonyakServer.global.common.external.client.dto.GoogleUserInfoResponse;
import org.sopt.seonyakServer.global.common.external.client.dto.MemberInfoResponse;
import org.sopt.seonyakServer.global.common.external.client.dto.MemberLoginRequest;
import org.sopt.seonyakServer.global.common.external.client.service.SocialService;
import org.sopt.seonyakServer.global.common.external.client.google.GoogleAccessTokenClient;
import org.sopt.seonyakServer.global.common.external.client.google.GoogleUserClient;
import org.sopt.seonyakServer.global.exception.enums.ErrorType;
import org.sopt.seonyakServer.global.exception.model.CustomException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package org.sopt.seonyakServer.global.common.external.googlemeet;

import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.gax.core.FixedCredentialsProvider;
import com.google.apps.meet.v2.SpacesServiceSettings;
import com.google.auth.Credentials;
import com.google.auth.oauth2.ClientId;
import com.google.auth.oauth2.DefaultPKCEProvider;
import com.google.auth.oauth2.TokenStore;
import com.google.auth.oauth2.UserAuthorizer;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import org.sopt.seonyakServer.global.exception.enums.ErrorType;
import org.sopt.seonyakServer.global.exception.model.CustomException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GoogleMeetConfig {

@Value("${google.credentials.file.path}")
private String credentialsFilePath;

@Value("${google.credentials.oauth2.callback.uri}")
private String callbackUri;

@Value("${google.credentials.tokens.directory.path}")
private String tokensDirectoryPath;

@Value("${google.credentials.scopes}")
private List<String> scopes;

private static final String USER = "default";

@Bean
public TokenStore tokenStore() {
return new TokenStore() {
private Path pathFor(String id) {
return Paths.get(".", tokensDirectoryPath, id + ".json");
}

@Override
public String load(String id) throws IOException {
if (!Files.exists(pathFor(id))) {
return null;
}
return Files.readString(pathFor(id));
}

@Override
public void store(String id, String token) throws IOException {
Files.createDirectories(Paths.get(".", tokensDirectoryPath));
Files.writeString(pathFor(id), token);
}

@Override
public void delete(String id) throws IOException {
if (!Files.exists(pathFor(id))) {
return;
}
Files.delete(pathFor(id));
}
};
}

@Bean
public UserAuthorizer userAuthorizer(TokenStore tokenStore) throws IOException {
try (InputStream in = getClass().getResourceAsStream(credentialsFilePath)) {
if (in == null) {
throw new CustomException(ErrorType.NOT_FOUND_CREDENTIALS_JSON_ERROR);
}
ClientId clientId = ClientId.fromStream(in);
return UserAuthorizer.newBuilder()
.setClientId(clientId)
.setCallbackUri(URI.create(callbackUri))
.setScopes(scopes)
.setPKCEProvider(new DefaultPKCEProvider() {
@Override
public String getCodeChallenge() {
return super.getCodeChallenge().split("=")[0];
}
})
.setTokenStore(tokenStore)
.build();
}
}

@Bean
public LocalServerReceiver localServerReceiver() {
return new LocalServerReceiver.Builder().setPort(8081).build();
}

@Bean
public SpacesServiceSettings spacesServiceSettings(Credentials credentials) throws IOException {
return SpacesServiceSettings.newBuilder()
.setCredentialsProvider(FixedCredentialsProvider.create(credentials))
.build();
}

@Bean
public Credentials credentials(UserAuthorizer userAuthorizer, LocalServerReceiver localServerReceiver)
throws Exception {
// UserAuthorizer를 사용하여 지정된 사용자의 Credentials를 가져옴
Credentials credentials = userAuthorizer.getCredentials(USER);
if (credentials != null) {
return credentials;
} else {
throw new CustomException(ErrorType.GET_GOOGLE_AUTHORIZER_ERROR);

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package org.sopt.seonyakServer.global.common.external.googlemeet;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sopt.seonyakServer.global.common.external.googlemeet.dto.GoogleMeetUrlResponse;
import org.sopt.seonyakServer.global.exception.enums.ErrorType;
import org.sopt.seonyakServer.global.exception.model.CustomException;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/v1/")
@RequiredArgsConstructor
@Slf4j
public class GoogleMeetController {
private final GoogleMeetService googleMeetService;

@PostMapping("/google-meet")
public ResponseEntity<GoogleMeetUrlResponse> createSpace() {
try {
return ResponseEntity.ok(googleMeetService.createMeetingSpace());
} catch (Exception e) {
log.info(e.getMessage());
throw new CustomException(ErrorType.GET_GOOGLE_MEET_URL_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.sopt.seonyakServer.global.common.external.googlemeet;

import com.google.apps.meet.v2.CreateSpaceRequest;
import com.google.apps.meet.v2.Space;
import com.google.apps.meet.v2.SpacesServiceClient;
import com.google.apps.meet.v2.SpacesServiceSettings;
import lombok.RequiredArgsConstructor;
import org.sopt.seonyakServer.global.common.external.googlemeet.dto.GoogleMeetUrlResponse;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class GoogleMeetService {
private final SpacesServiceSettings spacesServiceSettings;

public GoogleMeetUrlResponse createMeetingSpace() throws Exception {
SpacesServiceClient spacesServiceClient = SpacesServiceClient.create(spacesServiceSettings);
CreateSpaceRequest request = CreateSpaceRequest.newBuilder()
.setSpace(Space.newBuilder().build())
.build();
Space response = spacesServiceClient.createSpace(request);
return GoogleMeetUrlResponse.of(response.getMeetingUri());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.sopt.seonyakServer.global.common.external.googlemeet.dto;

public record GoogleMeetUrlResponse(
String googleMeet
) {
public static GoogleMeetUrlResponse of(
final String googleMeet
) {
return new GoogleMeetUrlResponse(googleMeet);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.sopt.seonyakServer.global.common.dto.OcrUnivResponse;
import org.sopt.seonyakServer.global.common.external.naver.dto.OcrUnivResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import lombok.RequiredArgsConstructor;
import org.json.JSONArray;
import org.json.JSONObject;
import org.sopt.seonyakServer.global.common.dto.OcrUnivResponse;
import org.sopt.seonyakServer.global.common.external.naver.dto.OcrUnivResponse;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package org.sopt.seonyakServer.global.common.dto;
package org.sopt.seonyakServer.global.common.external.naver.dto;

public record OcrUnivResponse(
String univName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ public enum ErrorType {
*/
NOT_FOUND_MEMBER_ERROR(HttpStatus.NOT_FOUND, "40401", "존재하지 않는 회원입니다."),
NOT_FOUND_REFRESH_TOKEN_ERROR(HttpStatus.NOT_FOUND, "40402", "존재하지 않는 리프레시 토큰입니다."),
NOT_FOUND_CREDENTIALS_JSON_ERROR(HttpStatus.NOT_FOUND, "40403", "구글미트 Credentials Json 파일을 찾을 수 없습니다."),

/**
* 409 CONFLICT
Expand All @@ -59,7 +60,9 @@ public enum ErrorType {
* 500 INTERNAL SERVER ERROR
*/
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "50001", "알 수 없는 서버 에러가 발생했습니다."),
GET_UPLOAD_PRESIGNED_URL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "50002", "업로드를 위한 Presigned URL 획득에 실패했습니다.");
GET_UPLOAD_PRESIGNED_URL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "50002", "업로드를 위한 Presigned URL 획득에 실패했습니다."),
GET_GOOGLE_MEET_URL_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "50003", "구글미트 URL 획득에 실패했습니다."),
GET_GOOGLE_AUTHORIZER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "50004", "구글미트 URL 획득에 실패했습니다.");

private final HttpStatus httpStatus;
private final String code;
Expand Down

0 comments on commit 13fb7f4

Please sign in to comment.