fix: ajustes de notificações e restrição de tamanho da imagem
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 4s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 7m58s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m39s
BCards Deployment Pipeline / Deploy to Staging (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 1s

This commit is contained in:
Ricardo Carneiro 2025-09-05 17:49:26 -03:00
parent 3becbe67c3
commit d700bd35a9
19 changed files with 638 additions and 67 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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]

34
clean-build.sh Normal file
View File

@ -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!"

View File

@ -13,24 +13,102 @@ namespace BCards.Web.Controllers;
public class AuthController : Controller
{
private readonly IAuthService _authService;
private readonly IOAuthHealthService _oauthHealthService;
private readonly ILogger<AuthController> _logger;
public AuthController(IAuthService authService)
public AuthController(
IAuthService authService,
IOAuthHealthService oauthHealthService,
ILogger<AuthController> logger)
{
_authService = authService;
_oauthHealthService = oauthHealthService;
_logger = logger;
}
[HttpGet]
[Route("Login")]
public IActionResult Login(string? returnUrl = null)
public async Task<IActionResult> 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();
}
/// <summary>
/// Endpoint AJAX para verificar status dos OAuth providers
/// </summary>
[HttpGet]
[Route("oauth-status")]
public async Task<IActionResult> 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<string>();
var unavailable = new List<string>();
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<IActionResult> 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<IActionResult> 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);

View File

@ -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<IActionResult> 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"
});
}

View File

@ -0,0 +1,139 @@
using Microsoft.Extensions.Diagnostics.HealthChecks;
using MongoDB.Driver;
using MongoDB.Bson;
using Stripe;
namespace BCards.Web.HealthChecks;
/// <summary>
/// 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)
/// </summary>
public class CriticalServicesHealthCheck : IHealthCheck
{
private readonly IMongoDatabase _database;
private readonly HttpClient _httpClient;
private readonly ILogger<CriticalServicesHealthCheck> _logger;
public CriticalServicesHealthCheck(
IMongoDatabase database,
HttpClient httpClient,
ILogger<CriticalServicesHealthCheck> logger)
{
_database = database;
_httpClient = httpClient;
_logger = logger;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
var results = new Dictionary<string, object>();
var allCritical = true;
var criticalFailures = new List<string>();
// 1. MongoDB Check
try
{
await _database.RunCommandAsync<object>(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<string, object>
{
{ "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);
}
}

View File

@ -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);
}
}
}

View File

@ -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<OAuthProvidersHealthCheck> _logger;
public OAuthProvidersHealthCheck(IOAuthHealthService oauthHealthService, ILogger<OAuthProvidersHealthCheck> logger)
{
_oauthHealthService = oauthHealthService;
_logger = logger;
}
public async Task<HealthCheckResult> CheckHealthAsync(
HealthCheckContext context,
CancellationToken cancellationToken = default)
{
try
{
var status = await _oauthHealthService.CheckOAuthProvidersAsync();
var data = new Dictionary<string, object>
{
{ "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<string>();
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}");
}
}
}

View File

