fix: logs para validação de imagens
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 5s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 8m9s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m58s
BCards Deployment Pipeline / Deploy to Test (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s

This commit is contained in:
Ricardo Carneiro 2025-09-17 23:48:59 -03:00
parent 7b0bc89f06
commit e043c853b1
9 changed files with 393 additions and 80 deletions

View File

@ -123,9 +123,9 @@ jobs:
echo "tag=$VERSION" >> $GITHUB_OUTPUT echo "tag=$VERSION" >> $GITHUB_OUTPUT
echo "platform=linux/amd64" >> $GITHUB_OUTPUT echo "platform=linux/amd64" >> $GITHUB_OUTPUT
echo "environment=Staging" >> $GITHUB_OUTPUT echo "environment=Testing" >> $GITHUB_OUTPUT
echo "dockerfile=Dockerfile.release" >> $GITHUB_OUTPUT echo "dockerfile=Dockerfile.release" >> $GITHUB_OUTPUT
echo "deploy_target=staging" >> $GITHUB_OUTPUT echo "deploy_target=testing" >> $GITHUB_OUTPUT
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
fi fi
@ -197,6 +197,28 @@ jobs:
# Cria o arquivo de configuração para produção # Cria o arquivo de configuração para produção
cat > appsettings.Production.json << 'CONFIG_EOF' cat > appsettings.Production.json << 'CONFIG_EOF'
{ {
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning",
"Microsoft.EntityFrameworkCore": "Warning",
"BCards": "Information",
"BCards.Web.Services.GridFSImageStorage": "Debug"
},
"Console": {
"IncludeScopes": false,
"LogLevel": {
"Default": "Information"
}
},
"File": {
"Path": "/app/logs/bcards-{Date}.log",
"LogLevel": {
"Default": "Information"
}
}
},
"AllowedHosts": "*",
"Stripe": { "Stripe": {
"PublishableKey": "${{ vars.STRIPE_PUBLISHABLE_KEY }}", "PublishableKey": "${{ vars.STRIPE_PUBLISHABLE_KEY }}",
"SecretKey": "${{ secrets.STRIPE_SECRET_KEY }}", "SecretKey": "${{ secrets.STRIPE_SECRET_KEY }}",
@ -334,6 +356,51 @@ jobs:
"${{ vars.MODERATOR_EMAIL_2 || 'rirocarneiro@gmail.com' }}" "${{ vars.MODERATOR_EMAIL_2 || 'rirocarneiro@gmail.com' }}"
] ]
}, },
"MongoDb": {
"ConnectionString": "mongodb://admin:c4rn31r0@129.146.116.218:27017,141.148.162.114:27017/BCardsDB?replicaSet=rs0&authSource=admin",
"DatabaseName": "BCardsDB",
"MaxConnectionPoolSize": 100,
"ConnectTimeout": "30s",
"ServerSelectionTimeout": "30s",
"SocketTimeout": "30s"
},
"BaseUrl": "https://bcards.site",
"Environment": {
"Name": "Production",
"IsStagingEnvironment": false,
"AllowTestData": false,
"EnableDetailedErrors": false
},
"Performance": {
"EnableCaching": true,
"CacheExpirationMinutes": 30,
"EnableCompression": true,
"EnableResponseCaching": true
},
"Security": {
"EnableHttpsRedirection": true,
"EnableHsts": true,
"RequireHttpsMetadata": true
},
"HealthChecks": {
"Enabled": true,
"Endpoints": {
"Health": "/health",
"Ready": "/ready",
"Live": "/live"
},
"MongoDb": {
"Enabled": true,
"Timeout": "10s"
}
},
"Features": {
"EnablePreviewMode": true,
"EnableModerationWorkflow": true,
"EnableAnalytics": true,
"EnableFileUploads": true,
"MaxFileUploadSize": "5MB"
},
"Serilog": { "Serilog": {
"OpenSearchUrl": "${{ vars.OPENSEARCH_URL || 'http://localhost:9201' }}" "OpenSearchUrl": "${{ vars.OPENSEARCH_URL || 'http://localhost:9201' }}"
} }
@ -470,8 +537,8 @@ jobs:
echo "Verificando Servidor 2 (ARM)..." echo "Verificando Servidor 2 (ARM)..."
ssh -o StrictHostKeyChecking=no ubuntu@129.146.116.218 'curl -f http://localhost:8080/health || echo "⚠️ Servidor 2 pode não estar respondendo"' ssh -o StrictHostKeyChecking=no ubuntu@129.146.116.218 'curl -f http://localhost:8080/health || echo "⚠️ Servidor 2 pode não estar respondendo"'
deploy-staging: deploy-test:
name: Deploy to Staging (x86 - Local) name: Deploy to Test (x86 - Local)
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [build-and-push] needs: [build-and-push]
if: startsWith(github.ref_name, 'Release/') if: startsWith(github.ref_name, 'Release/')
@ -487,9 +554,9 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "📦 Deploying version: $VERSION" echo "📦 Deploying version: $VERSION"
- name: Deploy to Staging Server - name: Deploy to Test Server
run: | run: |
echo "🚀 Deploying to staging server (x86)..." echo "🚀 Deploying to test server (x86)..."
# Configura SSH (igual ao QRRapido) # Configura SSH (igual ao QRRapido)
mkdir -p ~/.ssh mkdir -p ~/.ssh
@ -505,15 +572,15 @@ jobs:
# Deploy no Servidor Local x86 # Deploy no Servidor Local x86
ssh -o StrictHostKeyChecking=no ubuntu@192.168.0.100 << EOF ssh -o StrictHostKeyChecking=no ubuntu@192.168.0.100 << EOF
echo "🔄 Atualizando Servidor Staging..." echo "🔄 Atualizando Servidor Teste..."
# Remove containers bcards-infrastructure se existirem # Remove containers bcards-infrastructure se existirem
docker stop bcards-infrastructure bcards-test-app || true docker stop bcards-infrastructure bcards-test-app || true
docker rm bcards-infrastructure bcards-test-app || true docker rm bcards-infrastructure bcards-test-app || true
# Para o container BCards atual se existir # Para o container BCards atual se existir
docker stop bcards-staging || true docker stop bcards-test || true
docker rm bcards-staging || true docker rm bcards-test || true
# Remove imagem antiga # Remove imagem antiga
docker rmi ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} || true docker rmi ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} || true
@ -523,30 +590,30 @@ jobs:
# Executa novo container BCards # Executa novo container BCards
docker run -d \ docker run -d \
--name bcards-staging \ --name bcards-test \
--restart unless-stopped \ --restart unless-stopped \
--network host \ --network host \
-e ASPNETCORE_ENVIRONMENT=Staging \ -e ASPNETCORE_ENVIRONMENT=Testing \
-e ASPNETCORE_URLS=http://+:8080 \ -e ASPNETCORE_URLS=http://+:8080 \
-e Serilog__OpenSearchUrl="http://192.168.0.100:9200" \ -e Serilog__OpenSearchUrl="http://192.168.0.100:9200" \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
echo "✅ Servidor Staging atualizado" echo "✅ Servidor Teste atualizado"
EOF EOF
- name: Health Check Staging - name: Health Check Test
run: | run: |
echo "🏥 Verificando saúde do servidor de staging..." echo "🏥 Verificando saúde do servidor de teste..."
sleep 30 sleep 30
echo "Verificando Servidor Staging (x86)..." echo "Verificando Servidor Teste (x86)..."
ssh -o StrictHostKeyChecking=no ubuntu@192.168.0.100 'curl -f http://localhost:8080/health || echo "⚠️ Servidor staging pode não estar respondendo"' ssh -o StrictHostKeyChecking=no ubuntu@192.168.0.100 'curl -f http://localhost:8080/health || echo "⚠️ Servidor teste pode não estar respondendo"'
cleanup: cleanup:
name: Cleanup Old Resources name: Cleanup Old Resources
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [deploy-production, deploy-staging] needs: [deploy-production, deploy-test]
if: always() && (needs.deploy-production.result == 'success' || needs.deploy-staging.result == 'success') if: always() && (needs.deploy-production.result == 'success' || needs.deploy-test.result == 'success')
steps: steps:
- name: Cleanup containers and images - name: Cleanup containers and images
@ -592,7 +659,7 @@ jobs:
deployment-summary: deployment-summary:
name: Deployment Summary name: Deployment Summary
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [deploy-production, deploy-staging] needs: [deploy-production, deploy-test]
if: always() if: always()
steps: steps:
@ -610,10 +677,10 @@ jobs:
echo "📦 Tag: latest" echo "📦 Tag: latest"
echo "🔗 Status: ${{ needs.deploy-production.result }}" echo "🔗 Status: ${{ needs.deploy-production.result }}"
else else
echo "🌍 Environment: Staging (x86)" echo "🌍 Environment: Testing (x86)"
echo "🖥️ Server: 192.168.0.100" echo "🖥️ Server: 192.168.0.100"
echo "📦 Tag: ${{ github.ref_name }}" echo "📦 Tag: ${{ github.ref_name }}"
echo "🔗 Status: ${{ needs.deploy-staging.result }}" echo "🔗 Status: ${{ needs.deploy-test.result }}"
fi fi
echo "====================" echo "===================="

