using OED.Api.Core.Interfaces.Services; namespace OED.Api.Infrastructure.Esi; public interface IEsiTokenRefreshService { Task RefreshAsync(long characterId); } public class EsiTokenRefreshService( IHttpClientFactory httpClientFactory, ITokenStore tokenStore, IConfiguration config ) : IEsiTokenRefreshService { public async Task RefreshAsync(long characterId) { var refreshToken = await tokenStore.GetRefreshTokenAsync(characterId) ?? throw new InvalidOperationException($"No refresh token found for character {characterId}"); var clientId = config["Eve:ClientId"]!; var secretKey = config["Eve:SecretKey"]!; var credentials = Convert.ToBase64String( System.Text.Encoding.UTF8.GetBytes($"{clientId}:{secretKey}") ); var client = httpClientFactory.CreateClient(); var request = new HttpRequestMessage(HttpMethod.Post, "https://login.eveonline.com/v2/oauth/token"); request.Headers.Add("Authorization", $"Basic {credentials}"); request.Content = new FormUrlEncodedContent(new Dictionary { ["grant_type"] = "refresh_token", ["refresh_token"] = refreshToken }); var response = await client.SendAsync(request); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadFromJsonAsync() ?? throw new InvalidOperationException("Empty token response from EVE SSO"); // Always overwrite — EVE may rotate the refresh token await tokenStore.SetRefreshTokenAsync(characterId, result.RefreshToken); await tokenStore.SetAccessTokenAsync(characterId, result.AccessToken, TimeSpan.FromSeconds(result.ExpiresIn - 30)); return result.AccessToken; } } public record EsiTokenResponse( string access_token, int expires_in, string token_type, string refresh_token ) { public string AccessToken => access_token; public int ExpiresIn => expires_in; public string RefreshToken => refresh_token; }