using System.Net; using System.Net.Http; using System.Net.Http.Json; using System.Text.Json; namespace AxCopilot.Services; /// /// IBM Cloud Pak for Data (CP4D) 토큰 발급 및 캐싱 서비스. /// CP4D의 /icp4d-api/v1/authorize 엔드포인트에서 Bearer 토큰을 발급받고, /// 만료 전까지 캐싱하여 재사용합니다. /// internal sealed class Cp4dTokenService { private static readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(15) }; // 캐시: (cp4dUrl + username) → (token, expiry) private static readonly Dictionary _cache = new(); private static readonly object _lock = new(); /// /// CP4D 토큰을 발급받거나 캐시에서 반환합니다. /// /// CP4D 서버 URL (예: https://cpd-host.example.com) /// CP4D 사용자 이름 /// CP4D 비밀번호 또는 API 키 /// 취소 토큰 /// Bearer 토큰 문자열. 실패 시 null. public static async Task GetTokenAsync(string cp4dUrl, string username, string password, CancellationToken ct = default) { if (string.IsNullOrWhiteSpace(cp4dUrl) || string.IsNullOrWhiteSpace(username)) return null; var cacheKey = $"{cp4dUrl}|{username}"; lock (_lock) { if (_cache.TryGetValue(cacheKey, out var cached) && cached.Expiry > DateTime.UtcNow.AddMinutes(1)) return cached.Token; } try { var tokenUrl = cp4dUrl.TrimEnd('/') + "/icp4d-api/v1/authorize"; string? json = null; HttpStatusCode? statusCode = null; string? lastErrorBody = null; async Task TryAuthorizeAsync(object body, string bodyKind) { using var req = new HttpRequestMessage(HttpMethod.Post, tokenUrl) { Content = JsonContent.Create(body) }; using var resp = await _http.SendAsync(req, ct); statusCode = resp.StatusCode; var rawBody = await resp.Content.ReadAsStringAsync(ct); if (!resp.IsSuccessStatusCode) { lastErrorBody = rawBody; LogService.Warn($"CP4D 토큰 발급 실패({bodyKind}): {resp.StatusCode} - {rawBody}"); return false; } json = rawBody; LogService.Info($"CP4D 토큰 발급 성공({bodyKind})"); return true; } var authorized = await TryAuthorizeAsync(new { username, password }, "username+password"); if (!authorized && !string.IsNullOrWhiteSpace(password)) authorized = await TryAuthorizeAsync(new { username, api_key = password }, "username+api_key"); if (!authorized || string.IsNullOrWhiteSpace(json)) { LogService.Warn($"CP4D 토큰 발급 최종 실패: {statusCode} - {lastErrorBody}"); return null; } using var doc = JsonDocument.Parse(json); if (!doc.RootElement.TryGetProperty("token", out var tokenProp)) { LogService.Warn("CP4D 응답에 token 필드가 없습니다."); return null; } var token = tokenProp.GetString(); if (string.IsNullOrEmpty(token)) return null; var expiry = DateTime.UtcNow.AddHours(11); if (doc.RootElement.TryGetProperty("accessTokenExpiry", out var expiryProp) && expiryProp.TryGetInt64(out var expiryMs)) { expiry = DateTimeOffset.FromUnixTimeMilliseconds(expiryMs).UtcDateTime; } lock (_lock) { _cache[cacheKey] = (token, expiry); } LogService.Info($"CP4D 토큰 발급 완료: {cp4dUrl} (만료: {expiry:yyyy-MM-dd HH:mm} UTC)"); return token; } catch (Exception ex) { LogService.Error($"CP4D 토큰 발급 오류: {ex.Message}"); return null; } } /// 특정 CP4D 서버의 캐시된 토큰을 무효화합니다. public static void InvalidateToken(string cp4dUrl, string username) { var cacheKey = $"{cp4dUrl}|{username}"; lock (_lock) { _cache.Remove(cacheKey); } } /// 모든 캐시된 토큰을 초기화합니다. public static void ClearAllTokens() { lock (_lock) { _cache.Clear(); } } }