View File

@ -241,9 +241,15 @@ public class AdminController : Controller
} }
catch (Exception ex) catch (Exception ex)
{ {
_logger.LogError(ex, $"Userid: {userId} - Error uploading profile image"); _logger.LogError(ex, "Userid: {UserId} - Error uploading profile image. FileName: {FileName}, ContentType: {ContentType}, Size: {Size}KB, ExceptionType: {ExceptionType}",
ModelState.AddModelError("ProfileImageFile", "Erro ao fazer upload da imagem. Tente novamente."); userId, model.ProfileImageFile?.FileName ?? "Unknown", model.ProfileImageFile?.ContentType ?? "Unknown",
TempData["ImageError"] = "Erro ao processar a imagem. Verifique o formato e tamanho."; model.ProfileImageFile?.Length / 1024 ?? 0, ex.GetType().Name);
// Mensagem específica baseada no tipo de erro
var errorMessage = ex is ArgumentException argEx ? argEx.Message : "Erro ao processar a imagem. Verifique o formato e tamanho.";
ModelState.AddModelError("ProfileImageFile", errorMessage);
TempData["ImageError"] = errorMessage;
// Preservar dados do form e repopular dropdowns // Preservar dados do form e repopular dropdowns
var userPlanType = Enum.TryParse<PlanType>(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial; var userPlanType = Enum.TryParse<PlanType>(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial;

View File

@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using System.Security.Claims; using System.Security.Claims;
using BCards.Web.TestSupport;
namespace BCards.Web.Controllers; namespace BCards.Web.Controllers;
@ -13,17 +14,20 @@ 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 IOAuthHealthService _oauthHealthService;
private readonly ILogger<AuthController> _logger; private readonly ILogger<AuthController> _logger;
private readonly IWebHostEnvironment _env;
public AuthController( public AuthController(
IAuthService authService, IAuthService authService,
IOAuthHealthService oauthHealthService, IOAuthHealthService oauthHealthService,
ILogger<AuthController> logger) ILogger<AuthController> logger,
IWebHostEnvironment env)
{ {
_authService = authService; _authService = authService;
_oauthHealthService = oauthHealthService; _oauthHealthService = oauthHealthService;
_logger = logger; _logger = logger;
_env = env;
} }
[HttpGet] [HttpGet]
@ -31,6 +35,7 @@ public class AuthController : Controller
public async Task<IActionResult> Login(string? returnUrl = null) public async Task<IActionResult> Login(string? returnUrl = null)
{ {
ViewBag.ReturnUrl = returnUrl; ViewBag.ReturnUrl = returnUrl;
ViewBag.IsTestingEnvironment = _env.IsEnvironment("Testing");
// Verificar status dos OAuth providers e passar para a view // Verificar status dos OAuth providers e passar para a view
var oauthStatus = await _oauthHealthService.CheckOAuthProvidersAsync(); var oauthStatus = await _oauthHealthService.CheckOAuthProvidersAsync();
@ -143,6 +148,29 @@ public class AuthController : Controller
return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme); return Challenge(properties, MicrosoftAccountDefaults.AuthenticationScheme);
} }
[HttpPost]
[Route("LoginWithTest")]
public IActionResult LoginWithTest(string? returnUrl = null)
{
if (!_env.IsEnvironment("Testing"))
{
return NotFound(); // Endpoint de teste só funciona no ambiente de Testing
}
string redirectUrlString;
if (Url.IsLocalUrl(returnUrl))
{
redirectUrlString = returnUrl;
}
else
{
redirectUrlString = Url.Action("Dashboard", "Admin");
}
var properties = new AuthenticationProperties { RedirectUri = redirectUrlString };
return Challenge(properties, TestAuthConstants.AuthenticationScheme);
}
[HttpGet] [HttpGet]
[Route("GoogleCallback")] [Route("GoogleCallback")]
public async Task<IActionResult> GoogleCallback(string? returnUrl = null) public async Task<IActionResult> GoogleCallback(string? returnUrl = null)

View File

@ -17,6 +17,8 @@ using Serilog;
using Serilog.Events; using Serilog.Events;
using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks;
using Serilog.Sinks.OpenSearch; using Serilog.Sinks.OpenSearch;
using BCards.Web.TestSupport;
using Microsoft.AspNetCore.Authentication;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
@ -265,12 +267,13 @@ builder.Services.Configure<ModerationSettings>(
builder.Configuration.GetSection("Moderation")); builder.Configuration.GetSection("Moderation"));
// Authentication // Authentication
builder.Services.AddAuthentication(options => var authBuilder = builder.Services.AddAuthentication(options =>
{ {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme; // DefaultChallengeScheme will be set conditionally below
}) });
.AddCookie(options =>
authBuilder.AddCookie(options =>
{ {
options.LoginPath = "/Auth/Login"; options.LoginPath = "/Auth/Login";
options.LogoutPath = "/Auth/Logout"; options.LogoutPath = "/Auth/Logout";
@ -280,8 +283,10 @@ builder.Services.AddAuthentication(options =>
options.Cookie.IsEssential = true; options.Cookie.IsEssential = true;
options.Cookie.SameSite = SameSiteMode.Lax; options.Cookie.SameSite = SameSiteMode.Lax;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
}) });
.AddGoogle(options =>
// Always register Google and Microsoft authentication schemes
authBuilder.AddGoogle(options =>
{ {
var googleAuth = builder.Configuration.GetSection("Authentication:Google"); var googleAuth = builder.Configuration.GetSection("Authentication:Google");
options.ClientId = googleAuth["ClientId"] ?? ""; options.ClientId = googleAuth["ClientId"] ?? "";
@ -367,6 +372,23 @@ builder.Services.AddAuthentication(options =>
}; };
}); });
// Conditionally set the DefaultChallengeScheme and register the Test scheme
if (builder.Environment.IsEnvironment("Testing"))
{
authBuilder.Services.Configure<AuthenticationOptions>(options =>
{
options.DefaultChallengeScheme = TestAuthConstants.AuthenticationScheme;
});
authBuilder.AddScheme<TestAuthSchemeOptions, TestAuthHandler>(TestAuthConstants.AuthenticationScheme, _ => { });
}
else
{
authBuilder.Services.Configure<AuthenticationOptions>(options =>
{
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
});
}
// Localization // Localization
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");

View File

@ -7,6 +7,14 @@
"ASPNETCORE_ENVIRONMENT": "Development" "ASPNETCORE_ENVIRONMENT": "Development"
}, },
"applicationUrl": "https://localhost:49178;http://localhost:49179" "applicationUrl": "https://localhost:49178;http://localhost:49179"
},
"BCards.Web.Testing": {
"commandName": "Project",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Testing"
},
"applicationUrl": "https://localhost:49178;http://localhost:49179"
} }
} }
} }

