diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 0ffc967..a97d884 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -20,7 +20,10 @@ "Bash(docker-compose up:*)", "Bash(dotnet build:*)", "Bash(chmod:*)", - "Bash(mv:*)" + "Bash(mv:*)", + "Bash(dotnet nuget locals:*)", + "Bash(/mnt/c/vscode/vcart.me.novo/clean-build.sh:*)", + "Bash(sed:*)" ] }, "enableAllProjectMcpServers": false diff --git a/BCards.sln b/BCards.sln index 14841e2..6548584 100644 --- a/BCards.sln +++ b/BCards.sln @@ -21,6 +21,7 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}" ProjectSection(SolutionItems) = preProject Conexoes.txt = Conexoes.txt + README.md = README.md EndProjectSection EndProject Global diff --git a/CLAUDE.md b/CLAUDE.md index a20ae60..5fb06df 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,10 +10,11 @@ BCards is a professional LinkTree clone built in ASP.NET Core MVC, targeting Bra ### Build & Run ```bash -# Restore dependencies -dotnet restore +# Quick clean build (RECOMMENDED after VS 2022 updates) +./clean-build.sh -# Build solution +# Manual process: +dotnet restore dotnet build # Run development server @@ -26,6 +27,23 @@ docker-compose up -d # Access: http://localhost:8080 ``` +### 🚨 Known Issues After VS 2022 Updates + +**Problem**: After VS 2022 updates, build cache gets corrupted causing: +- OAuth login failures (especially Google in Edge browser) +- Need for constant clean/rebuild cycles +- NuGet package resolution errors + +**Solution**: Use the automated cleanup script: +```bash +./clean-build.sh +``` + +**Google OAuth Edge Issue**: If Google login shows "browser not secure" error: +1. Clear browser data for localhost:49178 and accounts.google.com +2. Test in incognito/private mode +3. Use Vivaldi or Chrome (Edge has known compatibility issues) + ### Testing ```bash # Run all tests diff --git a/README.md b/README.md index bb9ede9..6a7e350 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Um clone profissional do LinkTree desenvolvido em ASP.NET Core MVC, focado no me ## 🚀 Características Principais ### ✨ Funcionalidades -- **URLs Hierárquicas**: Organização por categoria (ex: `vcart.me/corretor/jose-silva`) +- **URLs Hierárquicas**: Organização por categoria (ex: `bcards.site/corretor/jose-silva`) - **Sistema de Pagamentos**: Integração completa com Stripe - **3 Planos com Efeito Decoy**: Estratégia psicológica de pricing - **Autenticação OAuth**: Google e Microsoft @@ -86,7 +86,7 @@ Edite `appsettings.json` ou `appsettings.Development.json`: "ClientSecret": "seu_microsoft_client_secret" } }, - "BaseUrl": "https://vcart.me" + "BaseUrl": "https://bcards.site" } ``` @@ -254,7 +254,7 @@ Sistema de analytics integrado que rastreia: "SecretKey": "sk_live_seu_secret_key", "WebhookSecret": "whsec_seu_webhook_secret_producao" }, - "BaseUrl": "https://vcart.me" + "BaseUrl": "https://bcards.site" } ``` @@ -303,14 +303,14 @@ Este projeto está sob a licença MIT. Veja o arquivo [LICENSE](LICENSE) para de ## 🆘 Suporte Para suporte técnico, entre em contato: -- Email: suporte@vcart.me +- Email: suporte@bcards.site - Discord: [Servidor da Comunidade] - Issues: [GitHub Issues](https://github.com/seuusuario/bcards/issues) ## 📞 Contato -- **Website**: https://vcart.me -- **Email**: contato@vcart.me +- **Website**: https://bcards.site +- **Email**: contato@bcards.site - **LinkedIn**: [Seu LinkedIn] - **Twitter**: [@vcartme] diff --git a/clean-build.sh b/clean-build.sh new file mode 100644 index 0000000..93625c0 --- /dev/null +++ b/clean-build.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +echo "🧹 Iniciando limpeza completa do projeto BCards..." + +# 1. Limpar todos os caches NuGet +echo "📦 Limpando cache NuGet..." +dotnet nuget locals all --clear + +# 2. Remover pastas bin/obj recursivamente +echo "🗑️ Removendo pastas bin/obj..." +find . -name "bin" -type d -exec rm -rf {} + 2>/dev/null || true +find . -name "obj" -type d -exec rm -rf {} + 2>/dev/null || true + +# 3. Limpar solution +echo "🧽 Executando dotnet clean..." +dotnet clean --verbosity quiet + +# 4. Restaurar packages sem cache +echo "📥 Restaurando packages..." +dotnet restore --no-cache --force --verbosity quiet + +# 5. Build completo +echo "🔨 Executando build..." +dotnet build --no-restore --verbosity quiet + +if [ $? -eq 0 ]; then + echo "✅ Build concluído com sucesso!" + echo "🚀 Pronto para executar: dotnet run" +else + echo "❌ Build falhou! Verifique os erros acima." + exit 1 +fi + +echo "🎉 Limpeza completa finalizada!" \ No newline at end of file diff --git a/src/BCards.Web/Controllers/AuthController.cs b/src/BCards.Web/Controllers/AuthController.cs index c92485e..0009647 100644 --- a/src/BCards.Web/Controllers/AuthController.cs +++ b/src/BCards.Web/Controllers/AuthController.cs @@ -13,24 +13,102 @@ namespace BCards.Web.Controllers; public class AuthController : Controller { private readonly IAuthService _authService; + private readonly IOAuthHealthService _oauthHealthService; + private readonly ILogger _logger; - public AuthController(IAuthService authService) + public AuthController( + IAuthService authService, + IOAuthHealthService oauthHealthService, + ILogger logger) { _authService = authService; + _oauthHealthService = oauthHealthService; + _logger = logger; } [HttpGet] [Route("Login")] - public IActionResult Login(string? returnUrl = null) + public async Task Login(string? returnUrl = null) { ViewBag.ReturnUrl = returnUrl; + + // Verificar status dos OAuth providers e passar para a view + var oauthStatus = await _oauthHealthService.CheckOAuthProvidersAsync(); + ViewBag.OAuthStatus = oauthStatus; + return View(); } + /// + /// Endpoint AJAX para verificar status dos OAuth providers + /// + [HttpGet] + [Route("oauth-status")] + public async Task GetOAuthStatus() + { + try + { + var status = await _oauthHealthService.CheckOAuthProvidersAsync(); + return Json(new + { + googleAvailable = status.GoogleAvailable, + microsoftAvailable = status.MicrosoftAvailable, + allProvidersHealthy = status.AllProvidersHealthy, + anyProviderHealthy = status.AnyProviderHealthy, + message = GetOAuthStatusMessage(status) + }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Erro ao verificar status OAuth"); + return Json(new { error = "Erro ao verificar disponibilidade de login" }); + } + } + + private string GetOAuthStatusMessage(OAuthHealthStatus status) + { + if (status.AllProvidersHealthy) + return "Todos os métodos de login estão funcionando normalmente"; + + if (!status.AnyProviderHealthy) + return "⚠️ Login temporariamente indisponível. Tente novamente em alguns minutos."; + + var available = new List(); + var unavailable = new List(); + + if (status.GoogleAvailable) available.Add("Google"); + else unavailable.Add("Google"); + + if (status.MicrosoftAvailable) available.Add("Microsoft"); + else unavailable.Add("Microsoft"); + + return $"⚠️ Login com {string.Join(" e ", unavailable)} temporariamente indisponível. Use {string.Join(" ou ", available)}."; + } + [HttpPost] [Route("LoginWithGoogle")] - public IActionResult LoginWithGoogle(string? returnUrl = null) + public async Task LoginWithGoogle(string? returnUrl = null) { + // TEMPORARIAMENTE DESABILITADO - para testar se a verificação OAuth está causando problemas + /* + try + { + // Verificar se Google está disponível (com timeout curto para não travar UX) + var isGoogleAvailable = await _oauthHealthService.IsGoogleAvailableAsync(); + if (!isGoogleAvailable) + { + _logger.LogWarning("🟡 Usuário tentou fazer login com Google, mas o serviço parece estar offline"); + TempData["LoginError"] = "Login com Google pode estar temporariamente indisponível. Se o problema persistir, tente Microsoft."; + // Mas PERMITA o login mesmo assim - o próprio Google vai dar erro se estiver offline + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Erro ao verificar status Google - permitindo login mesmo assim"); + // Se não conseguir verificar, permitir login + } + */ + var redirectUrl = Url.Action("GoogleCallback", "Auth", new { returnUrl }); var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; return Challenge(properties, GoogleDefaults.AuthenticationScheme); @@ -38,8 +116,28 @@ public class AuthController : Controller [HttpPost] [Route("LoginWithMicrosoft")] - public IActionResult LoginWithMicrosoft(string? returnUrl = null) + public async Task LoginWithMicrosoft(string? returnUrl = null) { + // TEMPORARIAMENTE DESABILITADO - para testar se a verificação OAuth está causando problemas + /* + try + { + // Verificar se Microsoft está disponível (com timeout curto para não travar UX) + var isMicrosoftAvailable = await _oauthHealthService.IsMicrosoftAvailableAsync(); + if (!isMicrosoftAvailable) + { + _logger.LogWarning("🟡 Usuário tentou fazer login com Microsoft, mas o serviço parece estar offline"); + TempData["LoginError"] = "Login com Microsoft pode estar temporariamente indisponível. Se o problema persistir, tente Google."; + // Mas PERMITA o login mesmo assim - o próprio Microsoft vai dar erro se estiver offline + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Erro ao verificar status Microsoft - permitindo login mesmo assim"); + // Se não conseguir verificar, permitir login + } + */ + var redirectUrl = Url.Action("MicrosoftCallback", "Auth", new { returnUrl }); var properties = new AuthenticationProperties { RedirectUri = redirectUrl }; return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme); diff --git a/src/BCards.Web/Controllers/ImageController.cs b/src/BCards.Web/Controllers/ImageController.cs index eab5ac1..9ad0744 100644 --- a/src/BCards.Web/Controllers/ImageController.cs +++ b/src/BCards.Web/Controllers/ImageController.cs @@ -51,7 +51,7 @@ public class ImageController : ControllerBase } [HttpPost("upload")] - [RequestSizeLimit(5 * 1024 * 1024)] // 5MB máximo + [RequestSizeLimit(2 * 1024 * 1024)] // 2MB máximo - otimizado para celulares [DisableRequestSizeLimit] // Para formulários grandes public async Task UploadImage(IFormFile file) { @@ -75,11 +75,11 @@ public class ImageController : ControllerBase } // Validação de tamanho - if (file.Length > 5 * 1024 * 1024) // 5MB + if (file.Length > 2 * 1024 * 1024) // 2MB { _logger.LogWarning("File too large: {Size}MB", file.Length / (1024 * 1024)); return BadRequest(new { - error = "File too large. Maximum size is 5MB.", + error = "Arquivo muito grande. Tamanho máximo: 2MB.", code = "FILE_TOO_LARGE" }); } diff --git a/src/BCards.Web/HealthChecks/CriticalServicesHealthCheck.cs b/src/BCards.Web/HealthChecks/CriticalServicesHealthCheck.cs new file mode 100644 index 0000000..4534363 --- /dev/null +++ b/src/BCards.Web/HealthChecks/CriticalServicesHealthCheck.cs @@ -0,0 +1,139 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using MongoDB.Driver; +using MongoDB.Bson; +using Stripe; + +namespace BCards.Web.HealthChecks; + +/// +/// Health check para serviços CRÍTICOS - falha = ALERTA VERMELHO 🔴 +/// - MongoDB (páginas não funcionam) +/// - Stripe (pagamentos não funcionam) +/// - Live Pages (usuários não conseguem acessar páginas) +/// +public class CriticalServicesHealthCheck : IHealthCheck +{ + private readonly IMongoDatabase _database; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + + public CriticalServicesHealthCheck( + IMongoDatabase database, + HttpClient httpClient, + ILogger logger) + { + _database = database; + _httpClient = httpClient; + _logger = logger; + } + + public async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + var results = new Dictionary(); + var allCritical = true; + var criticalFailures = new List(); + + // 1. MongoDB Check + try + { + await _database.RunCommandAsync(new BsonDocument("ping", 1), cancellationToken: cancellationToken); + results["mongodb"] = new { status = "healthy", service = "database" }; + _logger.LogDebug("✅ MongoDB está respondendo"); + } + catch (Exception ex) + { + allCritical = false; + criticalFailures.Add("MongoDB"); + results["mongodb"] = new { status = "unhealthy", error = ex.Message, service = "database" }; + + _logger.LogError(ex, "🔴 CRÍTICO: MongoDB falhou - páginas de usuários não funcionam!"); + + // Pequeno delay para garantir que logs críticos sejam enviados + await Task.Delay(1000, cancellationToken); + } + + // 2. Stripe API Check + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + using var response = await _httpClient.GetAsync("https://api.stripe.com/healthcheck", cts.Token); + + if (response.IsSuccessStatusCode) + { + results["stripe"] = new { status = "healthy", service = "payment" }; + _logger.LogDebug("✅ Stripe API está respondendo"); + } + else + { + allCritical = false; + criticalFailures.Add("Stripe"); + results["stripe"] = new { status = "unhealthy", status_code = (int)response.StatusCode, service = "payment" }; + + _logger.LogError("🔴 CRÍTICO: Stripe API falhou - pagamentos não funcionam! Status: {StatusCode}", + (int)response.StatusCode); + await Task.Delay(1000, cancellationToken); + } + } + catch (Exception ex) + { + allCritical = false; + criticalFailures.Add("Stripe"); + results["stripe"] = new { status = "unhealthy", error = ex.Message, service = "payment" }; + + _logger.LogError(ex, "🔴 CRÍTICO: Stripe API inacessível - pagamentos não funcionam!"); + await Task.Delay(1000, cancellationToken); + } + + // 3. Self Health Check - verificar se o próprio site responde + try + { + var baseUrl = Environment.GetEnvironmentVariable("BASE_URL") ?? "https://bcards.site"; + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var response = await _httpClient.GetAsync($"{baseUrl}/health", cts.Token); + + if (response.IsSuccessStatusCode) + { + results["website"] = new { status = "healthy", service = "website" }; + _logger.LogDebug("✅ Website principal está respondendo"); + } + else + { + allCritical = false; + criticalFailures.Add("Website"); + results["website"] = new { status = "unhealthy", status_code = (int)response.StatusCode, service = "website" }; + + _logger.LogError("🔴 CRÍTICO: Website principal não responde! Status: {StatusCode}", + (int)response.StatusCode); + await Task.Delay(2000, cancellationToken); // Delay maior para site down + } + } + catch (Exception ex) + { + allCritical = false; + criticalFailures.Add("Website"); + results["website"] = new { status = "unhealthy", error = ex.Message, service = "website" }; + + _logger.LogError(ex, "🔴 CRÍTICO: Website principal inacessível!"); + await Task.Delay(2000, cancellationToken); + } + + var data = new Dictionary + { + { "services", results }, + { "critical_failures", criticalFailures }, + { "failure_count", criticalFailures.Count }, + { "total_critical_services", 3 } + }; + + if (!allCritical) + { + var failureList = string.Join(", ", criticalFailures); + _logger.LogError("🔴 ALERTA VERMELHO: Serviços críticos falharam: {Services}", failureList); + return HealthCheckResult.Unhealthy($"Serviços críticos falharam: {failureList}", data: data); + } + + return HealthCheckResult.Healthy("Todos os serviços críticos funcionando", data: data); + } +} \ No newline at end of file diff --git a/src/BCards.Web/HealthChecks/ExternalServicesHealthCheck.cs b/src/BCards.Web/HealthChecks/ExternalServicesHealthCheck.cs index 805cf51..85e9d43 100644 --- a/src/BCards.Web/HealthChecks/ExternalServicesHealthCheck.cs +++ b/src/BCards.Web/HealthChecks/ExternalServicesHealthCheck.cs @@ -77,7 +77,16 @@ public class ExternalServicesHealthCheck : IHealthCheck { "url", service.Value } }; - _logger.LogError(ex, "External service {Service} health check failed", service.Key); + // Usar Warning para OAuth providers (alerta amarelo) + if (service.Key.Contains("oauth")) + { + _logger.LogWarning("🟡 OAuth service {Service} offline - usuários não conseguem fazer login: {Error}", + service.Key, ex.Message); + } + else + { + _logger.LogError(ex, "🔴 Critical service {Service} failed", service.Key); + } } } diff --git a/src/BCards.Web/HealthChecks/OAuthProvidersHealthCheck.cs b/src/BCards.Web/HealthChecks/OAuthProvidersHealthCheck.cs new file mode 100644 index 0000000..ac522ec --- /dev/null +++ b/src/BCards.Web/HealthChecks/OAuthProvidersHealthCheck.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.Diagnostics.HealthChecks; +using BCards.Web.Services; + +namespace BCards.Web.HealthChecks; + +public class OAuthProvidersHealthCheck : IHealthCheck +{ + private readonly IOAuthHealthService _oauthHealthService; + private readonly ILogger _logger; + + public OAuthProvidersHealthCheck(IOAuthHealthService oauthHealthService, ILogger logger) + { + _oauthHealthService = oauthHealthService; + _logger = logger; + } + + public async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + try + { + var status = await _oauthHealthService.CheckOAuthProvidersAsync(); + + var data = new Dictionary + { + { "google_status", status.GoogleStatus }, + { "microsoft_status", status.MicrosoftStatus }, + { "all_providers_healthy", status.AllProvidersHealthy }, + { "any_provider_healthy", status.AnyProviderHealthy }, + { "checked_at", status.CheckedAt } + }; + + // Política de saúde: OAuth offline é DEGRADED, não UNHEALTHY + if (!status.AnyProviderHealthy) + { + _logger.LogError("🔴 CRÍTICO: Todos os OAuth providers estão offline - login totalmente indisponível!"); + return HealthCheckResult.Degraded("Todos os OAuth providers estão offline", data: data); + } + + if (!status.AllProvidersHealthy) + { + var offlineProviders = new List(); + if (!status.GoogleAvailable) offlineProviders.Add("Google"); + if (!status.MicrosoftAvailable) offlineProviders.Add("Microsoft"); + + _logger.LogWarning("🟡 OAuth providers offline: {Providers} - alguns usuários não conseguem fazer login", + string.Join(", ", offlineProviders)); + + return HealthCheckResult.Degraded($"OAuth providers offline: {string.Join(", ", offlineProviders)}", data: data); + } + + return HealthCheckResult.Healthy("Todos os OAuth providers estão funcionando", data: data); + } + catch (Exception ex) + { + _logger.LogError(ex, "🔴 Erro ao verificar saúde dos OAuth providers"); + return HealthCheckResult.Degraded($"Erro na verificação OAuth: {ex.Message}"); + } + } +} \ No newline at end of file diff --git a/src/BCards.Web/Program.cs b/src/BCards.Web/Program.cs index 7568ed5..ef41dd2 100644 --- a/src/BCards.Web/Program.cs +++ b/src/BCards.Web/Program.cs @@ -35,38 +35,58 @@ var loggerConfig = new LoggerConfiguration() .Enrich.WithProperty("Environment", builder.Environment.EnvironmentName) .Enrich.WithProperty("Hostname", hostname); +// Nova abordagem de logging: Tudo em ambos, mas com controle de volume if (isDevelopment) { - // Development: Log EVERYTHING to console with detailed formatting + // Development: Debug level + detalhes completos loggerConfig .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Information) - .MinimumLevel.Override("System", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Information) + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Override("System", LogEventLevel.Warning) + // Console: Tudo acima de Information .WriteTo.Async(a => a.Console( + restrictedToMinimumLevel: LogEventLevel.Information, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{Hostname}] {Message:lj} {Properties:j}{NewLine}{Exception}")); var seqUrl = builder.Configuration["Serilog:SeqUrl"]; if (!string.IsNullOrEmpty(seqUrl)) { var apiKey = builder.Configuration["Serilog:ApiKey"]; - loggerConfig.WriteTo.Async(a => a.Seq(seqUrl, apiKey: string.IsNullOrEmpty(apiKey) ? null : apiKey)); + // Seq: Tudo (incluindo Debug) + loggerConfig.WriteTo.Async(a => a.Seq(seqUrl, + apiKey: string.IsNullOrEmpty(apiKey) ? null : apiKey, + restrictedToMinimumLevel: LogEventLevel.Debug)); } } else { - // Production: Only errors to console, everything to Seq + // Production: Log balanceado - reduzir spam de requisições loggerConfig .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning) // Reduzir spam de requests + .MinimumLevel.Override("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore.StaticFiles", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("System", LogEventLevel.Warning) + // Docker/Console: Information e acima (reduzindo volume do log) .WriteTo.Async(a => a.Console( - restrictedToMinimumLevel: LogEventLevel.Error, - outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] [{Hostname}] {Message:lj}{NewLine}{Exception}")); + restrictedToMinimumLevel: LogEventLevel.Information, // Mudança: não só errors + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] [{Hostname}] {Message:lj}{NewLine}{Exception}")) + // Arquivo de log local para backup (rotativo) + .WriteTo.Async(a => a.File( + "/app/logs/bcards-.log", + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 7, // Manter 7 dias + restrictedToMinimumLevel: LogEventLevel.Warning, // Só warnings e errors em arquivo + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [{Hostname}] {Message:lj} {Properties:j}{NewLine}{Exception}")); var seqUrl = builder.Configuration["Serilog:SeqUrl"]; if (!string.IsNullOrEmpty(seqUrl)) { var apiKey = builder.Configuration["Serilog:ApiKey"]; + // Seq: Tudo (Information e acima) - Seq pode estar offline loggerConfig.WriteTo.Async(a => a.Seq(seqUrl, apiKey: string.IsNullOrEmpty(apiKey) ? null : apiKey, restrictedToMinimumLevel: LogEventLevel.Information)); @@ -185,20 +205,31 @@ builder.Services.AddAuthentication(options => var googleAuth = builder.Configuration.GetSection("Authentication:Google"); options.ClientId = googleAuth["ClientId"] ?? ""; options.ClientSecret = googleAuth["ClientSecret"] ?? ""; - options.CallbackPath = "/signin-google"; // Path explícito + options.CallbackPath = "/signin-google"; // Path padrão do Google OAuth options.BackchannelTimeout = TimeSpan.FromSeconds(30); - // 🔥 CORREÇÃO: Handler para falhas remotas (ex: usuário nega permissão) + // Adicionar configurações mais robustas para Edge + options.SaveTokens = true; + options.UsePkce = true; // Usar PKCE para maior segurança + + // 🔥 CORREÇÃO: Handler para falhas remotas + debugging melhorado options.Events = new OAuthEvents { OnRemoteFailure = context => { var logger = context.HttpContext.RequestServices.GetRequiredService>(); - logger.LogWarning("Google remote failure: {Failure}", context.Failure?.Message); + logger.LogError("🔴 Google OAuth falhou: {Failure}", context.Failure?.Message); + logger.LogError("🔴 User Agent: {UserAgent}", context.Request.Headers.UserAgent.ToString()); - context.Response.Redirect("/Auth/Login?error=remote_failure"); + context.Response.Redirect("/Auth/Login?error=google_oauth_failed"); context.HandleResponse(); return Task.CompletedTask; + }, + OnTicketReceived = context => + { + var logger = context.HttpContext.RequestServices.GetRequiredService>(); + logger.LogInformation("✅ Google OAuth ticket recebido com sucesso"); + return Task.CompletedTask; } }; }) @@ -335,50 +366,54 @@ builder.Services.AddMemoryCache(); builder.Services.AddRazorPages(); -// ===== CONFIGURAÇÃO DOS HEALTH CHECKS ===== +// ===== CONFIGURAÇÃO DOS HEALTH CHECKS POR GRAVIDADE ===== + +// OAuth Health Service (para verificações rápidas de login) +builder.Services.AddScoped(); + +// HttpClient otimizado para OAuth checks - usar implementação concreta +builder.Services.AddHttpClient(client => +{ + client.Timeout = TimeSpan.FromSeconds(5); + client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); +}); + builder.Services.AddHealthChecks() - // MongoDB Health Check usando configuração existente - .AddCheck( - name: "mongodb", - failureStatus: HealthStatus.Unhealthy, + // 🔴 CRÍTICO (ALERTA VERMELHO) - Serviços essenciais + .AddCheck( + name: "critical_services", + failureStatus: HealthStatus.Unhealthy, // Falha = site fora do ar + timeout: TimeSpan.FromSeconds(30)) + + // 🟡 DEGRADADO (ALERTA AMARELO) - OAuth providers + .AddCheck( + name: "oauth_providers", + failureStatus: HealthStatus.Degraded, // Falha = login offline, site funciona timeout: TimeSpan.FromSeconds(10)) - // Stripe Health Check - .AddCheck( - name: "stripe", - failureStatus: HealthStatus.Degraded, // Stripe não é crítico para funcionalidade básica - timeout: TimeSpan.FromSeconds(15)) - - // SendGrid Health Check + // 🟡 DEGRADADO (ALERTA AMARELO) - Serviços auxiliares .AddCheck( name: "sendgrid", - failureStatus: HealthStatus.Degraded, // Email não é crítico para funcionalidade básica + failureStatus: HealthStatus.Degraded, // Email não impede funcionamento timeout: TimeSpan.FromSeconds(10)) - // External Services (OAuth providers) - .AddCheck( - name: "external_services", - failureStatus: HealthStatus.Degraded, // OAuth pode estar indisponível temporariamente - timeout: TimeSpan.FromSeconds(20)) - - // System Resources + // 🟡 DEGRADADO (ALERTA AMARELO) - Recursos do sistema .AddCheck( name: "resources", - failureStatus: HealthStatus.Degraded, + failureStatus: HealthStatus.Degraded, // Performance reduzida timeout: TimeSpan.FromSeconds(5)); // Registrar health checks customizados no DI -builder.Services.AddTransient(); -builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); builder.Services.AddTransient(); -builder.Services.AddTransient(); builder.Services.AddTransient(); -// HttpClient para External Services Health Check -builder.Services.AddHttpClient(client => +// HttpClient para Critical Services Health Check +builder.Services.AddHttpClient(client => { - client.Timeout = TimeSpan.FromSeconds(10); - client.DefaultRequestHeaders.Add("User-Agent", "BCards-HealthCheck/1.0"); + client.Timeout = TimeSpan.FromSeconds(15); + client.DefaultRequestHeaders.Add("User-Agent", "BCards-CriticalCheck/1.0"); }); var app = builder.Build(); diff --git a/src/BCards.Web/Services/GridFSImageStorage.cs b/src/BCards.Web/Services/GridFSImageStorage.cs index cfc35b2..9dbb85b 100644 --- a/src/BCards.Web/Services/GridFSImageStorage.cs +++ b/src/BCards.Web/Services/GridFSImageStorage.cs @@ -14,7 +14,8 @@ public class GridFSImageStorage : IImageStorageService private readonly ILogger _logger; private const int TARGET_SIZE = 400; - private const int MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB + private const int MAX_FILE_SIZE = 2 * 1024 * 1024; // 2MB - optimal for mobile uploads + private const int MAX_RESOLUTION = 4000; // 4000x4000px máximo (16MP) - adequado para celulares modernos private static readonly string[] ALLOWED_TYPES = { "image/jpeg", "image/jpg", "image/png", "image/gif" }; public GridFSImageStorage(IMongoDatabase database, ILogger logger) @@ -36,10 +37,13 @@ public class GridFSImageStorage : IImageStorageService throw new ArgumentException("Image bytes cannot be null or empty"); if (imageBytes.Length > MAX_FILE_SIZE) - throw new ArgumentException($"File size exceeds maximum allowed size of {MAX_FILE_SIZE / (1024 * 1024)}MB"); + throw new ArgumentException($"Arquivo muito grande. Tamanho máximo permitido: {MAX_FILE_SIZE / (1024 * 1024)}MB"); if (!ALLOWED_TYPES.Contains(contentType.ToLower())) - throw new ArgumentException($"Content type {contentType} is not allowed"); + throw new ArgumentException($"Tipo de arquivo {contentType} não permitido"); + + // Validar resolução da imagem + await ValidateImageResolution(imageBytes); // Processar e redimensionar imagem var processedImage = await ProcessImageAsync(imageBytes); @@ -196,4 +200,26 @@ public class GridFSImageStorage : IImageStorageService var ratio = Math.Min((double)targetSize / originalWidth, (double)targetSize / originalHeight); return ((int)(originalWidth * ratio), (int)(originalHeight * ratio)); } + + private async Task ValidateImageResolution(byte[] imageBytes) + { + await Task.Run(() => + { + try + { + using var image = Image.Load(imageBytes); + + if (image.Width > MAX_RESOLUTION || image.Height > MAX_RESOLUTION) + { + throw new ArgumentException( + $"Resolução muito alta. Máximo permitido: {MAX_RESOLUTION}x{MAX_RESOLUTION}px. " + + $"Sua imagem: {image.Width}x{image.Height}px"); + } + } + catch (Exception ex) when (!(ex is ArgumentException)) + { + throw new ArgumentException("Arquivo de imagem inválido ou corrompido"); + } + }); + } } \ No newline at end of file diff --git a/src/BCards.Web/Services/IOAuthHealthService.cs b/src/BCards.Web/Services/IOAuthHealthService.cs new file mode 100644 index 0000000..f87bc50 --- /dev/null +++ b/src/BCards.Web/Services/IOAuthHealthService.cs @@ -0,0 +1,20 @@ +namespace BCards.Web.Services; + +public interface IOAuthHealthService +{ + Task CheckOAuthProvidersAsync(); + Task IsGoogleAvailableAsync(); + Task IsMicrosoftAvailableAsync(); + Task LogOAuthStatusAsync(); +} + +public class OAuthHealthStatus +{ + public bool GoogleAvailable { get; set; } + public bool MicrosoftAvailable { get; set; } + public bool AllProvidersHealthy => GoogleAvailable && MicrosoftAvailable; + public bool AnyProviderHealthy => GoogleAvailable || MicrosoftAvailable; + public string GoogleStatus => GoogleAvailable ? "online" : "offline"; + public string MicrosoftStatus => MicrosoftAvailable ? "online" : "offline"; + public DateTime CheckedAt { get; set; } = DateTime.UtcNow; +} \ No newline at end of file diff --git a/src/BCards.Web/Services/OAuthHealthService.cs b/src/BCards.Web/Services/OAuthHealthService.cs new file mode 100644 index 0000000..270139c --- /dev/null +++ b/src/BCards.Web/Services/OAuthHealthService.cs @@ -0,0 +1,127 @@ +using System.Net; + +namespace BCards.Web.Services; + +public class OAuthHealthService : IOAuthHealthService +{ + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private static OAuthHealthStatus? _cachedStatus; + private static DateTime _lastCheck = DateTime.MinValue; + private static readonly TimeSpan CacheTimeout = TimeSpan.FromMinutes(1); // Reduzido para 1 minuto + + public OAuthHealthService(HttpClient httpClient, ILogger logger) + { + _httpClient = httpClient; + _logger = logger; + + // IMPORTANTE: Não modificar o HttpClient aqui para evitar conflitos + // O timeout já foi configurado no Program.cs + } + + public async Task CheckOAuthProvidersAsync() + { + // Usar cache se ainda válido + if (_cachedStatus != null && DateTime.UtcNow - _lastCheck < CacheTimeout) + { + return _cachedStatus; + } + + var status = new OAuthHealthStatus(); + + var tasks = new[] + { + CheckProviderAsync("Google", "https://accounts.google.com"), + CheckProviderAsync("Microsoft", "https://login.microsoftonline.com") + }; + + var results = await Task.WhenAll(tasks); + status.GoogleAvailable = results[0]; + status.MicrosoftAvailable = results[1]; + + // Cache o resultado + _cachedStatus = status; + _lastCheck = DateTime.UtcNow; + + return status; + } + + public async Task IsGoogleAvailableAsync() + { + var status = await CheckOAuthProvidersAsync(); + return status.GoogleAvailable; + } + + public async Task IsMicrosoftAvailableAsync() + { + var status = await CheckOAuthProvidersAsync(); + return status.MicrosoftAvailable; + } + + public async Task LogOAuthStatusAsync() + { + var status = await CheckOAuthProvidersAsync(); + + if (!status.GoogleAvailable) + { + _logger.LogWarning("🟡 OAuth Provider Google está OFFLINE - usuários não conseguem fazer login com Google"); + } + + if (!status.MicrosoftAvailable) + { + _logger.LogWarning("🟡 OAuth Provider Microsoft está OFFLINE - usuários não conseguem fazer login com Microsoft"); + } + + if (status.AllProvidersHealthy) + { + _logger.LogInformation("✅ Todos os OAuth providers estão funcionando normalmente"); + } + else if (!status.AnyProviderHealthy) + { + _logger.LogError("🔴 CRÍTICO: TODOS os OAuth providers estão offline - login completamente indisponível!"); + } + } + + private async Task CheckProviderAsync(string provider, string url) + { + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(8)); // Aumentei para 8 segundos + using var response = await _httpClient.GetAsync(url, cts.Token); + + // Aceitar tanto 200 quanto 302/redirects como saudável + var isHealthy = response.IsSuccessStatusCode || + response.StatusCode == System.Net.HttpStatusCode.Found || + response.StatusCode == System.Net.HttpStatusCode.Moved || + response.StatusCode == System.Net.HttpStatusCode.MovedPermanently; + + if (!isHealthy) + { + _logger.LogWarning("OAuth provider {Provider} retornou status {StatusCode}", + provider, (int)response.StatusCode); + } else { + _logger.LogDebug("OAuth provider {Provider} está saudável: {StatusCode}", + provider, (int)response.StatusCode); + } + + return isHealthy; + } + catch (TaskCanceledException) + { + _logger.LogWarning("OAuth provider {Provider} timeout após 8 segundos", provider); + return false; + } + catch (HttpRequestException ex) + { + _logger.LogWarning("OAuth provider {Provider} erro de conexão: {Error}", provider, ex.Message); + return false; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Erro ao verificar OAuth provider {Provider} - assumindo disponível para não bloquear login", provider); + // MUDANÇA CRÍTICA: Se houver erro desconhecido, assumir que está disponível + // para não bloquear login dos usuários + return true; + } + } +} \ No newline at end of file diff --git a/src/BCards.Web/Views/Admin/ManagePage.cshtml b/src/BCards.Web/Views/Admin/ManagePage.cshtml index 69f5eac..b9e0386 100644 --- a/src/BCards.Web/Views/Admin/ManagePage.cshtml +++ b/src/BCards.Web/Views/Admin/ManagePage.cshtml @@ -119,7 +119,7 @@
- Formatos aceitos: JPG, PNG, GIF. Máximo: 5MB. Será redimensionada para 400x400px. + Formatos aceitos: JPG, PNG, GIF. Máximo: 2MB e 4000x4000px. Será redimensionada para 400x400px.
@@ -1599,8 +1599,8 @@ return; } - if (file.size > 5 * 1024 * 1024) { // 5MB - errorSpan.text('Arquivo muito grande. Máximo 5MB.'); + if (file.size > 2 * 1024 * 1024) { // 2MB + errorSpan.text('Arquivo muito grande. Máximo 2MB.'); fileInput.val(''); return; } diff --git a/src/BCards.Web/Views/Legal/CommunityGuidelines.cshtml b/src/BCards.Web/Views/Legal/CommunityGuidelines.cshtml index 8f3d62a..327e97e 100644 --- a/src/BCards.Web/Views/Legal/CommunityGuidelines.cshtml +++ b/src/BCards.Web/Views/Legal/CommunityGuidelines.cshtml @@ -48,7 +48,7 @@ diff --git a/src/BCards.Web/Views/Legal/Privacy.cshtml b/src/BCards.Web/Views/Legal/Privacy.cshtml index 3a961f4..8524e01 100644 --- a/src/BCards.Web/Views/Legal/Privacy.cshtml +++ b/src/BCards.Web/Views/Legal/Privacy.cshtml @@ -73,7 +73,7 @@

8. Contato do Encarregado de Proteção de Dados (DPO)

Para qualquer dúvida sobre esta Política de Privacidade ou para exercer seus direitos, entre em contato com nosso DPO:

-

E-mail: dpo@vcart.me

+

E-mail: dpo@bcards.site

9. Alterações a esta Política

Podemos atualizar esta Política de Privacidade periodicamente. Notificaremos você sobre quaisquer alterações significativas através de um aviso em nosso site ou por e-mail.

diff --git a/src/BCards.Web/Views/Legal/PrivacyES.cshtml b/src/BCards.Web/Views/Legal/PrivacyES.cshtml index 602fed8..f51cc22 100644 --- a/src/BCards.Web/Views/Legal/PrivacyES.cshtml +++ b/src/BCards.Web/Views/Legal/PrivacyES.cshtml @@ -54,7 +54,7 @@

6. Contacto del Oficial de Protección de Datos (DPO)

Para cualquier pregunta sobre esta Política de Privacidad o para ejercer sus derechos, contacte a nuestro DPO:

-

Correo Electrónico: dpo@vcart.me

+

Correo Electrónico: dpo@bcards.site

7. Cambios a esta Política

Podemos actualizar esta Política de Privacidad periódicamente. Le notificaremos sobre cualquier cambio significativo a través de un aviso en nuestro sitio web o por correo electrónico.

diff --git a/src/BCards.Web/Views/Shared/_Layout.cshtml b/src/BCards.Web/Views/Shared/_Layout.cshtml index a358780..059f0fe 100644 --- a/src/BCards.Web/Views/Shared/_Layout.cshtml +++ b/src/BCards.Web/Views/Shared/_Layout.cshtml @@ -229,7 +229,7 @@
Empresa