h-lab/src/main/java/com/hlab/yanalyst/domain/upload/YoutubeOAuthController.java

109 lines
4.7 KiB
Java

package com.hlab.yanalyst.domain.upload;
import com.hlab.yanalyst.global.common.ApiResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.List;
import java.util.UUID;
@Controller
@RequestMapping("/api/youtube")
@RequiredArgsConstructor
public class YoutubeOAuthController {
private static final String STATE_SESSION_KEY = "youtube.oauth.state";
private final YoutubeOAuthService service;
private final YoutubeOAuthLocalSettings localSettings;
@Value("${youtube.oauth.setup-token:}")
private String setupToken;
@GetMapping("/oauth/start")
public ResponseEntity<Void> start(HttpSession session) {
String state = UUID.randomUUID().toString();
session.setAttribute(STATE_SESSION_KEY, state);
return ResponseEntity.status(302).location(service.authorizationUri(state)).build();
}
@GetMapping("/oauth/callback")
public ResponseEntity<Void> callback(@RequestParam(required = false) String code,
@RequestParam(required = false) String state,
@RequestParam(required = false) String error,
HttpSession session) {
String expected = (String) session.getAttribute(STATE_SESSION_KEY);
session.removeAttribute(STATE_SESSION_KEY);
if (error != null) return redirect("/upload-studio?youtube=denied");
if (expected == null || !expected.equals(state)) return redirect("/upload-studio?youtube=invalid-state");
if (code == null || code.isBlank()) return redirect("/upload-studio?youtube=missing-code");
try {
service.connect(code);
return redirect("/upload-studio?youtube=connected");
} catch (RuntimeException e) {
return redirect("/upload-studio?youtube=failed");
}
}
@GetMapping("/connections")
@ResponseBody
public ApiResponse<List<YoutubeOAuthService.ConnectionDto>> connections() {
return ApiResponse.ok(service.connections());
}
@GetMapping("/settings/status")
@ResponseBody
public ApiResponse<SettingsStatus> settingsStatus() {
YoutubeOAuthLocalSettings.Status status = localSettings.status();
return ApiResponse.ok(new SettingsStatus(status.configured(), status.redirectUri(),
setupToken != null && !setupToken.isBlank()));
}
@PostMapping("/settings")
@ResponseBody
public ApiResponse<YoutubeOAuthLocalSettings.Status> saveSettings(@RequestBody SettingsRequest body,
HttpServletRequest request) {
requireSetupToken(request.getHeader("X-YouTube-Setup-Token"));
localSettings.save(body.clientId(), body.clientSecret(), body.redirectUri());
return ApiResponse.ok(localSettings.status());
}
private void requireSetupToken(String provided) {
if (setupToken == null || setupToken.isBlank()) {
throw new IllegalStateException("서버에 YOUTUBE_SETUP_TOKEN을 먼저 설정해 주세요.");
}
byte[] expected = setupToken.getBytes(StandardCharsets.UTF_8);
byte[] actual = (provided == null ? "" : provided).getBytes(StandardCharsets.UTF_8);
if (!MessageDigest.isEqual(expected, actual)) {
throw new IllegalArgumentException("관리자 설정 토큰이 올바르지 않습니다.");
}
}
@DeleteMapping("/connections/{id}")
@ResponseBody
public ApiResponse<Void> disconnect(@PathVariable Long id) {
service.disconnect(id);
return ApiResponse.ok(null);
}
private ResponseEntity<Void> redirect(String location) {
return ResponseEntity.status(302).location(URI.create(location)).build();
}
public record SettingsRequest(String clientId, String clientSecret, String redirectUri) { }
public record SettingsStatus(boolean configured, String redirectUri, boolean setupTokenConfigured) { }
}