@ -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<ILogger<Program>>();
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<ILogger<Program>>();
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<IOAuthHealthService, OAuthHealthService>();
// HttpClient otimizado para OAuth checks - usar implementação concreta
builder.Services.AddHttpClient<OAuthHealthService>(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<MongoDbHealthCheck>(
name: "mongodb",
failureStatus: HealthStatus.Unhealthy,
// 🔴 CRÍTICO (ALERTA VERMELHO) - Serviços essenciais
.AddCheck<CriticalServicesHealthCheck>(
name: "critical_services",
failureStatus: HealthStatus.Unhealthy, // Falha = site fora do ar
timeout: TimeSpan.FromSeconds(30))
// 🟡 DEGRADADO (ALERTA AMARELO) - OAuth providers
.AddCheck<OAuthProvidersHealthCheck>(
name: "oauth_providers",
failureStatus: HealthStatus.Degraded, // Falha = login offline, site funciona
timeout: TimeSpan.FromSeconds(10))
// Stripe Health Check
.AddCheck<StripeHealthCheck>(
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<SendGridHealthCheck>(
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<ExternalServicesHealthCheck>(
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<SystemResourcesHealthCheck>(
name: "resources",
failureStatus: HealthStatus.Degraded,
failureStatus: HealthStatus.Degraded, // Performance reduzida
timeout: TimeSpan.FromSeconds(5));
// Registrar health checks customizados no DI
builder.Services.AddTransient<MongoDbHealthCheck>();
builder.Services.AddTransient<StripeHealthCheck>();
builder.Services.AddTransient<CriticalServicesHealthCheck>();
builder.Services.AddTransient<OAuthProvidersHealthCheck>();
builder.Services.AddTransient<SendGridHealthCheck>();
builder.Services.AddTransient<ExternalServicesHealthCheck>();
builder.Services.AddTransient<SystemResourcesHealthCheck>();
// HttpClient para External Services Health Check
builder.Services.AddHttpClient<ExternalServicesHealthCheck>(client =>
// HttpClient para Critical Services Health Check
builder.Services.AddHttpClient<CriticalServicesHealthCheck>(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();

View File

@ -14,7 +14,8 @@ public class GridFSImageStorage : IImageStorageService
private readonly ILogger<GridFSImageStorage> _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<GridFSImageStorage> 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");
}
});
}
}

View File

@ -0,0 +1,20 @@
namespace BCards.Web.Services;
public interface IOAuthHealthService
{
Task<OAuthHealthStatus> CheckOAuthProvidersAsync();
Task<bool> IsGoogleAvailableAsync();
Task<bool> 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;
}

View File

@ -0,0 +1,127 @@
using System.Net;
namespace BCards.Web.Services;
public class OAuthHealthService : IOAuthHealthService
{
private readonly HttpClient _httpClient;
private readonly ILogger<OAuthHealthService> _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<OAuthHealthService> 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<OAuthHealthStatus> 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<bool> IsGoogleAvailableAsync()
{
var status = await CheckOAuthProvidersAsync();
return status.GoogleAvailable;
}
public async Task<bool> 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<bool> 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;
}
}
}

View File

@ -119,7 +119,7 @@
<input type="file" class="form-control" id="profileImageInput" name="ProfileImageFile" accept="image/*">
<div class="form-text">
<i class="fas fa-info-circle me-1"></i>
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.
</div>
<span class="text-danger" id="imageError"></span>
</div>
@ -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;
}

View File

@ -48,7 +48,7 @@
<div class="alert alert-info mt-5" role="alert">
<h5 class="alert-heading">Como Denunciar</h5>
<p>Se você encontrar conteúdo que viola estas diretrizes, por favor, denuncie através do link "Denunciar" presente no rodapé de todas as páginas de usuário ou envie um e-mail para <a href="mailto:suporte@vcart.me">suporte@vcart.me</a>. Levamos todas as denúncias a sério.</p>
<p>Se você encontrar conteúdo que viola estas diretrizes, por favor, denuncie através do link "Denunciar" presente no rodapé de todas as páginas de usuário ou envie um e-mail para <a href="mailto:suporte@bcards.site">suporte@bcards.site</a>. Levamos todas as denúncias a sério.</p>
</div>
</div>
</div>

View File

@ -73,7 +73,7 @@
<h4 class="mt-5 fw-bold">8. Contato do Encarregado de Proteção de Dados (DPO)</h4>
<p>Para qualquer dúvida sobre esta Política de Privacidade ou para exercer seus direitos, entre em contato com nosso DPO:</p>
<p><strong>E-mail:</strong> <a href="mailto:dpo@vcart.me" class="fw-bold">dpo@vcart.me</a></p>
<p><strong>E-mail:</strong> <a href="mailto:dpo@bcards.site" class="fw-bold">dpo@bcards.site</a></p>
<h4 class="mt-5 fw-bold">9. Alterações a esta Política</h4>
<p>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.</p>

View File

@ -54,7 +54,7 @@
<h4 class="mt-5 fw-bold">6. Contacto del Oficial de Protección de Datos (DPO)</h4>
<p>Para cualquier pregunta sobre esta Política de Privacidad o para ejercer sus derechos, contacte a nuestro DPO:</p>
<p><strong>Correo Electrónico:</strong> <a href="mailto:dpo@vcart.me" class="fw-bold">dpo@vcart.me</a></p>
<p><strong>Correo Electrónico:</strong> <a href="mailto:dpo@bcards.site" class="fw-bold">dpo@bcards.site</a></p>
<h4 class="mt-5 fw-bold">7. Cambios a esta Política</h4>
<p>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.</p>

View File

@ -229,7 +229,7 @@
<h5 class="fw-bold">Empresa</h5>
<ul class="list-unstyled">
<li><a href="#" class="text-muted text-decoration-none">Sobre nós</a></li>
<li><a href="mailto:suporte@vcart.me" class="text-muted text-decoration-none">Contato</a></li>
<li><a href="mailto:suporte@bcards.site" class="text-muted text-decoration-none">Contato</a></li>
</ul>
</div>
<div class="col-lg-3">