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(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
|
||||
|
||||
@ -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
|
||||
|
||||
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
|
||||
```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
|
||||
|
||||
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
|
||||
|
||||
### ✨ 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
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
|
||||
{
|
||||
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);
|
||||
|
||||
@ -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"
|
||||
});
|
||||
}
|
||||
|
||||
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 }
|
||||
};
|
||||
|
||||
_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("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();
|
||||
|
||||
@ -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");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
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/*">
|
||||
<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;
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user