diff --git a/Program.cs b/Program.cs index 1dea0f7..58396cd 100644 --- a/Program.cs +++ b/Program.cs @@ -127,7 +127,7 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); +// builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Program/Services/V1/Interfaces/IKakaoService.cs b/Program/Services/V1/Interfaces/IKakaoService.cs new file mode 100644 index 0000000..e24f23e --- /dev/null +++ b/Program/Services/V1/Interfaces/IKakaoService.cs @@ -0,0 +1,12 @@ +using Back.Program.Common.Model; + +namespace Back.Program.Services.V1.Interfaces; + +public interface IKakaoService +{ + Task GetAccessToken(string code); + Task GetAuthorizationUrl(string scope); + Task<(bool Success, string Response)> Redirect(string code); + Task<(bool Success, string Response)> Logout(string accessToken); + Task<(bool Success, string Response)> Unlink(string accessToken); +} \ No newline at end of file diff --git a/Program/Services/V1/KakaoService.cs b/Program/Services/V1/KakaoService.cs new file mode 100644 index 0000000..efffa87 --- /dev/null +++ b/Program/Services/V1/KakaoService.cs @@ -0,0 +1,135 @@ +using System.Text.Json; +using System.Net.Http.Headers; + +using Back.Program.Services.V1.Interfaces; + +namespace Back.Program.Services.V1; + +public class KakaoService: IKakaoService +{ + private readonly HttpClient _httpClient; + private const string KAKAO_API_BASE_URL = "https://kapi.kakao.com"; + private const string KAKAO_AUTH_BASE_URL = "https://kauth.kakao.com"; + private readonly string _clientId; + private readonly string _clientSecret; + private readonly string _redirectUri; + + public KakaoService(HttpClient httpClient, IConfiguration configuration) + { + _httpClient = httpClient; + _clientId = configuration["Kakao:ClientId"] ?? throw new InvalidOperationException("Kakao:ClientId not configured"); + _redirectUri = configuration["Kakao:RedirectUri"] ?? throw new InvalidOperationException("Kakao:RedirectUri not configured"); + _clientSecret = configuration["Kakao:ClientSecret"] ?? throw new InvalidOperationException("Kakao:ClientSecret not configured"); + } + + private void SetHeaders(string accessToken) + { + _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + } + private async Task Call(HttpMethod method, string url, HttpContent? content = null) + { + try + { + var request = new HttpRequestMessage(method, url); + if (content != null) + { + request.Content = content; + } + + var response = await _httpClient.SendAsync(request); + var responseContent = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + { + return JsonSerializer.Serialize(new { error = $"HTTP {(int)response.StatusCode}: {responseContent}" }); + } + + return responseContent; + } + catch (Exception ex) + { + return JsonSerializer.Serialize(new { error = ex.Message }); + } + } + + public async Task GetAccessToken(string code) + { + var content = new FormUrlEncodedContent(new[] + { + new KeyValuePair("grant_type", "authorization_code"), + new KeyValuePair("client_id", _clientId), + new KeyValuePair("redirect_uri", _redirectUri), + new KeyValuePair("code", code), + new KeyValuePair("client_secret", _clientSecret) + }); + content.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded"); + + var response = await Call(HttpMethod.Post, $"{KAKAO_AUTH_BASE_URL}/oauth/token", content); + var responseData = JsonSerializer.Deserialize(response); + + if (responseData.TryGetProperty("error", out var error)) + { + return response; + } + + if (!responseData.TryGetProperty("access_token", out var accessToken)) + { + return JsonSerializer.Serialize(new { error = "Access token is missing from response" }); + } + + return JsonSerializer.Serialize(new { access_token = accessToken.GetString() }); + } + + public Task GetAuthorizationUrl(string scope) + { + var authUrl = $"{KAKAO_AUTH_BASE_URL}/oauth/authorize?client_id={_clientId}&redirect_uri={_redirectUri}&response_type=code"; + if (!string.IsNullOrEmpty(scope)) + { + authUrl += $"&scope={scope}"; + } + return Task.FromResult(authUrl); + } + + public async Task<(bool Success, string Response)> Redirect(string code) + { + if (string.IsNullOrEmpty(code)) + return (false, JsonSerializer.Serialize(new { error = "Authorization code not found" })); + + var response = await GetAccessToken(code); + var responseData = JsonSerializer.Deserialize(response); + + if (responseData.TryGetProperty("error", out var error)) + { + return (false, response); + } + + var accessToken = responseData.GetProperty("access_token").GetString(); + if (string.IsNullOrEmpty(accessToken)) + { + return (false, response); + } + + return (true, accessToken); + } + + public async Task<(bool Success, string Response)> Logout(string accessToken) + { + if (string.IsNullOrEmpty(accessToken)) + return (false, JsonSerializer.Serialize(new { error = "Not logged in" })); + + SetHeaders(accessToken); + var response = await Call(HttpMethod.Post, $"{KAKAO_API_BASE_URL}/v1/user/logout"); + return (true, response); + } + + public async Task<(bool Success, string Response)> Unlink(string accessToken) + { + if (string.IsNullOrEmpty(accessToken)) + return (false, JsonSerializer.Serialize(new { error = "Not logged in" })); + + SetHeaders(accessToken); + var response = await Call(HttpMethod.Post, $"{KAKAO_API_BASE_URL}/v1/user/unlink"); + return (true, response); + } +} \ No newline at end of file