using BCards.Web.Configuration; using BCards.Web.Services; using BCards.Web.Repositories; using BCards.Web.HealthChecks; using AspNetCore.DataProtection.MongoDb; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Localization; using Microsoft.Extensions.Options; using MongoDB.Driver; using System.Globalization; using Microsoft.AspNetCore.Authentication.OAuth; using SendGrid; using BCards.Web.Middleware; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpOverrides; using Serilog; using Serilog.Events; using Microsoft.Extensions.Diagnostics.HealthChecks; using Serilog.Sinks.OpenSearch; #if TESTING using BCards.Web.TestSupport; #endif using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.DataProtection; var builder = WebApplication.CreateBuilder(args); var isDevelopment = builder.Environment.IsDevelopment(); var hostname = Environment.MachineName; Serilog.Debugging.SelfLog.Enable(msg => { Console.WriteLine($"[SERILOG SELF] {DateTime.Now:HH:mm:ss} {msg}"); System.Diagnostics.Debug.WriteLine($"[SERILOG SELF] {msg}"); }); var loggerConfig = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .Enrich.FromLogContext() .Enrich.WithEnvironmentName() .Enrich.WithProcessId() .Enrich.WithThreadId() .Enrich.WithProperty("ApplicationName", builder.Configuration["ApplicationName"] ?? "BCards") .Enrich.WithProperty("Environment", builder.Environment.EnvironmentName) .Enrich.WithProperty("Hostname", hostname); if (isDevelopment) { loggerConfig .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft.AspNetCore.Hosting", LogEventLevel.Information) .MinimumLevel.Override("Microsoft.AspNetCore.Routing", LogEventLevel.Information) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("System", LogEventLevel.Warning) // Console sempre ativo - logs garantidos .WriteTo.Console( restrictedToMinimumLevel: LogEventLevel.Debug, outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") // Arquivo local com rotação para Docker logs não ficarem enormes .WriteTo.File( "./logs/bcards-dev-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 2, // Só 2 dias como você quer restrictedToMinimumLevel: LogEventLevel.Debug, outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}"); var openSearchUrl = builder.Configuration["Serilog:OpenSearchUrl"]; if (!string.IsNullOrEmpty(openSearchUrl)) { var indexFormat = "b-cards-dev-{0:yyyy-MM}"; try { // OpenSearch configurado para ser MUITO agressivo no envio loggerConfig.WriteTo.Async(a => a.OpenSearch(new OpenSearchSinkOptions(new Uri(openSearchUrl)) { IndexFormat = indexFormat, AutoRegisterTemplate = true, BufferBaseFilename = "./logs/opensearch-buffer", // Buffer em disco ModifyConnectionSettings = conn => conn .RequestTimeout(TimeSpan.FromSeconds(8)) .PingTimeout(TimeSpan.FromSeconds(4)), MinimumLogEventLevel = LogEventLevel.Debug, EmitEventFailure = EmitEventFailureHandling.WriteToSelfLog, RegisterTemplateFailure = RegisterTemplateRecovery.IndexAnyway, BatchPostingLimit = 10, // Lotes pequenos = envio mais frequente Period = TimeSpan.FromSeconds(2), // Envia a cada 2 segundos // Configurações para máxima persistência BufferRetainedInvalidPayloadsLimitBytes = 100 * 1024 * 1024, // 100MB buffer BufferLogShippingInterval = TimeSpan.FromSeconds(1), // Tenta reenviar rapidamente TemplateCustomSettings = new Dictionary { {"number_of_shards", "1"}, {"number_of_replicas", "0"} } }), bufferSize: 10000, // Buffer grande na memória blockWhenFull: false); // Nunca bloquear aplicação } catch (Exception) { // Falha silenciosa - logs continuam no console e arquivo } } } else { // PRODUÇÃO permanece igual, mas também com fallback garantido loggerConfig .MinimumLevel.Information() .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.StaticFiles", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("System", LogEventLevel.Warning) .WriteTo.Console( restrictedToMinimumLevel: LogEventLevel.Information, outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") .WriteTo.File( "/app/logs/bcards-.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 7, restrictedToMinimumLevel: LogEventLevel.Warning, outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} {Level:u3}] [{Hostname}] {Message:lj} {Properties:j}{NewLine}{Exception}"); var openSearchUrl = builder.Configuration["Serilog:OpenSearchUrl"]; if (!string.IsNullOrEmpty(openSearchUrl)) { var environment = builder.Environment.EnvironmentName.ToLower(); var envMapping = environment switch { "production" => "prod", "staging" => "release", "development" => "dev", _ => environment }; var indexFormat = $"b-cards-{envMapping}-{{0:yyyy-MM}}"; try { loggerConfig.WriteTo.Async(a => a.OpenSearch(new OpenSearchSinkOptions(new Uri(openSearchUrl)) { IndexFormat = indexFormat, AutoRegisterTemplate = false, BufferBaseFilename = "./logs/buffer", ModifyConnectionSettings = conn => conn .RequestTimeout(TimeSpan.FromSeconds(30)) .PingTimeout(TimeSpan.FromSeconds(10)), MinimumLogEventLevel = LogEventLevel.Information, EmitEventFailure = EmitEventFailureHandling.WriteToSelfLog, RegisterTemplateFailure = RegisterTemplateRecovery.IndexAnyway, BatchPostingLimit = 50, Period = TimeSpan.FromSeconds(5), }), bufferSize: 10000, blockWhenFull: false); } catch (Exception) { // Falha silenciosa em produção - logs continuam no console/arquivo } } } var logger = loggerConfig.CreateLogger(); Log.Logger = logger; Console.WriteLine($"[STARTUP] {DateTime.Now:HH:mm:ss} - Logger configurado"); Log.Information("=== APLICAÇÃO INICIANDO ==="); Log.Information("BCards iniciando em {Environment} no host {Hostname}", builder.Environment.EnvironmentName, hostname); // Use Serilog for the host builder.Host.UseSerilog(); // Log startup information Log.Information("Starting BCards application on {Hostname} in {Environment} mode", hostname, builder.Environment.EnvironmentName); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; options.RequireHeaderSymmetry = false; options.KnownNetworks.Clear(); options.KnownProxies.Clear(); options.ForwardLimit = null; }); builder.Services.AddResponseCompression(options => { options.EnableForHttps = true; options.Providers.Add(); options.Providers.Add(); options.MimeTypes = Microsoft.AspNetCore.ResponseCompression.ResponseCompressionDefaults.MimeTypes.Concat( new[] { "application/javascript", "application/json", "application/xml", "text/css", "text/plain", "image/svg+xml" }); }); builder.Services.Configure(options => { options.Level = System.IO.Compression.CompressionLevel.Optimal; }); builder.Services.Configure(options => { options.Level = System.IO.Compression.CompressionLevel.Optimal; }); // Add services to the container. builder.Services.AddControllersWithViews() .AddRazorRuntimeCompilation() .AddViewLocalization() .AddDataAnnotationsLocalization(); // MongoDB Configuration builder.Services.Configure( builder.Configuration.GetSection("MongoDb")); builder.Services.AddSingleton(serviceProvider => { var settings = serviceProvider.GetRequiredService>().Value; var connectionString = settings.ConnectionString; if (!connectionString.Contains("maxPoolSize")) { var separator = connectionString.Contains("?") ? "&" : "?"; connectionString += $"{separator}maxPoolSize=200&minPoolSize=20&maxIdleTimeMS=300000&socketTimeoutMS=60000&connectTimeoutMS=30000"; } Log.Information("Connecting to MongoDB with optimized pool settings."); return new MongoClient(connectionString); }); builder.Services.AddScoped(serviceProvider => { var client = serviceProvider.GetRequiredService(); var settings = serviceProvider.GetRequiredService>().Value; return client.GetDatabase(settings.DatabaseName); }); var dataProtectionSection = builder.Configuration.GetSection("DataProtection:Mongo"); var dataProtectionConnectionString = dataProtectionSection.GetValue("ConnectionString") ?? builder.Configuration.GetSection("MongoDb").GetValue("ConnectionString"); var dataProtectionDatabase = dataProtectionSection.GetValue("DatabaseName") ?? builder.Configuration.GetSection("MongoDb").GetValue("DatabaseName") ?? "BCardsDB"; var dataProtectionCollection = dataProtectionSection.GetValue("CollectionName") ?? "DataProtectionKeys"; if (!string.IsNullOrWhiteSpace(dataProtectionConnectionString)) { Log.Information("Configuring DataProtection to persist keys in MongoDB database {Database} / collection {Collection}", dataProtectionDatabase, dataProtectionCollection); builder.Services.AddDataProtection() .SetApplicationName("BCards") .PersistKeysToMongoDb( () => new MongoClient(dataProtectionConnectionString).GetDatabase(dataProtectionDatabase), dataProtectionCollection); } else { Log.Warning("DataProtection MongoDB configuration missing; encryption keys will be ephemeral per container."); } // Stripe Configuration with validation builder.Services.Configure( builder.Configuration.GetSection("Stripe")); var stripeSettings = builder.Configuration.GetSection("Stripe").Get(); if (stripeSettings == null || string.IsNullOrEmpty(stripeSettings.SecretKey)) { Log.Fatal("❌ STRIPE CONFIGURATION MISSING! Check your appsettings.json or environment variables."); throw new InvalidOperationException("Stripe configuration is required"); } Log.Information("🔧 Stripe Environment: {Environment} | Test Mode: {IsTestMode}", stripeSettings.Environment.ToUpper(), stripeSettings.IsTestMode); if (stripeSettings.IsTestMode) { Log.Warning("⚠️ STRIPE TEST MODE ENABLED - Only test payments will work"); } else { Log.Information("💰 STRIPE LIVE MODE ENABLED - Real payments active"); } // OAuth Configuration builder.Services.Configure( builder.Configuration.GetSection("Authentication:Google")); builder.Services.Configure( builder.Configuration.GetSection("Authentication:Microsoft")); builder.Services.Configure( builder.Configuration.GetSection("Moderation")); // Authentication var authBuilder = builder.Services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; // DefaultChallengeScheme will be set conditionally below }); authBuilder.AddCookie(options => { options.LoginPath = "/Auth/Login"; options.LogoutPath = "/Auth/Logout"; options.ExpireTimeSpan = TimeSpan.FromDays(7); // 7 dias em vez de 8 horas options.SlidingExpiration = true; options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; options.Cookie.SameSite = SameSiteMode.None; // Para Cloudflare options.Cookie.SecurePolicy = CookieSecurePolicy.Always; }); // Always register Google and Microsoft authentication schemes authBuilder.AddGoogle(options => { var googleAuth = builder.Configuration.GetSection("Authentication:Google"); options.ClientId = googleAuth["ClientId"] ?? ""; options.ClientSecret = googleAuth["ClientSecret"] ?? ""; options.CallbackPath = "/signin-google"; options.BackchannelTimeout = TimeSpan.FromSeconds(30); options.SaveTokens = true; options.UsePkce = true; options.Events = new OAuthEvents { OnRedirectToAuthorizationEndpoint = context => { // Fix para Cloudflare - remover porta 443 explícita if (!builder.Environment.IsDevelopment()) { context.RedirectUri = context.RedirectUri.Replace(":443", ""); } context.Response.Redirect(context.RedirectUri); return Task.CompletedTask; }, OnRemoteFailure = context => { var logger = context.HttpContext.RequestServices.GetRequiredService>(); logger.LogError("🔴 Google OAuth falhou: {Failure}", context.Failure?.Message); logger.LogError("🔴 User Agent: {UserAgent}", context.Request.Headers.UserAgent.ToString()); context.Response.Redirect("/Auth/Login?error=google_oauth_failed"); context.HandleResponse(); return Task.CompletedTask; }, OnTicketReceived = context => { var logger = context.HttpContext.RequestServices.GetRequiredService>(); logger.LogInformation("✅ Google OAuth ticket recebido com sucesso"); return Task.CompletedTask; } }; }) .AddMicrosoftAccount(options => { var msAuth = builder.Configuration.GetSection("Authentication:Microsoft"); options.ClientId = msAuth["ClientId"] ?? ""; options.ClientSecret = msAuth["ClientSecret"] ?? ""; options.CallbackPath = "/signin-microsoft"; options.BackchannelTimeout = TimeSpan.FromSeconds(30); options.AuthorizationEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; options.TokenEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; options.Events = new OAuthEvents { OnRedirectToAuthorizationEndpoint = context => { var logger = context.HttpContext.RequestServices.GetRequiredService>(); 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"]}"); var originalUri = context.RedirectUri; // Fix para Cloudflare - remover porta 443 explícita (mesmo fix do Google) if (!builder.Environment.IsDevelopment()) { context.RedirectUri = originalUri.Replace(":443", ""); logger.LogWarning($"REMOVED :443 - Modified RedirectUri: {context.RedirectUri}"); } if (originalUri.Contains("bcards.site")) { context.RedirectUri = context.RedirectUri .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}"); } var redirectUri = context.RedirectUri; if (!redirectUri.Contains("prompt=")) { redirectUri += "&prompt=login"; } if (!redirectUri.Contains("options=")) { redirectUri += "&options=disable_passkey"; logger.LogWarning($"ADDED options=disable_passkey - Modified RedirectUri: {redirectUri}"); } logger.LogWarning($"Final RedirectUri: {redirectUri}"); context.Response.Redirect(redirectUri); return Task.CompletedTask; }, OnRemoteFailure = context => { var logger = context.HttpContext.RequestServices.GetRequiredService>(); logger.LogWarning("Microsoft remote failure: {Failure}", context.Failure?.Message); context.Response.Redirect("/Auth/Login?error=remote_failure"); context.HandleResponse(); return Task.CompletedTask; } }; }); #if TESTING // 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; }); } #else authBuilder.Services.Configure(options => { options.DefaultChallengeScheme = GoogleDefaults.AuthenticationScheme; }); #endif // Localization builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); builder.Services.Configure(options => { var supportedCultures = new[] { new CultureInfo("pt-BR"), new CultureInfo("es-ES") }; options.DefaultRequestCulture = new RequestCulture("pt-BR"); options.SupportedCultures = supportedCultures; options.SupportedUICultures = supportedCultures; }); // 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.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(); // Support Area - Rating and Contact System builder.Services.Configure( builder.Configuration.GetSection("Support")); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Markdown/Articles System - Tutoriais e Artigos Areas builder.Services.AddScoped(); // Configure upload limits for file handling (images up to 5MB) builder.Services.Configure(options => { var maxUploadSize = 12 * 1024 * 1024; // 12MB options.MultipartBodyLengthLimit = maxUploadSize; options.ValueLengthLimit = int.MaxValue; options.ValueCountLimit = int.MaxValue; options.KeyLengthLimit = int.MaxValue; options.BufferBody = true; options.BufferBodyLengthLimit = maxUploadSize; options.MultipartBodyLengthLimit = maxUploadSize; options.MultipartHeadersLengthLimit = 16384; }); // Configure Kestrel server limits for larger requests builder.Services.Configure(options => { options.Limits.MaxRequestBodySize = 12 * 1024 * 1024; // 12MB options.Limits.RequestHeadersTimeout = TimeSpan.FromMinutes(2); options.Limits.KeepAliveTimeout = TimeSpan.FromMinutes(2); }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpClient(); builder.Services.AddSingleton(provider => { var apiKey = builder.Configuration["SendGrid:ApiKey"]; return new SendGridClient(apiKey); }); builder.Services.AddHostedService(); builder.Services.AddResponseCaching(); builder.Services.AddMemoryCache(); builder.Services.AddRazorPages(); builder.Services.AddScoped(); builder.Services.AddHttpClient(client => { client.Timeout = TimeSpan.FromSeconds(5); client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"); }); builder.Services.AddHealthChecks() .AddCheck( name: "critical_services", failureStatus: HealthStatus.Unhealthy, timeout: TimeSpan.FromSeconds(30)) .AddCheck( name: "oauth_providers", failureStatus: HealthStatus.Degraded, timeout: TimeSpan.FromSeconds(10)) .AddCheck( name: "sendgrid", failureStatus: HealthStatus.Degraded, timeout: TimeSpan.FromSeconds(10)) .AddCheck( name: "resources", failureStatus: HealthStatus.Degraded, timeout: TimeSpan.FromSeconds(5)); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddHttpClient(client => { client.Timeout = TimeSpan.FromSeconds(15); client.DefaultRequestHeaders.Add("User-Agent", "BCards-CriticalCheck/1.0"); }); var app = builder.Build(); app.UseForwardedHeaders(); if (!app.Environment.IsDevelopment()) { app.Use(async (context, next) => { context.Request.Scheme = "https"; if (context.Request.Host.Host == "bcards.site") { // Fix para Cloudflare - não especificar porta explícita context.Request.Host = new HostString("bcards.site"); } await next(); }); } app.Use(async (context, next) => { // Security headers context.Response.Headers.Append("X-Content-Type-Options", "nosniff"); context.Response.Headers.Append("X-Frame-Options", "DENY"); context.Response.Headers.Append("Referrer-Policy", "no-referrer"); context.Response.Headers.Append("Permissions-Policy", "camera=(), microphone=(), geolocation=()"); // Load balancer e debugging headers context.Response.Headers.Append("X-Server-ID", Environment.MachineName); context.Response.Headers.Append("X-Instance-ID", $"{Environment.MachineName}-{Environment.ProcessId}"); // Cloudflare information headers (quando disponível) var cfCountry = context.Request.Headers["CF-IPCountry"].FirstOrDefault(); var cfRay = context.Request.Headers["CF-RAY"].FirstOrDefault(); if (!string.IsNullOrEmpty(cfCountry)) context.Response.Headers.Append("X-Country", cfCountry); if (!string.IsNullOrEmpty(cfRay)) context.Response.Headers.Append("X-CF-Ray", cfRay); context.Response.OnStarting(() => { context.Response.Headers.Remove("Server"); context.Response.Headers.Remove("X-Powered-By"); return Task.CompletedTask; }); await next(); }); if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } app.UseHttpsRedirection(); app.UseResponseCompression(); app.UseStaticFiles(new StaticFileOptions { OnPrepareResponse = ctx => { var fileName = ctx.File.Name; var extension = Path.GetExtension(fileName).ToLowerInvariant(); TimeSpan maxAge; string cacheControl; if (extension == ".css" || extension == ".js") { maxAge = TimeSpan.FromDays(30); cacheControl = $"public, max-age={maxAge.TotalSeconds}, immutable"; } else if (extension == ".woff" || extension == ".woff2" || extension == ".ttf" || extension == ".eot" || extension == ".svg" || extension == ".otf") { maxAge = TimeSpan.FromDays(365); cacheControl = $"public, max-age={maxAge.TotalSeconds}, immutable"; } else if (extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".gif" || extension == ".ico" || extension == ".webp") { maxAge = TimeSpan.FromDays(180); cacheControl = $"public, max-age={maxAge.TotalSeconds}"; } else { maxAge = TimeSpan.FromDays(1); cacheControl = $"public, max-age={maxAge.TotalSeconds}"; } ctx.Context.Response.Headers.CacheControl = cacheControl; ctx.Context.Response.Headers.Append("Vary", "Accept-Encoding"); } }); app.UseRouting(); app.UseRequestLocalization(); app.UseAuthentication(); app.UseAuthorization(); // Cache middleware para páginas dinâmicas app.Use(async (context, next) => { // Páginas user dinâmicas - sem cache if (context.Request.Path.StartsWithSegments("/page/") || context.Request.Path.StartsWithSegments("/Admin") || context.Request.Path.StartsWithSegments("/Auth") || context.Request.Path.StartsWithSegments("/Payment")) { context.Response.Headers.Append("Cache-Control", "private, no-store, must-revalidate"); context.Response.Headers.Append("Pragma", "no-cache"); context.Response.Headers.Append("Expires", "0"); } // API endpoints - sem cache else if (context.Request.Path.StartsWithSegments("/api/") || context.Request.Path.StartsWithSegments("/click/") || context.Request.Path.StartsWithSegments("/health")) { context.Response.Headers.Append("Cache-Control", "no-cache, no-store, must-revalidate"); context.Response.Headers.Append("Pragma", "no-cache"); } // Páginas públicas - cache moderado else if (context.Request.Path.StartsWithSegments("/Home") || context.Request.Path.Value == "/") { context.Response.Headers.Append("Cache-Control", "public, max-age=300"); // 5 min } await next(); }); app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); app.UseMiddleware(); if (app.Environment.IsDevelopment()) { app.Use(async (context, next) => { var logger = context.RequestServices.GetRequiredService>(); logger.LogDebug("Request - Path: {Path}, Query: {Query}, Method: {Method}, Scheme: {Scheme}, IsHttps: {IsHttps}, Host: {Host}", context.Request.Path, context.Request.QueryString, context.Request.Method, context.Request.Scheme, context.Request.IsHttps, context.Request.Host); if (context.Request.Path.StartsWithSegments("/signin-microsoft")) { logger.LogWarning("SIGNIN-MICROSOFT CALLBACK - Path: {Path}, Query: {Query}, Scheme: {Scheme}, IsHttps: {IsHttps}, Host: {Host}, X-Forwarded-Proto: {ForwardedProto}", context.Request.Path, context.Request.QueryString, context.Request.Scheme, context.Request.IsHttps, context.Request.Host, context.Request.Headers["X-Forwarded-Proto"]); } await next(); }); } app.UseResponseCaching(); // Support Area Routes app.MapAreaControllerRoute( name: "support-area", areaName: "Support", pattern: "Support/{controller=Support}/{action=Index}/{id?}"); 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" }); app.MapControllerRoute( name: "livepage", pattern: "page/{category}/{slug}", defaults: new { controller = "LivePage", action = "Display" }, constraints: new { category = @"^[a-zA-Z0-9\-\u00C0-\u017F]+$", slug = @"^[a-z0-9-]+$" }); app.MapControllerRoute( name: "privacy-pt", pattern: "privacidade", defaults: new { controller = "Legal", action = "Privacy" }); app.MapControllerRoute( name: "terms-pt", pattern: "termos", defaults: new { controller = "Legal", action = "Terms" }); app.MapControllerRoute( name: "guidelines-pt", pattern: "regras", defaults: new { controller = "Legal", action = "CommunityGuidelines" }); app.MapControllerRoute( name: "privacy-es", pattern: "privacy", defaults: new { controller = "Legal", action = "PrivacyES" }); app.MapControllerRoute( name: "terms-es", pattern: "terminos", defaults: new { controller = "Legal", action = "TermsES" }); // ======================================== // AREAS: Tutoriais e Artigos // ======================================== // IMPORTANTE: Ordem importa! Rotas mais específicas primeiro. // Artigos Area - Índice (ANTES para evitar conflito) app.MapAreaControllerRoute( name: "artigos-index", areaName: "Artigos", pattern: "artigos", defaults: new { controller = "Artigos", action = "Index" }); // Artigos Area - Artigo específico app.MapAreaControllerRoute( name: "artigos-article", areaName: "Artigos", pattern: "artigos/{slug}", defaults: new { controller = "Artigos", action = "Article" }, constraints: new { slug = @"^[a-z0-9\-]+$" }); // Tutoriais Area - Índice app.MapAreaControllerRoute( name: "tutoriais-index", areaName: "Tutoriais", pattern: "tutoriais", defaults: new { controller = "Tutoriais", action = "Index" }); // Tutoriais Area - Lista de artigos por categoria app.MapAreaControllerRoute( name: "tutoriais-category", areaName: "Tutoriais", pattern: "tutoriais/{categoria}", defaults: new { controller = "Tutoriais", action = "Category" }, constraints: new { categoria = @"^[a-z\-]+$" }); // Tutoriais Area - Artigo específico app.MapAreaControllerRoute( name: "tutoriais-article", areaName: "Tutoriais", pattern: "tutoriais/{categoria}/{slug}", defaults: new { controller = "Tutoriais", action = "Article" }, constraints: new { categoria = @"^[a-z\-]+$", // slug de categoria slug = @"^[a-z0-9\-]+$" // slug do artigo }); // ======================================== // Rota default // ======================================== app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); using (var scope = app.Services.CreateScope()) { var themeService = scope.ServiceProvider.GetRequiredService(); var categoryService = scope.ServiceProvider.GetRequiredService(); try { var existingThemes = await themeService.GetAvailableThemesAsync(); if (!existingThemes.Any()) { await themeService.InitializeDefaultThemesAsync(); } var existingCategories = await categoryService.GetAllCategoriesAsync(); if (!existingCategories.Any()) { await categoryService.InitializeDefaultCategoriesAsync(); } Log.Information("Default themes and categories initialized successfully"); } catch (Exception ex) { Log.Error(ex, "Error initializing default data"); } } try { Log.Information("BCards application started successfully on {Hostname}", hostname); if (isDevelopment) { Console.WriteLine("[DEBUG] Aguardando envio de logs iniciais..."); await Task.Delay(2000); Console.WriteLine("[DEBUG] Iniciando aplicação..."); } app.Run(); } catch (Exception ex) { Log.Fatal(ex, "BCards application terminated unexpectedly on {Hostname}", hostname); throw; } finally { Log.Information("BCards application shutting down on {Hostname}", hostname); await Task.Delay(5000); Log.CloseAndFlush(); } public partial class Program { }