diff --git a/EbayListingTool/EbayListingTool.csproj b/EbayListingTool/EbayListingTool.csproj index e7d79a6..4f84d80 100644 --- a/EbayListingTool/EbayListingTool.csproj +++ b/EbayListingTool/EbayListingTool.csproj @@ -24,6 +24,7 @@ + diff --git a/EbayListingTool/Services/EbayAuthService.cs b/EbayListingTool/Services/EbayAuthService.cs index 6552324..3afb69c 100644 --- a/EbayListingTool/Services/EbayAuthService.cs +++ b/EbayListingTool/Services/EbayAuthService.cs @@ -1,8 +1,8 @@ -using System.Diagnostics; -using System.Net; using System.Net.Http.Headers; using System.Text; +using System.Windows; using EbayListingTool.Models; +using EbayListingTool.Views; using Microsoft.Extensions.Configuration; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -27,7 +27,6 @@ public class EbayAuthService [ "https://api.ebay.com/oauth/api_scope", "https://api.ebay.com/oauth/api_scope/sell.inventory", - "https://api.ebay.com/oauth/api_scope/sell.listing", "https://api.ebay.com/oauth/api_scope/sell.fulfillment", "https://api.ebay.com/oauth/api_scope/sell.account" ]; @@ -69,7 +68,6 @@ public class EbayAuthService public async Task LoginAsync() { - var redirectUri = $"http://localhost:{_settings.RedirectPort}/"; var scopeString = Uri.EscapeDataString(string.Join(" ", Scopes)); var authBase = _settings.Sandbox ? "https://auth.sandbox.ebay.com/oauth2/authorize" @@ -79,39 +77,39 @@ public class EbayAuthService $"&redirect_uri={Uri.EscapeDataString(_settings.RuName)}" + $"&response_type=code&scope={scopeString}"; - // Start local listener before opening browser - using var listener = new HttpListener(); - listener.Prefixes.Add(redirectUri); - listener.Start(); + Log($"LoginAsync start — RuName={_settings.RuName}, ClientId={_settings.ClientId}"); - // Open browser - Process.Start(new ProcessStartInfo(authUrl) { UseShellExecute = true }); + string? code = null; - // Wait for redirect with code (60s timeout) - var contextTask = listener.GetContextAsync(); - if (await Task.WhenAny(contextTask, Task.Delay(TimeSpan.FromSeconds(60))) != contextTask) + // Open embedded browser dialog on UI thread and block until it closes + await Application.Current.Dispatcher.InvokeAsync(() => { - listener.Stop(); - throw new TimeoutException("eBay login timed out. Please try again."); + var win = new EbayLoginWindow(authUrl); + var result = win.ShowDialog(); + if (result == true) + code = win.AuthCode; + }); + + if (string.IsNullOrEmpty(code)) + { + Log("User cancelled login or no code returned"); + throw new InvalidOperationException("eBay login was cancelled or did not return an authorisation code."); } - var context = await contextTask; - var code = context.Request.QueryString["code"] - ?? throw new InvalidOperationException("No authorisation code received from eBay."); - - // Send OK page to browser - var responseHtml = "

Connected! You can close this tab.

"; - var responseBytes = Encoding.UTF8.GetBytes(responseHtml); - context.Response.ContentType = "text/html"; - context.Response.ContentLength64 = responseBytes.Length; - await context.Response.OutputStream.WriteAsync(responseBytes); - context.Response.Close(); - listener.Stop(); - + Log($"Auth code received (length={code.Length})"); await ExchangeCodeForTokenAsync(code); return _token!.EbayUsername; } + private static readonly string LogFile = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "EbayListingTool", "auth_log.txt"); + + private static void Log(string msg) + { + try { File.AppendAllText(LogFile, $"{DateTime.Now:HH:mm:ss} {msg}\n"); } catch { } + } + private async Task ExchangeCodeForTokenAsync(string code) { var tokenUrl = _settings.Sandbox @@ -130,20 +128,33 @@ public class EbayAuthService ["redirect_uri"] = _settings.RuName }); + Log($"Token exchange → POST {tokenUrl}"); var response = await _http.SendAsync(codeRequest); var json = await response.Content.ReadAsStringAsync(); + Log($"Token exchange ← {(int)response.StatusCode}: {json}"); + System.Diagnostics.Debug.WriteLine($"[eBay token exchange] {(int)response.StatusCode}: {json}"); if (!response.IsSuccessStatusCode) - throw new HttpRequestException($"Token exchange failed: {json}"); + throw new HttpRequestException($"Token exchange failed ({(int)response.StatusCode}): {json}"); var obj = JObject.Parse(json); + + var accessToken = obj["access_token"]?.ToString() + ?? throw new InvalidOperationException($"No access_token in response: {json}"); + var expiresIn = obj["expires_in"]?.Value() ?? 7200; + var refreshToken = obj["refresh_token"]?.ToString() ?? ""; + var refreshExpiresIn = obj["refresh_token_expires_in"]?.Value() ?? 0; + _token = new EbayToken { - AccessToken = obj["access_token"]!.ToString(), - RefreshToken = obj["refresh_token"]!.ToString(), - AccessTokenExpiry = DateTime.UtcNow.AddSeconds(obj["expires_in"]!.Value()), - RefreshTokenExpiry = DateTime.UtcNow.AddSeconds(obj["refresh_token_expires_in"]!.Value()), + AccessToken = accessToken, + RefreshToken = refreshToken, + AccessTokenExpiry = DateTime.UtcNow.AddSeconds(expiresIn), + RefreshTokenExpiry = refreshExpiresIn > 0 + ? DateTime.UtcNow.AddSeconds(refreshExpiresIn) + : DateTime.UtcNow.AddDays(18 * 30), // eBay default: 18 months }; + Log($"Token set — AccessToken length={accessToken.Length}, Expiry={_token.AccessTokenExpiry:HH:mm:ss}, IsValid={_token.IsAccessTokenValid}"); // Fetch username _token.EbayUsername = await FetchUsernameAsync(_token.AccessToken); diff --git a/EbayListingTool/Views/EbayLoginWindow.xaml b/EbayListingTool/Views/EbayLoginWindow.xaml new file mode 100644 index 0000000..bad421f --- /dev/null +++ b/EbayListingTool/Views/EbayLoginWindow.xaml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + diff --git a/EbayListingTool/Views/EbayLoginWindow.xaml.cs b/EbayListingTool/Views/EbayLoginWindow.xaml.cs new file mode 100644 index 0000000..d82c14b --- /dev/null +++ b/EbayListingTool/Views/EbayLoginWindow.xaml.cs @@ -0,0 +1,92 @@ +using System.IO; +using System.Web; +using System.Windows; +using Microsoft.Web.WebView2.Core; + +namespace EbayListingTool.Views; + +public partial class EbayLoginWindow : Window +{ + public string? AuthCode { get; private set; } + + private readonly string _authUrl; + + private static readonly string LogFile = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "EbayListingTool", "auth_log.txt"); + + private static void Log(string msg) + { + try { File.AppendAllText(LogFile, $"{DateTime.Now:HH:mm:ss} [Browser] {msg}\n"); } catch { } + } + + public EbayLoginWindow(string authUrl) + { + InitializeComponent(); + _authUrl = authUrl; + Loaded += OnLoaded; + } + + private async void OnLoaded(object sender, RoutedEventArgs e) + { + Log($"WebView2 initialising..."); + try + { + await Browser.EnsureCoreWebView2Async(); + Browser.CoreWebView2.NavigationStarting += CoreWebView2_NavigationStarting; + Log($"Navigating to auth URL"); + Browser.CoreWebView2.Navigate(_authUrl); + } + catch (Exception ex) + { + Log($"WebView2 init failed: {ex.Message}"); + MessageBox.Show($"Browser could not initialise: {ex.Message}\n\nEnsure Microsoft Edge WebView2 Runtime is installed.", + "Browser Error", MessageBoxButton.OK, MessageBoxImage.Error); + DialogResult = false; + Close(); + } + } + + private void CoreWebView2_NavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs e) + { + var url = e.Uri ?? ""; + Log($"NavigationStarting → {url}"); + + if (!url.Contains("ThirdPartyAuth", StringComparison.OrdinalIgnoreCase)) + return; + + e.Cancel = true; + Log($"ThirdPartyAuth intercepted: {url}"); + + try + { + var uri = new Uri(url); + var qs = HttpUtility.ParseQueryString(uri.Query); + var code = qs["code"]; + + Log($"Query params: {string.Join(", ", qs.AllKeys.Select(k => $"{k}={qs[k]?.Substring(0, Math.Min(qs[k]?.Length ?? 0, 30))}"))}"); + + if (!string.IsNullOrEmpty(code)) + { + AuthCode = code; + DialogResult = true; + } + else + { + var error = qs["error"] ?? qs["error_id"] ?? "unknown"; + var desc = qs["error_description"] ?? qs["error_message"] ?? ""; + Log($"No code — error={error}, desc={desc}"); + MessageBox.Show($"eBay login error: {error}\n{desc}", "Login Failed", + MessageBoxButton.OK, MessageBoxImage.Warning); + DialogResult = false; + } + } + catch (Exception ex) + { + Log($"Exception parsing redirect: {ex.Message}"); + DialogResult = false; + } + + Dispatcher.Invoke(Close); + } +} diff --git a/EbayListingTool/Views/MainWindow.xaml.cs b/EbayListingTool/Views/MainWindow.xaml.cs index 131bd7a..09965b5 100644 --- a/EbayListingTool/Views/MainWindow.xaml.cs +++ b/EbayListingTool/Views/MainWindow.xaml.cs @@ -63,7 +63,11 @@ public partial class MainWindow : MetroWindow MessageBox.Show(ex.Message, "eBay Login Failed", MessageBoxButton.OK, MessageBoxImage.Warning); } - finally { ConnectBtn.IsEnabled = true; } + finally + { + ConnectBtn.IsEnabled = true; + UpdateConnectionState(); // always sync UI to actual auth state + } } private void DisconnectBtn_Click(object sender, RoutedEventArgs e)