using Microsoft.Extensions.Diagnostics.HealthChecks; using System.Diagnostics; using System.Text.Json; namespace QRRapidoApp.Services.HealthChecks { public class ExternalServicesHealthCheck : IHealthCheck { private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly HttpClient _httpClient; private readonly int _timeoutSeconds; private readonly bool _testStripeConnection; private readonly bool _testGoogleAuth; private readonly bool _testMicrosoftAuth; public ExternalServicesHealthCheck( IConfiguration configuration, ILogger logger, IHttpClientFactory httpClientFactory) { _configuration = configuration; _logger = logger; _httpClient = httpClientFactory.CreateClient(); _timeoutSeconds = configuration.GetValue("HealthChecks:ExternalServices:TimeoutSeconds", 10); _testStripeConnection = configuration.GetValue("HealthChecks:ExternalServices:TestStripeConnection", true); _testGoogleAuth = configuration.GetValue("HealthChecks:ExternalServices:TestGoogleAuth", false); _testMicrosoftAuth = configuration.GetValue("HealthChecks:ExternalServices:TestMicrosoftAuth", false); } public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { var stopwatch = Stopwatch.StartNew(); var data = new Dictionary(); var services = new List(); var issues = new List(); var warnings = new List(); try { using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(_timeoutSeconds)); using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); // Test Stripe connection if (_testStripeConnection) { var stripeResult = await TestStripeConnectionAsync(combinedCts.Token); services.Add(stripeResult); if (stripeResult.GetProperty("status").GetString() == "error") { issues.Add($"Stripe: {stripeResult.GetProperty("error").GetString()}"); } else if (stripeResult.GetProperty("status").GetString() == "warning") { warnings.Add($"Stripe: slow response ({stripeResult.GetProperty("latencyMs").GetInt32()}ms)"); } } // Test Google OAuth endpoints if (_testGoogleAuth) { var googleResult = await TestGoogleAuthAsync(combinedCts.Token); services.Add(googleResult); if (googleResult.GetProperty("status").GetString() == "error") { warnings.Add($"Google Auth: {googleResult.GetProperty("error").GetString()}"); } } // Test Microsoft OAuth endpoints if (_testMicrosoftAuth) { var microsoftResult = await TestMicrosoftAuthAsync(combinedCts.Token); services.Add(microsoftResult); if (microsoftResult.GetProperty("status").GetString() == "error") { warnings.Add($"Microsoft Auth: {microsoftResult.GetProperty("error").GetString()}"); } } stopwatch.Stop(); data["services"] = services; data["totalCheckTimeMs"] = stopwatch.ElapsedMilliseconds; data["status"] = DetermineOverallStatus(issues.Count, warnings.Count); // Return appropriate health status if (issues.Any()) { return HealthCheckResult.Unhealthy($"External service issues: {string.Join(", ", issues)}", data: data); } if (warnings.Any()) { return HealthCheckResult.Degraded($"External service warnings: {string.Join(", ", warnings)}", data: data); } return HealthCheckResult.Healthy($"All external services healthy ({services.Count} services checked)", data: data); } catch (OperationCanceledException) { return HealthCheckResult.Unhealthy($"External services health check timed out after {_timeoutSeconds} seconds", data: data); } catch (Exception ex) { _logger.LogError(ex, "External services health check failed"); return HealthCheckResult.Unhealthy($"External services health check failed: {ex.Message}", data: data); } } private async Task TestStripeConnectionAsync(CancellationToken cancellationToken) { var serviceStopwatch = Stopwatch.StartNew(); try { var secretKey = _configuration["Stripe:SecretKey"]; if (string.IsNullOrEmpty(secretKey) || secretKey.StartsWith("sk_test_xxxxx")) { return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "stripe", status = "warning", error = "Stripe not configured", latencyMs = serviceStopwatch.ElapsedMilliseconds })).RootElement; } // Test Stripe API connectivity by hitting a simple endpoint var request = new HttpRequestMessage(HttpMethod.Get, "https://api.stripe.com/v1/account"); request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", secretKey); var response = await _httpClient.SendAsync(request, cancellationToken); serviceStopwatch.Stop(); var latencyMs = serviceStopwatch.ElapsedMilliseconds; if (response.IsSuccessStatusCode) { var status = latencyMs > 2000 ? "warning" : "ok"; return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "stripe", status = status, statusCode = (int)response.StatusCode, latencyMs = latencyMs, lastChecked = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") })).RootElement; } else { return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "stripe", status = "error", statusCode = (int)response.StatusCode, error = $"HTTP {response.StatusCode}", latencyMs = latencyMs })).RootElement; } } catch (Exception ex) { serviceStopwatch.Stop(); _logger.LogWarning(ex, "Failed to test Stripe connection"); return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "stripe", status = "error", error = ex.Message, latencyMs = serviceStopwatch.ElapsedMilliseconds })).RootElement; } } private async Task TestGoogleAuthAsync(CancellationToken cancellationToken) { var serviceStopwatch = Stopwatch.StartNew(); try { var clientId = _configuration["Authentication:Google:ClientId"]; if (string.IsNullOrEmpty(clientId) || clientId == "your-google-client-id") { return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "google_auth", status = "warning", error = "Google Auth not configured", latencyMs = serviceStopwatch.ElapsedMilliseconds })).RootElement; } // Test Google's OAuth discovery document var response = await _httpClient.GetAsync("https://accounts.google.com/.well-known/openid_configuration", cancellationToken); serviceStopwatch.Stop(); var latencyMs = serviceStopwatch.ElapsedMilliseconds; if (response.IsSuccessStatusCode) { return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "google_auth", status = "ok", statusCode = (int)response.StatusCode, latencyMs = latencyMs, lastChecked = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") })).RootElement; } else { return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "google_auth", status = "error", statusCode = (int)response.StatusCode, error = $"HTTP {response.StatusCode}", latencyMs = latencyMs })).RootElement; } } catch (Exception ex) { serviceStopwatch.Stop(); _logger.LogWarning(ex, "Failed to test Google Auth connection"); return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "google_auth", status = "error", error = ex.Message, latencyMs = serviceStopwatch.ElapsedMilliseconds })).RootElement; } } private async Task TestMicrosoftAuthAsync(CancellationToken cancellationToken) { var serviceStopwatch = Stopwatch.StartNew(); try { var clientId = _configuration["Authentication:Microsoft:ClientId"]; if (string.IsNullOrEmpty(clientId) || clientId == "your-microsoft-client-id") { return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "microsoft_auth", status = "warning", error = "Microsoft Auth not configured", latencyMs = serviceStopwatch.ElapsedMilliseconds })).RootElement; } // Test Microsoft's OAuth discovery document var response = await _httpClient.GetAsync("https://login.microsoftonline.com/common/v2.0/.well-known/openid_configuration", cancellationToken); serviceStopwatch.Stop(); var latencyMs = serviceStopwatch.ElapsedMilliseconds; if (response.IsSuccessStatusCode) { return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "microsoft_auth", status = "ok", statusCode = (int)response.StatusCode, latencyMs = latencyMs, lastChecked = DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ssZ") })).RootElement; } else { return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "microsoft_auth", status = "error", statusCode = (int)response.StatusCode, error = $"HTTP {response.StatusCode}", latencyMs = latencyMs })).RootElement; } } catch (Exception ex) { serviceStopwatch.Stop(); _logger.LogWarning(ex, "Failed to test Microsoft Auth connection"); return JsonDocument.Parse(JsonSerializer.Serialize(new { service = "microsoft_auth", status = "error", error = ex.Message, latencyMs = serviceStopwatch.ElapsedMilliseconds })).RootElement; } } private string DetermineOverallStatus(int issueCount, int warningCount) { if (issueCount > 0) { return "error"; } if (warningCount > 0) { return "warning"; } return "ok"; } } }