View File

@ -32,21 +32,39 @@ public class GridFSImageStorage : IImageStorageService
{ {
try try
{ {
_logger.LogInformation("Starting image upload: FileName={FileName}, ContentType={ContentType}, Size={Size}KB",
fileName, contentType, imageBytes?.Length / 1024 ?? 0);
// Validações // Validações
if (imageBytes == null || imageBytes.Length == 0) if (imageBytes == null || imageBytes.Length == 0)
{
_logger.LogWarning("Image upload failed: null or empty image bytes");
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)
{
_logger.LogWarning("Image upload failed: file too large {Size}KB (max: {MaxSize}KB)",
imageBytes.Length / 1024, MAX_FILE_SIZE / 1024);
throw new ArgumentException($"Arquivo muito grande. Tamanho máximo permitido: {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()))
{
_logger.LogWarning("Image upload failed: invalid content type {ContentType}", contentType);
throw new ArgumentException($"Tipo de arquivo {contentType} não permitido"); throw new ArgumentException($"Tipo de arquivo {contentType} não permitido");
}
// Validar resolução da imagem // Validar resolução da imagem
_logger.LogDebug("Starting image resolution validation");
await ValidateImageResolution(imageBytes); await ValidateImageResolution(imageBytes);
_logger.LogDebug("Image resolution validation completed successfully");
// Processar e redimensionar imagem // Processar e redimensionar imagem
_logger.LogDebug("Starting image processing");
var processedImage = await ProcessImageAsync(imageBytes); var processedImage = await ProcessImageAsync(imageBytes);
_logger.LogInformation("Image processed successfully: OriginalSize={OriginalSize}KB, ProcessedSize={ProcessedSize}KB",
imageBytes.Length / 1024, processedImage.Length / 1024);
// Metadata // Metadata
var options = new GridFSUploadOptions var options = new GridFSUploadOptions
@ -161,37 +179,58 @@ public class GridFSImageStorage : IImageStorageService
{ {
return await Task.Run(() => return await Task.Run(() =>
{ {
using var originalImage = Image.Load(originalBytes); try
// Calcular dimensões mantendo aspect ratio
var (newWidth, newHeight) = CalculateResizeDimensions(
originalImage.Width, originalImage.Height, TARGET_SIZE);
// Criar imagem com fundo branco
using var processedImage = new Image<SixLabors.ImageSharp.PixelFormats.Rgb24>(TARGET_SIZE, TARGET_SIZE);
// Preencher com fundo branco
processedImage.Mutate(ctx => ctx.BackgroundColor(SixLabors.ImageSharp.Color.White));
// Redimensionar a imagem original mantendo aspect ratio
originalImage.Mutate(ctx => ctx.Resize(newWidth, newHeight));
// Calcular posição para centralizar a imagem
var x = (TARGET_SIZE - newWidth) / 2;
var y = (TARGET_SIZE - newHeight) / 2;
// Desenhar a imagem centralizada sobre o fundo branco
processedImage.Mutate(ctx => ctx.DrawImage(originalImage, new Point(x, y), 1f));
// Converter para JPEG com compressão otimizada
using var outputStream = new MemoryStream();
var encoder = new JpegEncoder()
{ {
Quality = 85 // 85% qualidade _logger.LogDebug("Loading original image for processing");
}; using var originalImage = Image.Load(originalBytes);
processedImage.SaveAsJpeg(outputStream, encoder); _logger.LogDebug("Original image loaded: {Width}x{Height}px, Format: {Format}",
return outputStream.ToArray(); originalImage.Width, originalImage.Height, originalImage.Metadata.DecodedImageFormat?.Name ?? "Unknown");
// Calcular dimensões mantendo aspect ratio
var (newWidth, newHeight) = CalculateResizeDimensions(
originalImage.Width, originalImage.Height, TARGET_SIZE);
_logger.LogDebug("Calculated resize dimensions: {NewWidth}x{NewHeight}px (target: {TargetSize}x{TargetSize}px)",
newWidth, newHeight, TARGET_SIZE, TARGET_SIZE);
// Criar imagem com fundo branco
using var processedImage = new Image<SixLabors.ImageSharp.PixelFormats.Rgb24>(TARGET_SIZE, TARGET_SIZE);
// Preencher com fundo branco
processedImage.Mutate(ctx => ctx.BackgroundColor(SixLabors.ImageSharp.Color.White));
// Redimensionar a imagem original mantendo aspect ratio
originalImage.Mutate(ctx => ctx.Resize(newWidth, newHeight));
// Calcular posição para centralizar a imagem
var x = (TARGET_SIZE - newWidth) / 2;
var y = (TARGET_SIZE - newHeight) / 2;
_logger.LogDebug("Centering image at position: x={X}, y={Y}", x, y);
// Desenhar a imagem centralizada sobre o fundo branco
processedImage.Mutate(ctx => ctx.DrawImage(originalImage, new Point(x, y), 1f));
// Converter para JPEG com compressão otimizada
using var outputStream = new MemoryStream();
var encoder = new JpegEncoder()
{
Quality = 85 // 85% qualidade
};
processedImage.SaveAsJpeg(outputStream, encoder);
var result = outputStream.ToArray();
_logger.LogDebug("Image processing completed: Output size={Size}KB", result.Length / 1024);
return result;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process image. Input size: {Size} bytes, Exception type: {ExceptionType}",
originalBytes.Length, ex.GetType().Name);
throw;
}
}); });
} }
@ -207,10 +246,18 @@ public class GridFSImageStorage : IImageStorageService
{ {
try try
{ {
_logger.LogDebug("Validating image resolution for {Size} bytes", imageBytes.Length);
using var image = Image.Load(imageBytes); using var image = Image.Load(imageBytes);
_logger.LogDebug("Image loaded successfully: {Width}x{Height}px, Format: {Format}",
image.Width, image.Height, image.Metadata.DecodedImageFormat?.Name ?? "Unknown");
if (image.Width > MAX_RESOLUTION || image.Height > MAX_RESOLUTION) if (image.Width > MAX_RESOLUTION || image.Height > MAX_RESOLUTION)
{ {
_logger.LogWarning("Image resolution too high: {Width}x{Height}px (max: {MaxResolution}x{MaxResolution}px)",
image.Width, image.Height, MAX_RESOLUTION, MAX_RESOLUTION);
throw new ArgumentException( throw new ArgumentException(
$"Resolução muito alta. Máximo permitido: {MAX_RESOLUTION}x{MAX_RESOLUTION}px. " + $"Resolução muito alta. Máximo permitido: {MAX_RESOLUTION}x{MAX_RESOLUTION}px. " +
$"Sua imagem: {image.Width}x{image.Height}px"); $"Sua imagem: {image.Width}x{image.Height}px");
@ -218,7 +265,19 @@ public class GridFSImageStorage : IImageStorageService
} }
catch (Exception ex) when (!(ex is ArgumentException)) catch (Exception ex) when (!(ex is ArgumentException))
{ {
throw new ArgumentException("Arquivo de imagem inválido ou corrompido"); _logger.LogError(ex, "Failed to validate image resolution. Image size: {Size} bytes, Exception type: {ExceptionType}",
imageBytes.Length, ex.GetType().Name);
// Log mais detalhes sobre o tipo de erro
var errorDetails = ex switch
{
OutOfMemoryException => "Imagem muito grande para processar",
UnknownImageFormatException => "Formato de imagem não suportado",
InvalidImageContentException => "Conteúdo de imagem inválido",
_ => $"Erro inesperado: {ex.GetType().Name}"
};
throw new ArgumentException($"Arquivo de imagem inválido ou corrompido. {errorDetails}");
} }
}); });
} }

View File

@ -0,0 +1,84 @@
using Microsoft.AspNetCore.Authentication;
using Microsoft.Extensions.Options;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using BCards.Web.Services;
using BCards.Web.Models;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace BCards.Web.TestSupport
{
// Define o nome do nosso esquema de autenticação de teste
public static class TestAuthConstants
{
public const string AuthenticationScheme = "Test";
}
// Opções para o nosso manipulador, embora não precisemos de nenhuma
public class TestAuthSchemeOptions : AuthenticationSchemeOptions { }
// O manipulador de autenticação de teste
public class TestAuthHandler : AuthenticationHandler<TestAuthSchemeOptions>
{
private readonly IAuthService _authService;
public TestAuthHandler(
IOptionsMonitor<TestAuthSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
ISystemClock clock,
IAuthService authService) // Injetamos o AuthService para criar/atualizar o usuário no DB
: base(options, logger, encoder, clock)
{
_authService = authService;
}
// Este método é chamado quando um [Authorize] falha e um "Challenge" é emitido.
// É aqui que nossa mágica acontece.
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
// 1. Criar um usuário de teste falso com as informações que quisermos.
var testClaims = new[]
{
new Claim(ClaimTypes.NameIdentifier, "test-user-google-12345"),
new Claim(ClaimTypes.Name, "Usuário de Teste"),
new Claim(ClaimTypes.Email, "test.user@example.com"),
new Claim("picture", "/img/test-user-avatar.png"),
new Claim("urn:google:sub", "test-user-google-12345"),
new Claim("urn:google:email_verified", "true", ClaimValueTypes.Boolean)
};
var testIdentity = new ClaimsIdentity(testClaims, "Test");
var testPrincipal = new ClaimsPrincipal(testIdentity);
// 2. Usar o AuthService para garantir que este usuário exista no banco de dados.
// Isso simula o comportamento real do GoogleCallback.
var user = await _authService.CreateOrUpdateUserFromClaimsAsync(testPrincipal);
// 3. Criar os claims finais para o cookie de autenticação da aplicação.
var appClaims = new[]
{
new Claim(ClaimTypes.NameIdentifier, user.Id),
new Claim(ClaimTypes.Name, user.Name),
new Claim(ClaimTypes.Email, user.Email),
new Claim("picture", user.ProfileImage ?? "")
};
var appIdentity = new ClaimsIdentity(appClaims, CookieAuthenticationDefaults.AuthenticationScheme);
var appPrincipal = new ClaimsPrincipal(appIdentity);
// 4. Logar o usuário na aplicação usando o esquema de cookies padrão.
await Context.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, appPrincipal, properties);
// 5. Redirecionar o navegador para o destino original.
Context.Response.Redirect(properties.RedirectUri ?? "/");
}
// HandleAuthenticateAsync é necessário para completar a interface, mas não precisamos de lógica aqui
// porque o login real é feito pelo esquema de cookies padrão depois que HandleChallengeAsync é executado.
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
return Task.FromResult(AuthenticateResult.NoResult());
}
}
}

