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
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:
parent
7b0bc89f06
commit
e043c853b1
@ -123,9 +123,9 @@ jobs:
|
||||
|
||||
echo "tag=$VERSION" >> $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 "deploy_target=staging" >> $GITHUB_OUTPUT
|
||||
echo "deploy_target=testing" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
@ -197,6 +197,28 @@ jobs:
|
||||
# Cria o arquivo de configuração para produção
|
||||
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": {
|
||||
"PublishableKey": "${{ vars.STRIPE_PUBLISHABLE_KEY }}",
|
||||
"SecretKey": "${{ secrets.STRIPE_SECRET_KEY }}",
|
||||
@ -334,6 +356,51 @@ jobs:
|
||||
"${{ 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": {
|
||||
"OpenSearchUrl": "${{ vars.OPENSEARCH_URL || 'http://localhost:9201' }}"
|
||||
}
|
||||
@ -470,12 +537,12 @@ jobs:
|
||||
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"'
|
||||
|
||||
deploy-staging:
|
||||
name: Deploy to Staging (x86 - Local)
|
||||
deploy-test:
|
||||
name: Deploy to Test (x86 - Local)
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build-and-push]
|
||||
if: startsWith(github.ref_name, 'Release/')
|
||||
|
||||
|
||||
steps:
|
||||
- name: Extract version
|
||||
id: version
|
||||
@ -486,67 +553,67 @@ jobs:
|
||||
[ -z "$VERSION" ] && VERSION="0.0.1"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "📦 Deploying version: $VERSION"
|
||||
|
||||
- name: Deploy to Staging Server
|
||||
|
||||
- name: Deploy to Test Server
|
||||
run: |
|
||||
echo "🚀 Deploying to staging server (x86)..."
|
||||
|
||||
echo "🚀 Deploying to test server (x86)..."
|
||||
|
||||
# Configura SSH (igual ao QRRapido)
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
|
||||
# Adiciona hosts conhecidos
|
||||
ssh-keyscan -H 141.148.162.114 >> ~/.ssh/known_hosts
|
||||
ssh-keyscan -H 129.146.116.218 >> ~/.ssh/known_hosts
|
||||
|
||||
|
||||
# Testa a chave SSH
|
||||
ssh-add ~/.ssh/id_rsa 2>/dev/null || echo "SSH key loaded"
|
||||
|
||||
|
||||
# Deploy no Servidor Local x86
|
||||
ssh -o StrictHostKeyChecking=no ubuntu@192.168.0.100 << EOF
|
||||
echo "🔄 Atualizando Servidor Staging..."
|
||||
|
||||
echo "🔄 Atualizando Servidor Teste..."
|
||||
|
||||
# Remove containers bcards-infrastructure se existirem
|
||||
docker stop bcards-infrastructure bcards-test-app || true
|
||||
docker rm bcards-infrastructure bcards-test-app || true
|
||||
|
||||
|
||||
# Para o container BCards atual se existir
|
||||
docker stop bcards-staging || true
|
||||
docker rm bcards-staging || true
|
||||
|
||||
docker stop bcards-test || true
|
||||
docker rm bcards-test || true
|
||||
|
||||
# Remove imagem antiga
|
||||
docker rmi ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} || true
|
||||
|
||||
|
||||
# Puxa nova imagem
|
||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
|
||||
|
||||
|
||||
# Executa novo container BCards
|
||||
docker run -d \
|
||||
--name bcards-staging \
|
||||
--name bcards-test \
|
||||
--restart unless-stopped \
|
||||
--network host \
|
||||
-e ASPNETCORE_ENVIRONMENT=Staging \
|
||||
-e ASPNETCORE_ENVIRONMENT=Testing \
|
||||
-e ASPNETCORE_URLS=http://+:8080 \
|
||||
-e Serilog__OpenSearchUrl="http://192.168.0.100:9200" \
|
||||
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
|
||||
|
||||
echo "✅ Servidor Staging atualizado"
|
||||
|
||||
echo "✅ Servidor Teste atualizado"
|
||||
EOF
|
||||
|
||||
- name: Health Check Staging
|
||||
- name: Health Check Test
|
||||
run: |
|
||||
echo "🏥 Verificando saúde do servidor de staging..."
|
||||
echo "🏥 Verificando saúde do servidor de teste..."
|
||||
sleep 30
|
||||
|
||||
echo "Verificando Servidor Staging (x86)..."
|
||||
ssh -o StrictHostKeyChecking=no ubuntu@192.168.0.100 'curl -f http://localhost:8080/health || echo "⚠️ Servidor staging pode não estar respondendo"'
|
||||
|
||||
echo "Verificando Servidor Teste (x86)..."
|
||||
ssh -o StrictHostKeyChecking=no ubuntu@192.168.0.100 'curl -f http://localhost:8080/health || echo "⚠️ Servidor teste pode não estar respondendo"'
|
||||
|
||||
cleanup:
|
||||
name: Cleanup Old Resources
|
||||
runs-on: ubuntu-latest
|
||||
needs: [deploy-production, deploy-staging]
|
||||
if: always() && (needs.deploy-production.result == 'success' || needs.deploy-staging.result == 'success')
|
||||
needs: [deploy-production, deploy-test]
|
||||
if: always() && (needs.deploy-production.result == 'success' || needs.deploy-test.result == 'success')
|
||||
|
||||
steps:
|
||||
- name: Cleanup containers and images
|
||||
@ -592,7 +659,7 @@ jobs:
|
||||
deployment-summary:
|
||||
name: Deployment Summary
|
||||
runs-on: ubuntu-latest
|
||||
needs: [deploy-production, deploy-staging]
|
||||
needs: [deploy-production, deploy-test]
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
@ -610,10 +677,10 @@ jobs:
|
||||
echo "📦 Tag: latest"
|
||||
echo "🔗 Status: ${{ needs.deploy-production.result }}"
|
||||
else
|
||||
echo "🌍 Environment: Staging (x86)"
|
||||
echo "🌍 Environment: Testing (x86)"
|
||||
echo "🖥️ Server: 192.168.0.100"
|
||||
echo "📦 Tag: ${{ github.ref_name }}"
|
||||
echo "🔗 Status: ${{ needs.deploy-staging.result }}"
|
||||
echo "🔗 Status: ${{ needs.deploy-test.result }}"
|
||||
fi
|
||||
|
||||
echo "===================="
|
||||
|
||||
@ -241,9 +241,15 @@ public class AdminController : Controller
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Userid: {userId} - Error uploading profile image");
|
||||
ModelState.AddModelError("ProfileImageFile", "Erro ao fazer upload da imagem. Tente novamente.");
|
||||
TempData["ImageError"] = "Erro ao processar a imagem. Verifique o formato e tamanho.";
|
||||
_logger.LogError(ex, "Userid: {UserId} - Error uploading profile image. FileName: {FileName}, ContentType: {ContentType}, Size: {Size}KB, ExceptionType: {ExceptionType}",
|
||||
userId, model.ProfileImageFile?.FileName ?? "Unknown", model.ProfileImageFile?.ContentType ?? "Unknown",
|
||||
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
|
||||
var userPlanType = Enum.TryParse<PlanType>(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial;
|
||||
|
||||
@ -6,6 +6,7 @@ using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using System.Security.Claims;
|
||||
using BCards.Web.TestSupport;
|
||||
|
||||
namespace BCards.Web.Controllers;
|
||||
|
||||
@ -13,17 +14,20 @@ namespace BCards.Web.Controllers;
|
||||
public class AuthController : Controller
|
||||
{
|
||||
private readonly IAuthService _authService;
|
||||
private readonly IOAuthHealthService _oauthHealthService;
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
private readonly IOAuthHealthService _oauthHealthService;
|
||||
private readonly ILogger<AuthController> _logger;
|
||||
private readonly IWebHostEnvironment _env;
|
||||
|
||||
public AuthController(
|
||||
IAuthService authService,
|
||||
IOAuthHealthService oauthHealthService,
|
||||
ILogger<AuthController> logger)
|
||||
ILogger<AuthController> logger,
|
||||
IWebHostEnvironment env)
|
||||
{
|
||||
_authService = authService;
|
||||
_oauthHealthService = oauthHealthService;
|
||||
_logger = logger;
|
||||
_env = env;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
@ -31,6 +35,7 @@ public class AuthController : Controller
|
||||
public async Task<IActionResult> Login(string? returnUrl = null)
|
||||
{
|
||||
ViewBag.ReturnUrl = returnUrl;
|
||||
ViewBag.IsTestingEnvironment = _env.IsEnvironment("Testing");
|
||||
|
||||
// Verificar status dos OAuth providers e passar para a view
|
||||
var oauthStatus = await _oauthHealthService.CheckOAuthProvidersAsync();
|
||||
@ -143,6 +148,29 @@ public class AuthController : Controller
|
||||
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]
|
||||
[Route("GoogleCallback")]
|
||||
public async Task<IActionResult> GoogleCallback(string? returnUrl = null)
|
||||
|
||||
@ -17,6 +17,8 @@ using Serilog;
|
||||
using Serilog.Events;
|
||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||
using Serilog.Sinks.OpenSearch;
|
||||
using BCards.Web.TestSupport;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@ -265,12 +267,13 @@ builder.Services.Configure<ModerationSettings>(
|
||||
builder.Configuration.GetSection("Moderation"));
|
||||
|
||||
// Authentication
|
||||
builder.Services.AddAuthentication(options =>
|
||||
var authBuilder = builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
|
||||
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme;
|
||||
})
|
||||
.AddCookie(options =>
|
||||
// DefaultChallengeScheme will be set conditionally below
|
||||
});
|
||||
|
||||
authBuilder.AddCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/Auth/Login";
|
||||
options.LogoutPath = "/Auth/Logout";
|
||||
@ -280,8 +283,10 @@ builder.Services.AddAuthentication(options =>
|
||||
options.Cookie.IsEssential = true;
|
||||
options.Cookie.SameSite = SameSiteMode.Lax;
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
|
||||
})
|
||||
.AddGoogle(options =>
|
||||
});
|
||||
|
||||
// Always register Google and Microsoft authentication schemes
|
||||
authBuilder.AddGoogle(options =>
|
||||
{
|
||||
var googleAuth = builder.Configuration.GetSection("Authentication:Google");
|
||||
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
|
||||
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
|
||||
|
||||
|
||||
@ -7,6 +7,14 @@
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -32,22 +32,40 @@ public class GridFSImageStorage : IImageStorageService
|
||||
{
|
||||
try
|
||||
{
|
||||
_logger.LogInformation("Starting image upload: FileName={FileName}, ContentType={ContentType}, Size={Size}KB",
|
||||
fileName, contentType, imageBytes?.Length / 1024 ?? 0);
|
||||
|
||||
// Validações
|
||||
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");
|
||||
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
// Validar resolução da imagem
|
||||
_logger.LogDebug("Starting image resolution validation");
|
||||
await ValidateImageResolution(imageBytes);
|
||||
_logger.LogDebug("Image resolution validation completed successfully");
|
||||
|
||||
// Processar e redimensionar imagem
|
||||
_logger.LogDebug("Starting image processing");
|
||||
var processedImage = await ProcessImageAsync(imageBytes);
|
||||
|
||||
_logger.LogInformation("Image processed successfully: OriginalSize={OriginalSize}KB, ProcessedSize={ProcessedSize}KB",
|
||||
imageBytes.Length / 1024, processedImage.Length / 1024);
|
||||
|
||||
// Metadata
|
||||
var options = new GridFSUploadOptions
|
||||
{
|
||||
@ -161,37 +179,58 @@ public class GridFSImageStorage : IImageStorageService
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
using var originalImage = Image.Load(originalBytes);
|
||||
|
||||
// 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()
|
||||
try
|
||||
{
|
||||
Quality = 85 // 85% qualidade
|
||||
};
|
||||
_logger.LogDebug("Loading original image for processing");
|
||||
using var originalImage = Image.Load(originalBytes);
|
||||
|
||||
processedImage.SaveAsJpeg(outputStream, encoder);
|
||||
return outputStream.ToArray();
|
||||
_logger.LogDebug("Original image loaded: {Width}x{Height}px, Format: {Format}",
|
||||
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
|
||||
{
|
||||
_logger.LogDebug("Validating image resolution for {Size} bytes", imageBytes.Length);
|
||||
|
||||
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)
|
||||
{
|
||||
_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(
|
||||
$"Resolução muito alta. Máximo permitido: {MAX_RESOLUTION}x{MAX_RESOLUTION}px. " +
|
||||
$"Sua imagem: {image.Width}x{image.Height}px");
|
||||
@ -218,7 +265,19 @@ public class GridFSImageStorage : IImageStorageService
|
||||
}
|
||||
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}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
84
src/BCards.Web/TestSupport/TestAuthHandler.cs
Normal file
84
src/BCards.Web/TestSupport/TestAuthHandler.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -55,6 +55,20 @@
|
||||
</button>
|
||||
</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">
|
||||
<small class="text-muted">
|
||||
Não temos acesso à sua senha. <br>
|
||||
|
||||
25
src/BCards.Web/appsettings.Testing.json
Normal file
25
src/BCards.Web/appsettings.Testing.json
Normal 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"
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user