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
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:
parent
3becbe67c3
commit
d700bd35a9
@ -20,7 +20,10 @@
|
|||||||
"Bash(docker-compose up:*)",
|
"Bash(docker-compose up:*)",
|
||||||
"Bash(dotnet build:*)",
|
"Bash(dotnet build:*)",
|
||||||
"Bash(chmod:*)",
|
"Bash(chmod:*)",
|
||||||
"Bash(mv:*)"
|
"Bash(mv:*)",
|
||||||
|
"Bash(dotnet nuget locals:*)",
|
||||||
|
"Bash(/mnt/c/vscode/vcart.me.novo/clean-build.sh:*)",
|
||||||
|
"Bash(sed:*)"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enableAllProjectMcpServers": false
|
"enableAllProjectMcpServers": false
|
||||||
|
|||||||
@ -21,6 +21,7 @@ EndProject
|
|||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8EC462FD-D22E-90A8-E5CE-7E832BA40C5D}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
Conexoes.txt = Conexoes.txt
|
Conexoes.txt = Conexoes.txt
|
||||||
|
README.md = README.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
|
|||||||
24
CLAUDE.md
24
CLAUDE.md
@ -10,10 +10,11 @@ BCards is a professional LinkTree clone built in ASP.NET Core MVC, targeting Bra
|
|||||||
|
|
||||||
### Build & Run
|
### Build & Run
|
||||||
```bash
|
```bash
|
||||||
# Restore dependencies
|
# Quick clean build (RECOMMENDED after VS 2022 updates)
|
||||||
dotnet restore
|
./clean-build.sh
|
||||||
|
|
||||||
# Build solution
|
# Manual process:
|
||||||
|
dotnet restore
|
||||||
dotnet build
|
dotnet build
|
||||||
|
|
||||||
# Run development server
|
# Run development server
|
||||||
@ -26,6 +27,23 @@ docker-compose up -d
|
|||||||
# Access: http://localhost:8080
|
# 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
|
### Testing
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Run all tests
|
||||||
|
|||||||
12
README.md
12
README.md
@ -5,7 +5,7 @@ Um clone profissional do LinkTree desenvolvido em ASP.NET Core MVC, focado no me
|
|||||||
## 🚀 Características Principais
|
## 🚀 Características Principais
|
||||||
|
|
||||||
### ✨ Funcionalidades
|
### ✨ 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
|
- **Sistema de Pagamentos**: Integração completa com Stripe
|
||||||
- **3 Planos com Efeito Decoy**: Estratégia psicológica de pricing
|
- **3 Planos com Efeito Decoy**: Estratégia psicológica de pricing
|
||||||
- **Autenticação OAuth**: Google e Microsoft
|
- **Autenticação OAuth**: Google e Microsoft
|
||||||
@ -86,7 +86,7 @@ Edite `appsettings.json` ou `appsettings.Development.json`:
|
|||||||
"ClientSecret": "seu_microsoft_client_secret"
|
"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",
|
"SecretKey": "sk_live_seu_secret_key",
|
||||||
"WebhookSecret": "whsec_seu_webhook_secret_producao"
|
"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
|
## 🆘 Suporte
|
||||||
|
|
||||||
Para suporte técnico, entre em contato:
|
Para suporte técnico, entre em contato:
|
||||||
- Email: suporte@vcart.me
|
- Email: suporte@bcards.site
|
||||||
- Discord: [Servidor da Comunidade]
|
- Discord: [Servidor da Comunidade]
|
||||||
- Issues: [GitHub Issues](https://github.com/seuusuario/bcards/issues)
|
- Issues: [GitHub Issues](https://github.com/seuusuario/bcards/issues)
|
||||||
|
|
||||||
## 📞 Contato
|
## 📞 Contato
|
||||||
|
|
||||||
- **Website**: https://vcart.me
|
- **Website**: https://bcards.site
|
||||||
- **Email**: contato@vcart.me
|
- **Email**: contato@bcards.site
|
||||||
- **LinkedIn**: [Seu LinkedIn]
|
- **LinkedIn**: [Seu LinkedIn]
|
||||||
- **Twitter**: [@vcartme]
|
- **Twitter**: [@vcartme]
|
||||||
|
|
||||||
|
|||||||
34
clean-build.sh
Normal file
34
clean-build.sh
Normal 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!"
|
||||||
@ -13,24 +13,102 @@ namespace BCards.Web.Controllers;
|
|||||||
public class AuthController : Controller
|
public class AuthController : Controller
|
||||||
{
|
{
|
||||||
private readonly IAuthService _authService;
|
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;
|
_authService = authService;
|
||||||
|
_oauthHealthService = oauthHealthService;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("Login")]
|
[Route("Login")]
|
||||||
public IActionResult Login(string? returnUrl = null)
|
public async Task<IActionResult> Login(string? returnUrl = null)
|
||||||
{
|
{
|
||||||
ViewBag.ReturnUrl = returnUrl;
|
ViewBag.ReturnUrl = returnUrl;
|
||||||
|
|
||||||
|
// Verificar status dos OAuth providers e passar para a view
|
||||||
|
var oauthStatus = await _oauthHealthService.CheckOAuthProvidersAsync();
|
||||||
|
ViewBag.OAuthStatus = oauthStatus;
|
||||||
|
|
||||||
return View();
|
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]
|
[HttpPost]
|
||||||
[Route("LoginWithGoogle")]
|
[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 redirectUrl = Url.Action("GoogleCallback", "Auth", new { returnUrl });
|
||||||
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
|
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
|
||||||
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
|
return Challenge(properties, GoogleDefaults.AuthenticationScheme);
|
||||||
@ -38,8 +116,28 @@ public class AuthController : Controller
|
|||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
[Route("LoginWithMicrosoft")]
|
[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 redirectUrl = Url.Action("MicrosoftCallback", "Auth", new { returnUrl });
|
||||||
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
|
var properties = new AuthenticationProperties { RedirectUri = redirectUrl };
|
||||||
return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme);
|
return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme);
|
||||||
|
|||||||
@ -51,7 +51,7 @@ public class ImageController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("upload")]
|
[HttpPost("upload")]
|
||||||
[RequestSizeLimit(5 * 1024 * 1024)] // 5MB máximo
|
[RequestSizeLimit(2 * 1024 * 1024)] // 2MB máximo - otimizado para celulares
|
||||||
[DisableRequestSizeLimit] // Para formulários grandes
|
[DisableRequestSizeLimit] // Para formulários grandes
|
||||||
public async Task<IActionResult> UploadImage(IFormFile file)
|
public async Task<IActionResult> UploadImage(IFormFile file)
|
||||||
{
|
{
|
||||||
@ -75,11 +75,11 @@ public class ImageController : ControllerBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validação de tamanho
|
// 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));
|
_logger.LogWarning("File too large: {Size}MB", file.Length / (1024 * 1024));
|
||||||
return BadRequest(new {
|
return BadRequest(new {
|
||||||
error = "File too large. Maximum size is 5MB.",
|
error = "Arquivo muito grande. Tamanho máximo: 2MB.",
|
||||||
code = "FILE_TOO_LARGE"
|
code = "FILE_TOO_LARGE"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
139
src/BCards.Web/HealthChecks/CriticalServicesHealthCheck.cs
Normal file
139
src/BCards.Web/HealthChecks/CriticalServicesHealthCheck.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -77,7 +77,16 @@ public class ExternalServicesHealthCheck : IHealthCheck
|
|||||||
{ "url", service.Value }
|
{ "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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
61
src/BCards.Web/HealthChecks/OAuthProvidersHealthCheck.cs
Normal file
61
src/BCards.Web/HealthChecks/OAuthProvidersHealthCheck.cs
Normal 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -35,38 +35,58 @@ var loggerConfig = new LoggerConfiguration()
|
|||||||
.Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
|
.Enrich.WithProperty("Environment", builder.Environment.EnvironmentName)
|
||||||
.Enrich.WithProperty("Hostname", hostname);
|
.Enrich.WithProperty("Hostname", hostname);
|
||||||
|
|
||||||
|
// Nova abordagem de logging: Tudo em ambos, mas com controle de volume
|
||||||
if (isDevelopment)
|
if (isDevelopment)
|
||||||
{
|
{
|
||||||
// Development: Log EVERYTHING to console with detailed formatting
|
// Development: Debug level + detalhes completos
|
||||||
loggerConfig
|
loggerConfig
|
||||||
.MinimumLevel.Debug()
|
.MinimumLevel.Debug()
|
||||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
|
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Information)
|
||||||
.MinimumLevel.Override("System", 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(
|
.WriteTo.Async(a => a.Console(
|
||||||
|
restrictedToMinimumLevel: LogEventLevel.Information,
|
||||||
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{Hostname}] {Message:lj} {Properties:j}{NewLine}{Exception}"));
|
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{Hostname}] {Message:lj} {Properties:j}{NewLine}{Exception}"));
|
||||||
|
|
||||||
var seqUrl = builder.Configuration["Serilog:SeqUrl"];
|
var seqUrl = builder.Configuration["Serilog:SeqUrl"];
|
||||||
if (!string.IsNullOrEmpty(seqUrl))
|
if (!string.IsNullOrEmpty(seqUrl))
|
||||||
{
|
{
|
||||||
var apiKey = builder.Configuration["Serilog:ApiKey"];
|
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
|
else
|
||||||
{
|
{
|
||||||
// Production: Only errors to console, everything to Seq
|
// Production: Log balanceado - reduzir spam de requisições
|
||||||
loggerConfig
|
loggerConfig
|
||||||
.MinimumLevel.Information()
|
.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("Microsoft", LogEventLevel.Warning)
|
||||||
.MinimumLevel.Override("System", LogEventLevel.Warning)
|
.MinimumLevel.Override("System", LogEventLevel.Warning)
|
||||||
|
// Docker/Console: Information e acima (reduzindo volume do log)
|
||||||
.WriteTo.Async(a => a.Console(
|
.WriteTo.Async(a => a.Console(
|
||||||
restrictedToMinimumLevel: LogEventLevel.Error,
|
restrictedToMinimumLevel: LogEventLevel.Information, // Mudança: não só errors
|
||||||
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] [{Hostname}] {Message:lj}{NewLine}{Exception}"));
|
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"];
|
var seqUrl = builder.Configuration["Serilog:SeqUrl"];
|
||||||
if (!string.IsNullOrEmpty(seqUrl))
|
if (!string.IsNullOrEmpty(seqUrl))
|
||||||
{
|
{
|
||||||
var apiKey = builder.Configuration["Serilog:ApiKey"];
|
var apiKey = builder.Configuration["Serilog:ApiKey"];
|
||||||
|
// Seq: Tudo (Information e acima) - Seq pode estar offline
|
||||||
loggerConfig.WriteTo.Async(a => a.Seq(seqUrl,
|
loggerConfig.WriteTo.Async(a => a.Seq(seqUrl,
|
||||||
apiKey: string.IsNullOrEmpty(apiKey) ? null : apiKey,
|
apiKey: string.IsNullOrEmpty(apiKey) ? null : apiKey,
|
||||||
restrictedToMinimumLevel: LogEventLevel.Information));
|
restrictedToMinimumLevel: LogEventLevel.Information));
|
||||||
@ -185,20 +205,31 @@ builder.Services.AddAuthentication(options =>
|
|||||||
var googleAuth = builder.Configuration.GetSection("Authentication:Google");
|
var googleAuth = builder.Configuration.GetSection("Authentication:Google");
|
||||||
options.ClientId = googleAuth["ClientId"] ?? "";
|
options.ClientId = googleAuth["ClientId"] ?? "";
|
||||||
options.ClientSecret = googleAuth["ClientSecret"] ?? "";
|
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);
|
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
|
options.Events = new OAuthEvents
|
||||||
{
|
{
|
||||||
OnRemoteFailure = context =>
|
OnRemoteFailure = context =>
|
||||||
{
|
{
|
||||||
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
|
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();
|
context.HandleResponse();
|
||||||
return Task.CompletedTask;
|
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();
|
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()
|
builder.Services.AddHealthChecks()
|
||||||
// MongoDB Health Check usando configuração existente
|
// 🔴 CRÍTICO (ALERTA VERMELHO) - Serviços essenciais
|
||||||
.AddCheck<MongoDbHealthCheck>(
|
.AddCheck<CriticalServicesHealthCheck>(
|
||||||
name: "mongodb",
|
name: "critical_services",
|
||||||
failureStatus: HealthStatus.Unhealthy,
|
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))
|
timeout: TimeSpan.FromSeconds(10))
|
||||||
|
|
||||||
// Stripe Health Check
|
// 🟡 DEGRADADO (ALERTA AMARELO) - Serviços auxiliares
|
||||||
.AddCheck<StripeHealthCheck>(
|
|
||||||
name: "stripe",
|
|
||||||
failureStatus: HealthStatus.Degraded, // Stripe não é crítico para funcionalidade básica
|
|
||||||
timeout: TimeSpan.FromSeconds(15))
|
|
||||||
|
|
||||||
// SendGrid Health Check
|
|
||||||
.AddCheck<SendGridHealthCheck>(
|
.AddCheck<SendGridHealthCheck>(
|
||||||
name: "sendgrid",
|
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))
|
timeout: TimeSpan.FromSeconds(10))
|
||||||
|
|
||||||
// External Services (OAuth providers)
|
// 🟡 DEGRADADO (ALERTA AMARELO) - Recursos do sistema
|
||||||
.AddCheck<ExternalServicesHealthCheck>(
|
|
||||||
name: "external_services",
|
|
||||||
failureStatus: HealthStatus.Degraded, // OAuth pode estar indisponível temporariamente
|
|
||||||
timeout: TimeSpan.FromSeconds(20))
|
|
||||||
|
|
||||||
// System Resources
|
|
||||||
.AddCheck<SystemResourcesHealthCheck>(
|
.AddCheck<SystemResourcesHealthCheck>(
|
||||||
name: "resources",
|
name: "resources",
|
||||||
failureStatus: HealthStatus.Degraded,
|
failureStatus: HealthStatus.Degraded, // Performance reduzida
|
||||||
timeout: TimeSpan.FromSeconds(5));
|
timeout: TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
// Registrar health checks customizados no DI
|
// Registrar health checks customizados no DI
|
||||||
builder.Services.AddTransient<MongoDbHealthCheck>();
|
builder.Services.AddTransient<CriticalServicesHealthCheck>();
|
||||||
builder.Services.AddTransient<StripeHealthCheck>();
|
builder.Services.AddTransient<OAuthProvidersHealthCheck>();
|
||||||
builder.Services.AddTransient<SendGridHealthCheck>();
|
builder.Services.AddTransient<SendGridHealthCheck>();
|
||||||
builder.Services.AddTransient<ExternalServicesHealthCheck>();
|
|
||||||
builder.Services.AddTransient<SystemResourcesHealthCheck>();
|
builder.Services.AddTransient<SystemResourcesHealthCheck>();
|
||||||
|
|
||||||
// HttpClient para External Services Health Check
|
// HttpClient para Critical Services Health Check
|
||||||
builder.Services.AddHttpClient<ExternalServicesHealthCheck>(client =>
|
builder.Services.AddHttpClient<CriticalServicesHealthCheck>(client =>
|
||||||
{
|
{
|
||||||
client.Timeout = TimeSpan.FromSeconds(10);
|
client.Timeout = TimeSpan.FromSeconds(15);
|
||||||
client.DefaultRequestHeaders.Add("User-Agent", "BCards-HealthCheck/1.0");
|
client.DefaultRequestHeaders.Add("User-Agent", "BCards-CriticalCheck/1.0");
|
||||||
});
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|||||||
@ -14,7 +14,8 @@ public class GridFSImageStorage : IImageStorageService
|
|||||||
private readonly ILogger<GridFSImageStorage> _logger;
|
private readonly ILogger<GridFSImageStorage> _logger;
|
||||||
|
|
||||||
private const int TARGET_SIZE = 400;
|
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" };
|
private static readonly string[] ALLOWED_TYPES = { "image/jpeg", "image/jpg", "image/png", "image/gif" };
|
||||||
|
|
||||||
public GridFSImageStorage(IMongoDatabase database, ILogger<GridFSImageStorage> logger)
|
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");
|
throw new ArgumentException("Image bytes cannot be null or empty");
|
||||||
|
|
||||||
if (imageBytes.Length > MAX_FILE_SIZE)
|
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()))
|
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
|
// Processar e redimensionar imagem
|
||||||
var processedImage = await ProcessImageAsync(imageBytes);
|
var processedImage = await ProcessImageAsync(imageBytes);
|
||||||
@ -196,4 +200,26 @@ public class GridFSImageStorage : IImageStorageService
|
|||||||
var ratio = Math.Min((double)targetSize / originalWidth, (double)targetSize / originalHeight);
|
var ratio = Math.Min((double)targetSize / originalWidth, (double)targetSize / originalHeight);
|
||||||
return ((int)(originalWidth * ratio), (int)(originalHeight * ratio));
|
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");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
20
src/BCards.Web/Services/IOAuthHealthService.cs
Normal file
20
src/BCards.Web/Services/IOAuthHealthService.cs
Normal 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;
|
||||||
|
}
|
||||||
127
src/BCards.Web/Services/OAuthHealthService.cs
Normal file
127
src/BCards.Web/Services/OAuthHealthService.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -119,7 +119,7 @@
|
|||||||
<input type="file" class="form-control" id="profileImageInput" name="ProfileImageFile" accept="image/*">
|
<input type="file" class="form-control" id="profileImageInput" name="ProfileImageFile" accept="image/*">
|
||||||
<div class="form-text">
|
<div class="form-text">
|
||||||
<i class="fas fa-info-circle me-1"></i>
|
<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>
|
</div>
|
||||||
<span class="text-danger" id="imageError"></span>
|
<span class="text-danger" id="imageError"></span>
|
||||||
</div>
|
</div>
|
||||||
@ -1599,8 +1599,8 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.size > 5 * 1024 * 1024) { // 5MB
|
if (file.size > 2 * 1024 * 1024) { // 2MB
|
||||||
errorSpan.text('Arquivo muito grande. Máximo 5MB.');
|
errorSpan.text('Arquivo muito grande. Máximo 2MB.');
|
||||||
fileInput.val('');
|
fileInput.val('');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -48,7 +48,7 @@
|
|||||||
|
|
||||||
<div class="alert alert-info mt-5" role="alert">
|
<div class="alert alert-info mt-5" role="alert">
|
||||||
<h5 class="alert-heading">Como Denunciar</h5>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -73,7 +73,7 @@
|
|||||||
|
|
||||||
<h4 class="mt-5 fw-bold">8. Contato do Encarregado de Proteção de Dados (DPO)</h4>
|
<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>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>
|
<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>
|
<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>
|
||||||
|
|||||||
@ -54,7 +54,7 @@
|
|||||||
|
|
||||||
<h4 class="mt-5 fw-bold">6. Contacto del Oficial de Protección de Datos (DPO)</h4>
|
<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>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>
|
<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>
|
<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>
|
||||||
|
|||||||
@ -229,7 +229,7 @@
|
|||||||
<h5 class="fw-bold">Empresa</h5>
|
<h5 class="fw-bold">Empresa</h5>
|
||||||
<ul class="list-unstyled">
|
<ul class="list-unstyled">
|
||||||
<li><a href="#" class="text-muted text-decoration-none">Sobre nós</a></li>
|
<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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-lg-3">
|
<div class="col-lg-3">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user