View File

@ -55,6 +55,20 @@
</button> </button>
</form> </form>
@if (ViewBag.IsTestingEnvironment != null && ViewBag.IsTestingEnvironment)
{
<form asp-controller="Auth" asp-action="LoginWithTest" method="post" class="mb-3">
@if (!string.IsNullOrEmpty(returnUrl))
{
<input type="hidden" name="returnUrl" value="@returnUrl" />
}
<button type="submit" class="btn btn-warning w-100 mb-2 d-flex align-items-center justify-content-center">
<i class="me-2">🧪</i>
Login de Teste
</button>
</form>
}
<div class="text-center"> <div class="text-center">
<small class="text-muted"> <small class="text-muted">
Não temos acesso à sua senha. <br> Não temos acesso à sua senha. <br>

View File

@ -0,0 +1,25 @@
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"System": "Information",
"Microsoft": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"Stripe": {
"PublishableKey": "pk_test_51RjUmIBMIadsOxJVP4bWc54pHEOSf5km1hpOkOBSoGVoKxI46N4KSWtevpXCSq68OjFazBuXmPJGBwZ1KDN5MNJy003lj1YmAS",
"SecretKey": "sk_test_51RjUmIBMIadsOxJVeqsMFxnZ8ePR7d8IbnaF4sAwBVJv9rrfODPEQ2C9fF3beoABpITdfzEk0ZDzGTTQfvKv63xI00PeZoABGO",
"WebhookSecret": "whsec_8d189c137ff170ab5e62498003512b9d073e2db50c50ed7d8712b7ef11a37543",
"Environment": "test"
},
"Serilog": {
"OpenSearchUrl": "http://192.168.0.100:9200",
},
"DetailedErrors": true,
"MongoDb": {
"ConnectionString": "mongodb://localhost:27017",
"DatabaseName": "BCardsDB_Dev"
},
"BaseUrl": "https://localhost:49178"
}