From 346aa86216d258cf87f204ae1f4e243105dfe08d Mon Sep 17 00:00:00 2001 From: Ricardo Carneiro Date: Thu, 21 Aug 2025 22:25:32 -0300 Subject: [PATCH] fix: program.cs --- Program.cs | 528 +++++++++++++++++++++++------------------------------ 1 file changed, 233 insertions(+), 295 deletions(-) diff --git a/Program.cs b/Program.cs index 33410c9..b110585 100644 --- a/Program.cs +++ b/Program.cs @@ -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.Google; using Microsoft.AspNetCore.Authentication.MicrosoftAccount; 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 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 Microsoft.AspNetCore.Authentication.OAuth; -using SendGrid; -using BCards.Web.Middleware; -using Microsoft.AspNetCore.Http.Features; +using System.Globalization; +using Serilog; +using Serilog.Events; +using Serilog.Sinks.SystemConsole.Themes; +using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); -// 🔥 CONFIGURAR FORWARDED HEADERS NO BUILDER -builder.Services.Configure(options => -{ - options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; - options.RequireHeaderSymmetry = false; - options.KnownNetworks.Clear(); - options.KnownProxies.Clear(); - // 🚨 PERMITIR QUALQUER PROXY (NGINX) - options.ForwardLimit = null; -}); +// Configure Serilog +Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(builder.Configuration) + .Enrich.FromLogContext() + .Enrich.WithEnvironmentName() + .Enrich.WithProcessId() + .Enrich.WithThreadId() + .Enrich.WithProperty("ApplicationName", builder.Configuration["ApplicationName"] ?? "QRRapido") + .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() - .AddRazorRuntimeCompilation() - .AddViewLocalization() + .AddViewLocalization(Microsoft.AspNetCore.Mvc.Razor.LanguageViewLocationExpanderFormat.Suffix) .AddDataAnnotationsLocalization(); -// MongoDB Configuration -builder.Services.Configure( - builder.Configuration.GetSection("MongoDb")); - -builder.Services.AddSingleton(serviceProvider => +builder.Services.AddDistributedMemoryCache(); // Armazena os dados da sess�o na mem�ria +builder.Services.AddSession(options => { - var settings = serviceProvider.GetRequiredService>().Value; - return new MongoClient(settings.ConnectionString); + options.Cookie.HttpOnly = true; + options.Cookie.IsEssential = true; + options.IdleTimeout = TimeSpan.FromMinutes(30); // Tempo de expira��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(); - var settings = serviceProvider.GetRequiredService>().Value; - return client.GetDatabase(settings.DatabaseName); -}); + try + { + builder.Services.AddSingleton(serviceProvider => + { + return new MongoClient(mongoConnectionString); + }); + builder.Services.AddScoped(); + } + catch + { + // MongoDB not available - services will handle gracefully + builder.Services.AddScoped(); + } +} +else +{ + // Development mode without MongoDB + builder.Services.AddScoped(); +} + +// Cache Configuration - use Redis if available, otherwise memory cache +var redisConnectionString = builder.Configuration.GetConnectionString("Redis"); +if (!string.IsNullOrEmpty(redisConnectionString)) +{ + try + { + builder.Services.AddStackExchangeRedisCache(options => + { + options.Configuration = redisConnectionString; + }); + } + catch + { + // Fallback to memory cache if Redis fails + builder.Services.AddMemoryCache(); + builder.Services.AddSingleton(); + } +} +else +{ + // Use memory cache when Redis is not configured + builder.Services.AddMemoryCache(); + builder.Services.AddSingleton(); +} + +// Authentication Configuration +builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) + .AddCookie(options => + { + options.LoginPath = "/Account/Login"; + options.LogoutPath = "/Account/Logout"; + options.ExpireTimeSpan = TimeSpan.FromDays(30); + options.SlidingExpiration = true; + }) + .AddGoogle(options => + { + options.ClientId = builder.Configuration["Authentication:Google:ClientId"]; + options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]; + + // ADICIONE ESTAS LINHAS: + options.Events.OnRedirectToAuthorizationEndpoint = context => + { + context.Response.Redirect(context.RedirectUri); + return Task.CompletedTask; + }; + + // 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"]; + + options.AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; + options.TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; + + // Força sempre mostrar a tela de seleção de conta + options.Events.OnRedirectToAuthorizationEndpoint = context => + { + context.Response.Redirect(context.RedirectUri + "&prompt=select_account"); + return Task.CompletedTask; + }; + }); // Stripe Configuration -builder.Services.Configure( - builder.Configuration.GetSection("Stripe")); - -// OAuth Configuration -builder.Services.Configure( - builder.Configuration.GetSection("Authentication:Google")); - -builder.Services.Configure( - builder.Configuration.GetSection("Authentication:Microsoft")); - -// Adicionar configurações -builder.Services.Configure( - builder.Configuration.GetSection("Moderation")); - -// Authentication -builder.Services.AddAuthentication(options => -{ - options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; - options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme; -}) -.AddCookie(options => -{ - options.LoginPath = "/Auth/Login"; - options.LogoutPath = "/Auth/Logout"; - options.ExpireTimeSpan = TimeSpan.FromDays(30); - options.SlidingExpiration = true; -}) -.AddGoogle(options => -{ - var googleAuth = builder.Configuration.GetSection("Authentication:Google"); - options.ClientId = googleAuth["ClientId"] ?? ""; - options.ClientSecret = googleAuth["ClientSecret"] ?? ""; -}) -.AddMicrosoftAccount(options => -{ - var msAuth = builder.Configuration.GetSection("Authentication:Microsoft"); - options.ClientId = msAuth["ClientId"] ?? ""; - options.ClientSecret = msAuth["ClientSecret"] ?? ""; - options.CallbackPath = "/signin-microsoft"; - - // Força seleção de conta a cada login - options.AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; - options.TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; - - // 🔥 SOLUÇÃO RADICAL: SEMPRE FORÇAR HTTPS - options.Events = new OAuthEvents - { - OnRedirectToAuthorizationEndpoint = context => - { - var logger = context.HttpContext.RequestServices.GetRequiredService>(); - - // 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; - } - }; -}); +StripeConfiguration.ApiKey = builder.Configuration["Stripe:SecretKey"]; // Localization -builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); - +builder.Services.AddLocalization(options => options.ResourcesPath = ""); +//builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); builder.Services.Configure(options => { var supportedCultures = new[] { 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.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 -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddSingleton(); -//builder.Services.AddScoped(); - -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); -builder.Services.AddScoped(); - -// Image Storage Service -builder.Services.AddScoped(); - -// Configure upload limits for file uploads -builder.Services.Configure(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(); -builder.Services.AddScoped(); - -// Add HttpClient for OpenGraphService -builder.Services.AddHttpClient(); - -// Add SendGrid -builder.Services.AddSingleton(provider => -{ - var apiKey = builder.Configuration["SendGrid:ApiKey"]; - return new SendGridClient(apiKey); -}); +// Custom Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // Background Services -builder.Services.AddHostedService(); +builder.Services.AddHostedService(); -// Response Caching -builder.Services.AddResponseCaching(); -builder.Services.AddMemoryCache(); +// Monitoring Services +if (builder.Configuration.GetValue("ResourceMonitoring:Enabled", true)) +{ + builder.Services.AddHostedService(); +} -builder.Services.AddRazorPages(); +if (builder.Configuration.GetValue("MongoDbMonitoring:Enabled", true)) +{ + builder.Services.AddHostedService(); +} + +// 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(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddHealthChecks() + .AddCheck("mongodb") + .AddCheck("seq") + .AddCheck("resources") + .AddCheck("external_services"); + +builder.Services.Configure(options => +{ + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.KnownProxies.Clear(); + options.KnownNetworks.Clear(); +}); var app = builder.Build(); -// 🔥 PRIMEIRA COISA APÓS BUILD - FORWARDED HEADERS + BASE URL OVERRIDE app.UseForwardedHeaders(); -// 🚨 FORÇA BASE URL HTTPS EM PRODUÇÃO -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. +// Configure the HTTP request pipeline if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); @@ -239,128 +240,65 @@ if (!app.Environment.IsDevelopment()) } app.UseHttpsRedirection(); + app.UseStaticFiles(); +// Language redirection middleware (before routing) +app.UseMiddleware(); + app.UseRouting(); +// Localization middleware (after routing so route data is available) app.UseRequestLocalization(); +app.UseCors("AllowSpecificOrigins"); + app.UseAuthentication(); app.UseAuthorization(); -// Add custom middleware -app.UseMiddleware(); -app.UseMiddleware(); -app.UseMiddleware(); -app.UseMiddleware(); +app.UseSession(); -// 🔥 DEBUG MIDDLEWARE MELHORADO -app.Use(async (context, next) => -{ - // 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}"); +// Custom middleware +app.UseMiddleware(); - // Debug específico para Microsoft signin - if (context.Request.Path.StartsWithSegments("/signin-microsoft")) - { - var logger = context.RequestServices.GetRequiredService>(); - 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}"); - } - } +// Health check endpoint +app.MapHealthChecks("/health"); - 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( -// name: "userpage", -// pattern: "page/{category}/{slug}", -// defaults: new { controller = "UserPage", action = "Display" }, -// constraints: new { category = @"^[a-zA-Z-]+$", slug = @"^[a-z0-9-]+$" }); +// name: "auth", +// pattern: "signin-{provider}", +// defaults: new { controller = "Account", action = "ExternalLoginCallback" }); -// 🔥 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( - name: "livepage", - pattern: "page/{category}/{slug}", - defaults: new { controller = "LivePage", action = "Display" }, - constraints: new - { - category = @"^[a-zA-Z0-9\-\u00C0-\u017F]+$", // ← Aceita acentos - slug = @"^[a-z0-9-]+$" - }); + name: "localized", + pattern: "{culture:regex(^(pt-BR|es-PY|es)$)}/{controller=Home}/{action=Index}/{id?}"); -// 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( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); -// Initialize default data -using (var scope = app.Services.CreateScope()) +try { - var themeService = scope.ServiceProvider.GetRequiredService(); - var categoryService = scope.ServiceProvider.GetRequiredService(); - - try - { - // Initialize themes - var existingThemes = await themeService.GetAvailableThemesAsync(); - if (!existingThemes.Any()) - { - await themeService.InitializeDefaultThemesAsync(); - } - - // Initialize categories - var existingCategories = await categoryService.GetAllCategoriesAsync(); - if (!existingCategories.Any()) - { - await categoryService.InitializeDefaultCategoriesAsync(); - } - } - catch (Exception ex) - { - var logger = scope.ServiceProvider.GetRequiredService>(); - logger.LogError(ex, "Error initializing default data"); - } + Log.Information("Starting QRRapido application"); + app.Run(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "QRRapido application terminated unexpectedly"); +} +finally +{ + Log.CloseAndFlush(); } - -app.Run(); - -// Make Program accessible for integration tests -public partial class Program { } \ No newline at end of file