From e043c853b112d057774cbd3bf0b424e9e818d0e5 Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Wed, 17 Sep 2025 23:48:59 -0300 Subject: [PATCH] =?UTF-8?q?fix:=20logs=20para=20valida=C3=A7=C3=A3o=20de?= =?UTF-8?q?=20imagens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/deploy-bcards.yml | 135 +++++++++++++----- src/BCards.Web/Controllers/AdminController.cs | 12 +- src/BCards.Web/Controllers/AuthController.cs | 34 ++++- src/BCards.Web/Program.cs | 34 ++++- src/BCards.Web/Properties/launchSettings.json | 8 ++ src/BCards.Web/Services/GridFSImageStorage.cs | 127 +++++++++++----- src/BCards.Web/TestSupport/TestAuthHandler.cs | 84 +++++++++++ src/BCards.Web/Views/Auth/Login.cshtml | 14 ++ src/BCards.Web/appsettings.Testing.json | 25 ++++ 9 files changed, 393 insertions(+), 80 deletions(-) create mode 100644 src/BCards.Web/TestSupport/TestAuthHandler.cs create mode 100644 src/BCards.Web/appsettings.Testing.json diff --git a/.gitea/workflows/deploy-bcards.yml b/.gitea/workflows/deploy-bcards.yml index a7b3c04..9e002a3 100644 --- a/.gitea/workflows/deploy-bcards.yml +++ b/.gitea/workflows/deploy-bcards.yml @@ -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 "====================" diff --git a/src/BCards.Web/Controllers/AdminController.cs b/src/BCards.Web/Controllers/AdminController.cs index 8bdab6d..bd3a945 100644 --- a/src/BCards.Web/Controllers/AdminController.cs +++ b/src/BCards.Web/Controllers/AdminController.cs @@ -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(user.CurrentPlan, true, out var planType) ? planType : PlanType.Trial; diff --git a/src/BCards.Web/Controllers/AuthController.cs b/src/BCards.Web/Controllers/AuthController.cs index 0009647..1b1307b 100644 --- a/src/BCards.Web/Controllers/AuthController.cs +++ b/src/BCards.Web/Controllers/AuthController.cs @@ -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 _logger; + private readonly IOAuthHealthService _oauthHealthService; + private readonly ILogger _logger; + private readonly IWebHostEnvironment _env; public AuthController( IAuthService authService, IOAuthHealthService oauthHealthService, - ILogger logger) + ILogger logger, + IWebHostEnvironment env) { _authService = authService; _oauthHealthService = oauthHealthService; _logger = logger; + _env = env; } [HttpGet] @@ -31,6 +35,7 @@ public class AuthController : Controller public async Task 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 GoogleCallback(string? returnUrl = null) diff --git a/src/BCards.Web/Program.cs b/src/BCards.Web/Program.cs index f8e58a5..321ed90 100644 --- a/src/BCards.Web/Program.cs +++ b/src/BCards.Web/Program.cs @@ -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( 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(options => + { + options.DefaultChallengeScheme = TestAuthConstants.AuthenticationScheme; + }); + authBuilder.AddScheme(TestAuthConstants.AuthenticationScheme, _ => { }); +} +else +{ + authBuilder.Services.Configure(options => + { + options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme; + }); +} + // Localization builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); diff --git a/src/BCards.Web/Properties/launchSettings.json b/src/BCards.Web/Properties/launchSettings.json index b12dbb0..81929fb 100644 --- a/src/BCards.Web/Properties/launchSettings.json +++ b/src/BCards.Web/Properties/launchSettings.json @@ -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" } } } \ No newline at end of file diff --git a/src/BCards.Web/Services/GridFSImageStorage.cs b/src/BCards.Web/Services/GridFSImageStorage.cs index b50297f..2b2d9ed 100644 --- a/src/BCards.Web/Services/GridFSImageStorage.cs +++ b/src/BCards.Web/Services/GridFSImageStorage.cs @@ -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(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(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}"); } }); } diff --git a/src/BCards.Web/TestSupport/TestAuthHandler.cs b/src/BCards.Web/TestSupport/TestAuthHandler.cs new file mode 100644 index 0000000..18975f6 --- /dev/null +++ b/src/BCards.Web/TestSupport/TestAuthHandler.cs @@ -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 + { + private readonly IAuthService _authService; + + public TestAuthHandler( + IOptionsMonitor 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 HandleAuthenticateAsync() + { + return Task.FromResult(AuthenticateResult.NoResult()); + } + } +} diff --git a/src/BCards.Web/Views/Auth/Login.cshtml b/src/BCards.Web/Views/Auth/Login.cshtml index 617931a..41b64de 100644 --- a/src/BCards.Web/Views/Auth/Login.cshtml +++ b/src/BCards.Web/Views/Auth/Login.cshtml @@ -55,6 +55,20 @@ +@if (ViewBag.IsTestingEnvironment != null && ViewBag.IsTestingEnvironment) +{ +
+ @if (!string.IsNullOrEmpty(returnUrl)) + { + + } + +
+} +
Não temos acesso à sua senha.
diff --git a/src/BCards.Web/appsettings.Testing.json b/src/BCards.Web/appsettings.Testing.json new file mode 100644 index 0000000..ccf425a --- /dev/null +++ b/src/BCards.Web/appsettings.Testing.json @@ -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" +} \ No newline at end of file