Skip to content
Merged

Dev #85

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ dependencies {
// OAuth 2.0
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// Actuator/Health
implementation("org.springframework.boot:spring-boot-starter-actuator")

// lombok
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
Expand Down
6 changes: 3 additions & 3 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ services:
volumes:
- ./logs:/app/logs
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8080/actuator/health | grep -q 'UP' || exit 1"]
test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:8080/actuator/health | grep -q '\"status\":\"UP\"'" ]
interval: 15s
timeout: 5s
retries: 10
start_period: 30s
retries: 20
start_period: 60s

redis:
image: redis:7-alpine
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,4 @@ public interface CodeReferenceRepository extends JpaRepository<CodeReference, Lo

Optional<CodeReference> findByMindmapIdAndId(Long mindmapId, Long id);

boolean existsByMindmapIdAndId(Long mindmapId, Long id);
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,18 +127,16 @@ public ReferenceResponse updateReference(Long mapId, Long refId, Long userId, Cr

@Transactional
public void deleteReference(Long mapId, Long refId, Long userId) {
// 1. 권한 확인
// 권한 확인
if (!mindmapAuthService.hasEdit(mapId, userId)) {
throw new GlobalException(ErrorCode.FORBIDDEN_ACCESS);
}

// 2. 해당 마인드맵에 코드 참조가 존재하는지 확인
if (!codeReferenceRepository.existsByMindmapIdAndId(mapId, refId)) {
throw new GlobalException(ErrorCode.CODE_REFERENCE_NOT_FOUND);
}
CodeReference codeReference = codeReferenceRepository.findByMindmapIdAndId(mapId, refId)
.orElseThrow(() -> new GlobalException(ErrorCode.CODE_REFERENCE_NOT_FOUND));

// 3. 코드 참조 삭제
codeReferenceRepository.deleteById(refId);
// 코드 참조 삭제
codeReferenceRepository.delete(codeReference);
}

private String extractLines(String fullContent, Integer startLine, Integer endLine) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.teamEWSN.gitdeun.common.oauth.handler.CustomOAuth2SuccessHandler;
import com.teamEWSN.gitdeun.common.oauth.service.CustomOAuth2UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
Expand Down Expand Up @@ -35,6 +36,9 @@ public class SecurityConfig {
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;

@Value("${security.require-auth-for-unknown:true}")
private boolean requireAuthForUnknown;

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

Expand All @@ -53,9 +57,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint(userInfoEndpointConfig -> userInfoEndpointConfig
.userService(customOAuth2UserService))
// .defaultSuccessUrl("/oauth/success") // 로그인 성공시 이동할 URL
.successHandler(customOAuth2SuccessHandler)
// .failureUrl("/oauth/fail") // 로그인 실패시 이동할 URL
.failureHandler(customOAuthFailureHandler))
.logout(logout -> logout
.logoutUrl("/logout")
Expand All @@ -66,16 +68,18 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

// 경로별 인가 작업
http
.authorizeHttpRequests((auth) -> auth
// 내부 webhook 통신 API
.requestMatchers("/api/webhook/**").permitAll()
// 외부 공개 API(클라이언트 - JWT)
.requestMatchers(SecurityPath.ADMIN_ENDPOINTS).hasRole("ADMIN")
.requestMatchers(SecurityPath.USER_ENDPOINTS).hasAnyRole("USER", "ADMIN")
.requestMatchers(SecurityPath.PUBLIC_ENDPOINTS).permitAll()
.anyRequest().permitAll()
// .anyRequest().authenticated()
);
.authorizeHttpRequests((auth) -> {
auth
.requestMatchers(SecurityPath.ADMIN_ENDPOINTS).hasRole("ADMIN")
.requestMatchers(SecurityPath.USER_ENDPOINTS).hasAnyRole("USER", "ADMIN")
.requestMatchers(SecurityPath.PUBLIC_ENDPOINTS).permitAll();

if (requireAuthForUnknown) {
auth.anyRequest().authenticated();
} else {
auth.anyRequest().permitAll();
}
});

// 예외 처리
http
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ public class SecurityPath {
public static final String[] PUBLIC_ENDPOINTS = {
"/api/auth/token/refresh",
"/api/auth/oauth/refresh/*",
"/oauth/logout",
"/",

"/api/webhook/**",
"/actuator/health",
"/actuator/health/**",
"/actuator/info"
};

// hasRole("USER")
Expand All @@ -27,8 +32,10 @@ public class SecurityPath {
"/api/skills/**",
"/api/recruitments/**",
"/api/applications/**",
"/api/comments/**",
"/api/code-reviews/**",
"/api/references/**",
"/api/s3/bucket/**",
"/api/webhook/**"
};

// hasRole("ADMIN")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,14 +313,13 @@ public String getFileRaw(String repoUrl, String filePath, Integer startLine, Int
}*/

public String getCodeFromNode(String nodeKey, String filePath, String authHeader) {
String fileName = extractFileName(filePath);
try {
log.debug("FastAPI 노드 기반 코드 조회 시작 - nodeKey: {}, filePath: {}", nodeKey, fileName);
log.debug("FastAPI 노드 기반 코드 조회 시작 - nodeKey: {}, filePath: {}", nodeKey, filePath);

UriComponentsBuilder uriBuilder = UriComponentsBuilder
.fromPath("/content/file/by-node") // FastAPI의 해당 엔드포인트
.queryParam("node_key", nodeKey)
.queryParam("file_path", fileName);
.queryParam("file_path", filePath);
// 'prefer' 파라미터는 생략하여 FastAPI의 기본값(auto)을 따르도록 함

String uri = uriBuilder.build().toUriString();
Expand All @@ -336,8 +335,8 @@ public String getCodeFromNode(String nodeKey, String filePath, String authHeader
.onStatus(HttpStatusCode::is4xxClientError, clientResponse ->
clientResponse.bodyToMono(String.class)
.map(errorBody -> {
log.warn("FastAPI 노드 코드 조회 4xx 오류 - nodeKey: {}, fileName: {}, status: {}, body: {}",
nodeKey, fileName, clientResponse.statusCode(), errorBody);
log.warn("FastAPI 노드 코드 조회 4xx 오류 - nodeKey: {}, filePath: {}, status: {}, body: {}",
nodeKey, filePath, clientResponse.statusCode(), errorBody);
return new RuntimeException("FastAPI 클라이언트 오류: " + errorBody);
})
)
Expand All @@ -346,13 +345,13 @@ public String getCodeFromNode(String nodeKey, String filePath, String authHeader
.block();

String codeContent = (response != null) ? response.getCode() : "";
log.debug("FastAPI 노드 기반 코드 조회 성공 - nodeKey: {}, fileName: {}, 길이: {}",
nodeKey, fileName, codeContent != null ? codeContent.length() : 0);
log.debug("FastAPI 노드 기반 코드 조회 성공 - nodeKey: {}, filePath: {}, 길이: {}",
nodeKey, filePath, codeContent != null ? codeContent.length() : 0);

return codeContent;

} catch (Exception e) {
log.error("FastAPI 노드 기반 코드 조회 실패 - nodeKey: {}, fileName: {}", nodeKey, fileName, e);
log.error("FastAPI 노드 기반 코드 조회 실패 - nodeKey: {}, filePath: {}", nodeKey, filePath, e);
return ""; // 예외 발생 시 빈 문자열 반환
}
}
Expand All @@ -365,11 +364,6 @@ private String extractMapId(String repoUrl) {
return segments[segments.length - 1].replaceAll("\\.git$", "");
}

// 파일명 추출
public String extractFileName(String filePath) {
return filePath.substring(filePath.lastIndexOf('/') + 1);
}

private AnalysisResultDto buildAnalysisResultDto(
RepoInfoResponse repoInfo
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

private final GitHubApiHelper gitHubApiHelper;
private final GoogleApiHelper googleApiHelper;
private final SocialTokenRefreshService socialTokenRefreshService;
private final UserRepository userRepository;
private final SocialConnectionRepository socialConnectionRepository;
Expand Down Expand Up @@ -67,7 +68,11 @@ public User processUser(OAuth2User oAuth2User, OAuth2UserRequest userRequest) {
.map(conn -> {
// provider 별 refresh 정책
socialTokenRefreshService.refreshSocialToken(conn, accessToken, refreshToken);
return conn.getUser();

// 사용자 정보 갱신
User user = conn.getUser();
user.updateProfile(dto.getName(), dto.getProfileImageUrl());
return user;
})
.orElseGet(() -> createOrConnect(dto, provider, providerId, accessToken, refreshToken));
}
Expand All @@ -79,8 +84,24 @@ private OAuth2ResponseDto getOAuth2ResponseDto(OAuth2User oAuth2User, OAuth2User
Map<String, Object> attr = new HashMap<>(oAuth2User.getAttributes());

if (registrationId.equalsIgnoreCase("google")) {
String accessToken = userRequest.getAccessToken().getTokenValue();

// 만료 시 토큰 리프레시 후 최신 userinfo 재조회
if (googleApiHelper.isExpired(accessToken)) {
String refreshToken = (String) userRequest.getAdditionalParameters().get("refresh_token");
if (refreshToken != null) {
var tokenResp = googleApiHelper.refreshToken(refreshToken);
accessToken = tokenResp.accessToken();
}
}

// 항상 최신 userinfo로 덮어쓰기 (이름 변경 즉시 반영)
Map<String, Object> latestInfo = googleApiHelper.fetchLatestUserInfo(accessToken);
attr.putAll(latestInfo);

return new GoogleResponseDto(attr);
}

if (registrationId.equalsIgnoreCase("github")) {
/* ① 기본 프로필에 e-mail 없으면 /user/emails 호출 */
if (attr.get("email") == null) {
Expand All @@ -105,6 +126,7 @@ private User createOrConnect(OAuth2ResponseDto response, OauthProvider provider,
return userRepository.findByEmailAndDeletedAtIsNull(response.getEmail())
.map(user -> {
// 사용자가 존재하면, 새 소셜 계정을 연결
user.updateProfile(response.getName(), response.getProfileImageUrl());
connectSocialAccount(user, provider, providerId, accessToken, refreshToken);
return user;
})
Expand Down Expand Up @@ -150,6 +172,7 @@ private void connectSocialAccount(User user, OauthProvider provider, String prov
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();

socialConnectionRepository.save(connection);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
import org.springframework.web.reactive.function.client.WebClientResponseException;
import reactor.core.publisher.Mono;

import java.util.Map;


@Slf4j
@Component
Expand Down Expand Up @@ -96,6 +98,26 @@ protected GoogleTokenResponse refreshToken(String refreshToken) {
}
}

/**
* access token 으로 최신 프로필(userinfo) 조회
* (name, picture, email 등)
*/
public Map<String, Object> fetchLatestUserInfo(String accessToken) {
try {
return webClient.get()
.uri("https://www.googleapis.com/oauth2/v2/userinfo")
.header("Authorization", "Bearer " + accessToken)
.retrieve()
.bodyToMono(Map.class)
.block();
} catch (WebClientResponseException e) {
log.error("Google userinfo 조회 실패: {}", e.getResponseBodyAsString());
throw new GlobalException(ErrorCode.OAUTH_COMMUNICATION_FAILED);
} catch (Exception e) {
log.error("Google userinfo 조회 중 오류: {}", e.getMessage());
throw new GlobalException(ErrorCode.OAUTH_COMMUNICATION_FAILED);
}
}

public Mono<Void> revokeToken(String accessToken) {
String revokeUrl = "https://accounts.google.com/o/oauth2/revoke";
Expand Down
Loading