fix: program.cs
All checks were successful
Deploy QR Rapido / test (push) Successful in 27s
Deploy QR Rapido / build-and-push (push) Successful in 6m52s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 1m18s

This commit is contained in:
Ricardo Carneiro 2025-08-21 22:25:32 -03:00
parent 447d2863f4
commit 346aa86216

View File

@ -1,237 +1,238 @@
using BCards.Web.Configuration;
using BCards.Web.Services;
using BCards.Web.Repositories;
using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.MicrosoftAccount; using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Options; using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Localization;
using MongoDB.Driver; using MongoDB.Driver;
using System.Globalization; using QRRapidoApp.Data;
using QRRapidoApp.Middleware;
using QRRapidoApp.Providers;
using QRRapidoApp.Services;
using QRRapidoApp.Services.Monitoring;
using QRRapidoApp.Services.HealthChecks;
using StackExchange.Redis;
using Stripe; using Stripe;
using Microsoft.AspNetCore.Authentication.OAuth; using System.Globalization;
using SendGrid; using Serilog;
using BCards.Web.Middleware; using Serilog.Events;
using Microsoft.AspNetCore.Http.Features; using Serilog.Sinks.SystemConsole.Themes;
using Microsoft.AspNetCore.Mvc.Razor;
using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// 🔥 CONFIGURAR FORWARDED HEADERS NO BUILDER // Configure Serilog
builder.Services.Configure<ForwardedHeadersOptions>(options => Log.Logger = new LoggerConfiguration()
{ .ReadFrom.Configuration(builder.Configuration)
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; .Enrich.FromLogContext()
options.RequireHeaderSymmetry = false; .Enrich.WithEnvironmentName()
options.KnownNetworks.Clear(); .Enrich.WithProcessId()
options.KnownProxies.Clear(); .Enrich.WithThreadId()
// 🚨 PERMITIR QUALQUER PROXY (NGINX) .Enrich.WithProperty("ApplicationName", builder.Configuration["ApplicationName"] ?? "QRRapido")
options.ForwardLimit = null; .Enrich.WithProperty("Environment", "Dev")
}); .WriteTo.Async(a => a.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}",
theme: AnsiConsoleTheme.Code))
.WriteTo.Async(a => a.Seq(builder.Configuration["Serilog:SeqUrl"],
apiKey: builder.Configuration["Serilog:ApiKey"]=="" ? null : builder.Configuration["Serilog:ApiKey"]))
.CreateLogger();
// Add services to the container. builder.Host.UseSerilog();
// Add services to the container
builder.Services.AddControllersWithViews() builder.Services.AddControllersWithViews()
.AddRazorRuntimeCompilation() .AddViewLocalization(Microsoft.AspNetCore.Mvc.Razor.LanguageViewLocationExpanderFormat.Suffix)
.AddViewLocalization()
.AddDataAnnotationsLocalization(); .AddDataAnnotationsLocalization();
// MongoDB Configuration builder.Services.AddDistributedMemoryCache(); // Armazena os dados da sess<73>o na mem<65>ria
builder.Services.Configure<MongoDbSettings>( builder.Services.AddSession(options =>
builder.Configuration.GetSection("MongoDb"));
builder.Services.AddSingleton<IMongoClient>(serviceProvider =>
{ {
var settings = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value; options.Cookie.HttpOnly = true;
return new MongoClient(settings.ConnectionString); options.Cookie.IsEssential = true;
options.IdleTimeout = TimeSpan.FromMinutes(30); // Tempo de expira<72><61>o
}); });
builder.Services.AddScoped(serviceProvider => // Add HttpClient for health checks
builder.Services.AddHttpClient();
// MongoDB Configuration - optional for development
var mongoConnectionString = builder.Configuration.GetConnectionString("MongoDB");
if (!string.IsNullOrEmpty(mongoConnectionString))
{ {
var client = serviceProvider.GetRequiredService<IMongoClient>(); try
var settings = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value; {
return client.GetDatabase(settings.DatabaseName); builder.Services.AddSingleton<IMongoClient>(serviceProvider =>
}); {
return new MongoClient(mongoConnectionString);
// Stripe Configuration });
builder.Services.Configure<StripeSettings>( builder.Services.AddScoped<MongoDbContext>();
builder.Configuration.GetSection("Stripe")); }
catch
// OAuth Configuration {
builder.Services.Configure<GoogleAuthSettings>( // MongoDB not available - services will handle gracefully
builder.Configuration.GetSection("Authentication:Google")); builder.Services.AddScoped<MongoDbContext>();
}
builder.Services.Configure<MicrosoftAuthSettings>( }
builder.Configuration.GetSection("Authentication:Microsoft")); else
// Adicionar configurações
builder.Services.Configure<ModerationSettings>(
builder.Configuration.GetSection("Moderation"));
// Authentication
builder.Services.AddAuthentication(options =>
{ {
options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; // Development mode without MongoDB
options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme; builder.Services.AddScoped<MongoDbContext>();
}) }
.AddCookie(options =>
// Cache Configuration - use Redis if available, otherwise memory cache
var redisConnectionString = builder.Configuration.GetConnectionString("Redis");
if (!string.IsNullOrEmpty(redisConnectionString))
{ {
options.LoginPath = "/Auth/Login"; try
options.LogoutPath = "/Auth/Logout"; {
builder.Services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisConnectionString;
});
}
catch
{
// Fallback to memory cache if Redis fails
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IDistributedCache, QRRapidoApp.Services.MemoryDistributedCacheWrapper>();
}
}
else
{
// Use memory cache when Redis is not configured
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IDistributedCache, MemoryDistributedCacheWrapper>();
}
// Authentication Configuration
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(options =>
{
options.LoginPath = "/Account/Login";
options.LogoutPath = "/Account/Logout";
options.ExpireTimeSpan = TimeSpan.FromDays(30); options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true; options.SlidingExpiration = true;
}) })
.AddGoogle(options => .AddGoogle(options =>
{ {
var googleAuth = builder.Configuration.GetSection("Authentication:Google"); options.ClientId = builder.Configuration["Authentication:Google:ClientId"];
options.ClientId = googleAuth["ClientId"] ?? ""; options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"];
options.ClientSecret = googleAuth["ClientSecret"] ?? "";
}) // ADICIONE ESTAS LINHAS:
.AddMicrosoftAccount(options => options.Events.OnRedirectToAuthorizationEndpoint = context =>
{ {
var msAuth = builder.Configuration.GetSection("Authentication:Microsoft"); context.Response.Redirect(context.RedirectUri);
options.ClientId = msAuth["ClientId"] ?? ""; return Task.CompletedTask;
options.ClientSecret = msAuth["ClientSecret"] ?? ""; };
options.CallbackPath = "/signin-microsoft";
// OU use este método alternativo:
options.Scope.Add("email");
options.Scope.Add("profile");
options.SaveTokens = true;
}).AddMicrosoftAccount(MicrosoftAccountDefaults.AuthenticationScheme, options =>
{
options.ClientId = builder.Configuration["Authentication:Microsoft:ClientId"];
options.ClientSecret = builder.Configuration["Authentication:Microsoft:ClientSecret"];
// Força seleção de conta a cada login
options.AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; options.AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
options.TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; options.TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
// 🔥 SOLUÇÃO RADICAL: SEMPRE FORÇAR HTTPS // Força sempre mostrar a tela de seleção de conta
options.Events = new OAuthEvents options.Events.OnRedirectToAuthorizationEndpoint = context =>
{ {
OnRedirectToAuthorizationEndpoint = context => context.Response.Redirect(context.RedirectUri + "&prompt=select_account");
{
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<Program>>();
// Debug info
logger.LogWarning($"=== MICROSOFT AUTH DEBUG ===");
logger.LogWarning($"Original RedirectUri: {context.RedirectUri}");
logger.LogWarning($"Request Scheme: {context.Request.Scheme}");
logger.LogWarning($"Request IsHttps: {context.Request.IsHttps}");
logger.LogWarning($"Request Host: {context.Request.Host}");
logger.LogWarning($"X-Forwarded-Proto: {context.Request.Headers["X-Forwarded-Proto"]}");
// 🚨 FORÇA HTTPS SEMPRE (exceto localhost)
var originalUri = context.RedirectUri;
// Se contém bcards.site, força HTTPS
if (originalUri.Contains("bcards.site"))
{
context.RedirectUri = originalUri
.Replace("http://bcards.site", "https://bcards.site")
.Replace("http%3A%2F%2Fbcards.site", "https%3A%2F%2Fbcards.site");
logger.LogWarning($"FORCED HTTPS - Modified RedirectUri: {context.RedirectUri}");
}
// Adiciona prompt=login para forçar seleção de conta
var redirectUri = context.RedirectUri;
if (!redirectUri.Contains("prompt="))
{
redirectUri += "&prompt=login";
}
logger.LogWarning($"Final RedirectUri: {redirectUri}");
context.Response.Redirect(redirectUri);
return Task.CompletedTask; return Task.CompletedTask;
}
}; };
}); });
// Stripe Configuration
StripeConfiguration.ApiKey = builder.Configuration["Stripe:SecretKey"];
// Localization // Localization
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); builder.Services.AddLocalization(options => options.ResourcesPath = "");
//builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.Configure<RequestLocalizationOptions>(options => builder.Services.Configure<RequestLocalizationOptions>(options =>
{ {
var supportedCultures = new[] var supportedCultures = new[]
{ {
new CultureInfo("pt-BR"), new CultureInfo("pt-BR"),
new CultureInfo("es-ES") new CultureInfo("es-PY"),
new CultureInfo("es")
}; };
options.DefaultRequestCulture = new RequestCulture("pt-BR"); options.DefaultRequestCulture = new RequestCulture("pt-BR", "pt-BR");
options.SupportedCultures = supportedCultures; options.SupportedCultures = supportedCultures;
options.SupportedUICultures = supportedCultures; options.SupportedUICultures = supportedCultures;
options.FallBackToParentCultures = false;
options.FallBackToParentUICultures = false;
// Clear default providers and add custom ones in priority order
options.RequestCultureProviders.Clear();
options.RequestCultureProviders.Add(new CustomRouteDataRequestCultureProvider());
options.RequestCultureProviders.Add(new QueryStringRequestCultureProvider());
options.RequestCultureProviders.Add(new CookieRequestCultureProvider());
}); });
// Register Services // Custom Services
builder.Services.AddScoped<IUserRepository, UserRepository>(); builder.Services.AddScoped<IQRCodeService, QRRapidoService>();
builder.Services.AddScoped<IUserPageRepository, UserPageRepository>(); builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<ICategoryRepository, CategoryRepository>(); builder.Services.AddScoped<IPlanService, QRRapidoApp.Services.PlanService>();
builder.Services.AddScoped<ISubscriptionRepository, SubscriptionRepository>(); builder.Services.AddScoped<AdDisplayService>();
builder.Services.AddSingleton<IModerationAuthService, ModerationAuthService>(); builder.Services.AddScoped<StripeService>();
//builder.Services.AddScoped<IModerationAuthService, ModerationAuthService>(); builder.Services.AddScoped<LogoReadabilityAnalyzer>();
builder.Services.AddScoped<IUserPageService, UserPageService>();
builder.Services.AddScoped<IThemeService, ThemeService>();
builder.Services.AddScoped<ISeoService, SeoService>();
builder.Services.AddScoped<IAuthService, AuthService>();
builder.Services.AddScoped<IPaymentService, PaymentService>();
builder.Services.AddScoped<ICategoryService, CategoryService>();
builder.Services.AddScoped<IOpenGraphService, OpenGraphService>();
builder.Services.AddScoped<IModerationService, ModerationService>();
builder.Services.AddScoped<IEmailService, EmailService>();
// Image Storage Service
builder.Services.AddScoped<IImageStorageService, GridFSImageStorage>();
// Configure upload limits for file uploads
builder.Services.Configure<FormOptions>(options =>
{
options.MultipartBodyLengthLimit = 10 * 1024 * 1024; // 10MB for forms with files
options.ValueLengthLimit = int.MaxValue;
options.ValueCountLimit = int.MaxValue;
options.KeyLengthLimit = int.MaxValue;
});
// 🔥 NOVO: LivePage Services
builder.Services.AddScoped<ILivePageRepository, LivePageRepository>();
builder.Services.AddScoped<ILivePageService, LivePageService>();
// Add HttpClient for OpenGraphService
builder.Services.AddHttpClient<OpenGraphService>();
// Add SendGrid
builder.Services.AddSingleton<ISendGridClient>(provider =>
{
var apiKey = builder.Configuration["SendGrid:ApiKey"];
return new SendGridClient(apiKey);
});
// Background Services // Background Services
builder.Services.AddHostedService<TrialExpirationService>(); builder.Services.AddHostedService<HistoryCleanupService>();
// Response Caching // Monitoring Services
builder.Services.AddResponseCaching(); if (builder.Configuration.GetValue<bool>("ResourceMonitoring:Enabled", true))
builder.Services.AddMemoryCache(); {
builder.Services.AddHostedService<ResourceMonitoringService>();
}
builder.Services.AddRazorPages(); if (builder.Configuration.GetValue<bool>("MongoDbMonitoring:Enabled", true))
{
builder.Services.AddHostedService<MongoDbMonitoringService>();
}
// CORS for API endpoints
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigins", policy =>
{
policy.WithOrigins("https://qrrapido.site", "https://www.qrrapido.site")
.AllowAnyHeader()
.AllowAnyMethod();
});
});
// Health checks with custom implementations
builder.Services.AddScoped<MongoDbHealthCheck>();
builder.Services.AddScoped<SeqHealthCheck>();
builder.Services.AddScoped<ResourceHealthCheck>();
builder.Services.AddScoped<ExternalServicesHealthCheck>();
builder.Services.AddHealthChecks()
.AddCheck<MongoDbHealthCheck>("mongodb")
.AddCheck<SeqHealthCheck>("seq")
.AddCheck<ResourceHealthCheck>("resources")
.AddCheck<ExternalServicesHealthCheck>("external_services");
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownProxies.Clear();
options.KnownNetworks.Clear();
});
var app = builder.Build(); var app = builder.Build();
// 🔥 PRIMEIRA COISA APÓS BUILD - FORWARDED HEADERS + BASE URL OVERRIDE
app.UseForwardedHeaders(); app.UseForwardedHeaders();
// 🚨 FORÇA BASE URL HTTPS EM PRODUÇÃO // Configure the HTTP request pipeline
if (!app.Environment.IsDevelopment())
{
app.Use(async (context, next) =>
{
// Força o contexto a pensar que está em HTTPS
context.Request.Scheme = "https";
// Override do Host se necessário
if (context.Request.Host.Host == "bcards.site")
{
context.Request.Host = new HostString("bcards.site", 443);
}
await next();
});
}
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment()) if (!app.Environment.IsDevelopment())
{ {
app.UseExceptionHandler("/Home/Error"); app.UseExceptionHandler("/Home/Error");
@ -239,128 +240,65 @@ if (!app.Environment.IsDevelopment())
} }
app.UseHttpsRedirection(); app.UseHttpsRedirection();
app.UseStaticFiles(); app.UseStaticFiles();
// Language redirection middleware (before routing)
app.UseMiddleware<LanguageRedirectionMiddleware>();
app.UseRouting(); app.UseRouting();
// Localization middleware (after routing so route data is available)
app.UseRequestLocalization(); app.UseRequestLocalization();
app.UseCors("AllowSpecificOrigins");
app.UseAuthentication(); app.UseAuthentication();
app.UseAuthorization(); app.UseAuthorization();
// Add custom middleware app.UseSession();
app.UseMiddleware<BCards.Web.Middleware.PlanLimitationMiddleware>();
app.UseMiddleware<BCards.Web.Middleware.PageStatusMiddleware>();
app.UseMiddleware<BCards.Web.Middleware.PreviewTokenMiddleware>();
app.UseMiddleware<ModerationAuthMiddleware>();
// 🔥 DEBUG MIDDLEWARE MELHORADO // Custom middleware
app.Use(async (context, next) => app.UseMiddleware<LastLoginUpdateMiddleware>();
{
// Debug geral
Console.WriteLine($"=== REQUEST DEBUG ===");
Console.WriteLine($"Path: {context.Request.Path}");
Console.WriteLine($"Query: {context.Request.QueryString}");
Console.WriteLine($"Method: {context.Request.Method}");
Console.WriteLine($"Scheme: {context.Request.Scheme}");
Console.WriteLine($"IsHttps: {context.Request.IsHttps}");
Console.WriteLine($"Host: {context.Request.Host}");
// Debug específico para Microsoft signin // Health check endpoint
if (context.Request.Path.StartsWithSegments("/signin-microsoft")) app.MapHealthChecks("/health");
{
var logger = context.RequestServices.GetRequiredService<ILogger<Program>>();
logger.LogWarning($"=== SIGNIN-MICROSOFT CALLBACK DEBUG ===");
logger.LogWarning($"Path: {context.Request.Path}");
logger.LogWarning($"Query: {context.Request.QueryString}");
logger.LogWarning($"Method: {context.Request.Method}");
logger.LogWarning($"Scheme: {context.Request.Scheme}");
logger.LogWarning($"IsHttps: {context.Request.IsHttps}");
logger.LogWarning($"Host: {context.Request.Host}");
logger.LogWarning($"X-Forwarded-Proto: {context.Request.Headers["X-Forwarded-Proto"]}");
logger.LogWarning($"X-Forwarded-For: {context.Request.Headers["X-Forwarded-For"]}");
logger.LogWarning($"All Headers:");
foreach (var header in context.Request.Headers)
{
logger.LogWarning($" {header.Key}: {header.Value}");
}
}
await next();
});
app.UseResponseCaching();
// Rotas específicas primeiro
app.MapControllerRoute(
name: "userpage-preview-path",
pattern: "page/preview/{category}/{slug}",
defaults: new { controller = "UserPage", action = "Preview" },
constraints: new { category = @"^[a-zA-Z-]+$", slug = @"^[a-z0-9-]+$" });
app.MapControllerRoute(
name: "userpage-click",
pattern: "page/click/{pageId}",
defaults: new { controller = "UserPage", action = "RecordClick" });
app.MapControllerRoute(
name: "moderation",
pattern: "moderation/{action=Dashboard}/{id?}",
defaults: new { controller = "Moderation" });
// Rota principal que vai pegar ?preview=token
//app.MapControllerRoute( //app.MapControllerRoute(
// name: "userpage", // name: "auth",
// pattern: "page/{category}/{slug}", // pattern: "signin-{provider}",
// defaults: new { controller = "UserPage", action = "Display" }, // defaults: new { controller = "Account", action = "ExternalLoginCallback" });
// constraints: new { category = @"^[a-zA-Z-]+$", slug = @"^[a-z0-9-]+$" });
// 🔥 NOVA ROTA: LivePageController para páginas otimizadas de SEO //app.MapControllerRoute(
// name: "account",
// pattern: "Account/{action}",
// defaults: new { controller = "Account" });
// Language routes (must be first)
app.MapControllerRoute( app.MapControllerRoute(
name: "livepage", name: "localized",
pattern: "page/{category}/{slug}", pattern: "{culture:regex(^(pt-BR|es-PY|es)$)}/{controller=Home}/{action=Index}/{id?}");
defaults: new { controller = "LivePage", action = "Display" },
constraints: new
{
category = @"^[a-zA-Z0-9\-\u00C0-\u017F]+$", // ← Aceita acentos
slug = @"^[a-z0-9-]+$"
});
// Rota padrão por último // API routes
app.MapControllerRoute(
name: "api",
pattern: "api/{controller}/{action=Index}/{id?}");
// Default fallback route (for development/testing without culture)
app.MapControllerRoute( app.MapControllerRoute(
name: "default", name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}"); pattern: "{controller=Home}/{action=Index}/{id?}");
// Initialize default data try
using (var scope = app.Services.CreateScope())
{ {
var themeService = scope.ServiceProvider.GetRequiredService<IThemeService>(); Log.Information("Starting QRRapido application");
var categoryService = scope.ServiceProvider.GetRequiredService<ICategoryService>(); app.Run();
}
try catch (Exception ex)
{ {
// Initialize themes Log.Fatal(ex, "QRRapido application terminated unexpectedly");
var existingThemes = await themeService.GetAvailableThemesAsync(); }
if (!existingThemes.Any()) finally
{ {
await themeService.InitializeDefaultThemesAsync(); Log.CloseAndFlush();
}
// Initialize categories
var existingCategories = await categoryService.GetAllCategoriesAsync();
if (!existingCategories.Any())
{
await categoryService.InitializeDefaultCategoriesAsync();
}
}
catch (Exception ex)
{
var logger = scope.ServiceProvider.GetRequiredService<ILogger<Program>>();
logger.LogError(ex, "Error initializing default data");
}
} }
app.Run();
// Make Program accessible for integration tests
public partial class Program { }