using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authentication.Google; using Microsoft.AspNetCore.Authentication.MicrosoftAccount; using Microsoft.AspNetCore.Localization; using Microsoft.AspNetCore.StaticFiles; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Localization; using MongoDB.Driver; using QRRapidoApp.Configuration; using QRRapidoApp.Data; using QRRapidoApp.Middleware; using QRRapidoApp.Providers; using QRRapidoApp.Services; using QRRapidoApp.Models.Ads; using QRRapidoApp.Services.Ads; using QRRapidoApp.Services.Monitoring; using QRRapidoApp.Services.HealthChecks; using StackExchange.Redis; using Stripe; using System.Globalization; using Serilog; using Serilog.Events; using Serilog.Sinks.OpenSearch; using Serilog.Sinks.SystemConsole.Themes; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.RateLimiting; using System.Threading.RateLimiting; using Microsoft.AspNetCore.Server.Kestrel.Core; using AspNetCore.DataProtection.MongoDb; // Disable StaticWebAssets for WSL compatibility (keeping this as it might still be needed for your mixed env) Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUP__STATICWEBASSETS__ENABLED", "false"); var builder = WebApplication.CreateBuilder(args); // Configure Serilog var loggerConfig = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .Enrich.FromLogContext() .Enrich.WithEnvironmentName() .Enrich.WithProcessId() .Enrich.WithThreadId() .Enrich.WithProperty("ApplicationName", builder.Configuration["ApplicationName"] ?? "QRRapido") .Enrich.WithProperty("Environment", builder.Environment.EnvironmentName) .WriteTo.Async(a => a.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} {Properties:j}{NewLine}{Exception}", theme: AnsiConsoleTheme.Code)); var openSearchUrl = builder.Configuration["Serilog:OpenSearchUrl"]; if (!string.IsNullOrEmpty(openSearchUrl)) { var environment = builder.Environment.EnvironmentName.ToLower(); var envMapping = environment switch { "Production" => "prod", "Staging" => "staging", "Development" => "dev", _ => environment }; var indexFormat = $"qrrapido-logs-{envMapping}-{{0:yyyy-MM-dd}}"; try { loggerConfig.WriteTo.Async(a => a.OpenSearch(new OpenSearchSinkOptions(new Uri(openSearchUrl)) { IndexFormat = indexFormat, AutoRegisterTemplate = true, 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 ex) { // Fails silently, logs will continue on console. loggerConfig.WriteTo.Console(outputTemplate: $"Error setting up OpenSearch sink: {ex.Message}"); } } Log.Logger = loggerConfig.CreateLogger(); builder.Host.UseSerilog(); // Add services to the container builder.Services.AddControllersWithViews() .AddViewLocalization(Microsoft.AspNetCore.Mvc.Razor.LanguageViewLocationExpanderFormat.Suffix) .AddDataAnnotationsLocalization(); builder.Services.AddDistributedMemoryCache(); // Armazena os dados da sess�o na mem�ria builder.Services.AddSession(options => { options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; options.IdleTimeout = TimeSpan.FromMinutes(30); // Tempo de expira��o }); // Add HttpClient for health checks builder.Services.AddHttpClient(); // MongoDB Configuration - optional for development var mongoConnectionString = builder.Configuration.GetConnectionString("MongoDB"); if (!string.IsNullOrEmpty(mongoConnectionString)) { 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(); } builder.Services.AddMemoryCache(); builder.Services.AddSingleton(); builder.Services.Configure(builder.Configuration.GetSection("Ads")); builder.Services.AddScoped(); // ✅ DataProtection compartilhado via MongoDB (para múltiplas réplicas do Swarm) if (!string.IsNullOrEmpty(mongoConnectionString)) { Log.Information("Configuring DataProtection to persist keys in MongoDB for Swarm compatibility"); builder.Services.AddDataProtection() .SetApplicationName("QRRapido") .PersistKeysToMongoDb( () => new MongoClient(mongoConnectionString).GetDatabase("QRRapidoDB"), "DataProtectionKeys"); } else { // Fallback para FileSystem em desenvolvimento Log.Warning("MongoDB not available - using FileSystem for DataProtection (development only)"); var keysDirectory = Path.Combine(Directory.GetCurrentDirectory(), "keys"); Directory.CreateDirectory(keysDirectory); builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(keysDirectory)) .SetApplicationName("QRRapido"); } // 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 StripeConfiguration.ApiKey = builder.Configuration["Stripe:SecretKey"]; // Localization 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-PY"), }; 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()); }); // Custom Services builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Background Services builder.Services.AddHostedService(); // Monitoring Services if (builder.Configuration.GetValue("ResourceMonitoring:Enabled", true)) { builder.Services.AddHostedService(); } //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("resources") .AddCheck("external_services"); builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; options.KnownProxies.Clear(); options.KnownNetworks.Clear(); }); builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = 429; options.AddFixedWindowLimiter("api", options => { options.PermitLimit = 600; options.Window = TimeSpan.FromMinutes(1); options.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; options.QueueLimit = 10; }); }); builder.Services.Configure(options => { options.Limits.MaxConcurrentConnections = 2000; options.Limits.RequestHeadersTimeout = TimeSpan.FromSeconds(30); }); var app = builder.Build(); app.UseRateLimiter(); app.UseForwardedHeaders(); // Configure the HTTP request pipeline if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); app.UseHsts(); } 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(); app.UseSession(); // Custom middleware app.UseMiddleware(); // Health check endpoint app.MapHealthChecks("/healthcheck"); //app.MapControllerRoute( // name: "auth", // pattern: "signin-{provider}", // defaults: new { controller = "Account", action = "ExternalLoginCallback" }); //app.MapControllerRoute( // name: "account", // pattern: "Account/{action}", // defaults: new { controller = "Account" }); // Language routes (must be first) app.MapControllerRoute( name: "localized", pattern: "{culture:regex(^(pt-BR|es-PY)$)}/{controller=Home}/{action=Index}/{id?}"); // 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?}"); try { Log.Information("Starting QRRapido application"); app.Run(); } catch (Exception ex) { Log.Fatal(ex, "QRRapido application terminated unexpectedly"); } finally { Log.CloseAndFlush(); }