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 "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,12 +537,12 @@ 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/')
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Extract version
|
- name: Extract version
|
||||||
id: version
|
id: version
|
||||||
@ -486,67 +553,67 @@ jobs:
|
|||||||
[ -z "$VERSION" ] && VERSION="0.0.1"
|
[ -z "$VERSION" ] && VERSION="0.0.1"
|
||||||
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
|
||||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||||
chmod 600 ~/.ssh/id_rsa
|
chmod 600 ~/.ssh/id_rsa
|
||||||
|
|
||||||
# Adiciona hosts conhecidos
|
# Adiciona hosts conhecidos
|
||||||
ssh-keyscan -H 141.148.162.114 >> ~/.ssh/known_hosts
|
ssh-keyscan -H 141.148.162.114 >> ~/.ssh/known_hosts
|
||||||
ssh-keyscan -H 129.146.116.218 >> ~/.ssh/known_hosts
|
ssh-keyscan -H 129.146.116.218 >> ~/.ssh/known_hosts
|
||||||
|
|
||||||
# Testa a chave SSH
|
# Testa a chave SSH
|
||||||
ssh-add ~/.ssh/id_rsa 2>/dev/null || echo "SSH key loaded"
|
ssh-add ~/.ssh/id_rsa 2>/dev/null || echo "SSH key loaded"
|
||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
# Puxa nova imagem
|
# Puxa nova imagem
|
||||||
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
|
docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
|
||||||
|
|
||||||
# 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 "===================="
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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");
|
||||||
|
|
||||||
|
|||||||
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -32,22 +32,40 @@ 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}");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
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>
|
</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>
|
||||||
|
|||||||
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