diff --git a/Program.cs b/Program.cs index b110585..33410c9 100644 --- a/Program.cs +++ b/Program.cs @@ -1,238 +1,237 @@ +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.AspNetCore.StaticFiles; -using Microsoft.Extensions.Caching.Distributed; -using Microsoft.Extensions.Localization; +using Microsoft.Extensions.Options; using MongoDB.Driver; -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 System.Globalization; -using Serilog; -using Serilog.Events; -using Serilog.Sinks.SystemConsole.Themes; -using Microsoft.AspNetCore.Mvc.Razor; +using Stripe; +using Microsoft.AspNetCore.Authentication.OAuth; +using SendGrid; +using BCards.Web.Middleware; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.HttpOverrides; var builder = WebApplication.CreateBuilder(args); -// 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(); - -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 => +// 🔥 CONFIGURAR FORWARDED HEADERS NO BUILDER +builder.Services.Configure(options => { - options.Cookie.HttpOnly = true; - options.Cookie.IsEssential = true; - options.IdleTimeout = TimeSpan.FromMinutes(30); // Tempo de expira��o + options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; + options.RequireHeaderSymmetry = false; + options.KnownNetworks.Clear(); + options.KnownProxies.Clear(); + // 🚨 PERMITIR QUALQUER PROXY (NGINX) + options.ForwardLimit = null; }); -// Add HttpClient for health checks -builder.Services.AddHttpClient(); +// Add services to the container. +builder.Services.AddControllersWithViews() + .AddRazorRuntimeCompilation() + .AddViewLocalization() + .AddDataAnnotationsLocalization(); -// MongoDB Configuration - optional for development -var mongoConnectionString = builder.Configuration.GetConnectionString("MongoDB"); -if (!string.IsNullOrEmpty(mongoConnectionString)) +// MongoDB Configuration +builder.Services.Configure( + builder.Configuration.GetSection("MongoDb")); + +builder.Services.AddSingleton(serviceProvider => { - try - { - builder.Services.AddSingleton(serviceProvider => - { - return new MongoClient(mongoConnectionString); - }); - builder.Services.AddScoped(); - } - catch - { - // MongoDB not available - services will handle gracefully - builder.Services.AddScoped(); - } -} -else + var settings = serviceProvider.GetRequiredService>().Value; + return new MongoClient(settings.ConnectionString); +}); + +builder.Services.AddScoped(serviceProvider => { - // 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; - }; - }); + var client = serviceProvider.GetRequiredService(); + var settings = serviceProvider.GetRequiredService>().Value; + return client.GetDatabase(settings.DatabaseName); +}); // Stripe Configuration -StripeConfiguration.ApiKey = builder.Configuration["Stripe:SecretKey"]; +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; + } + }; +}); // Localization -builder.Services.AddLocalization(options => options.ResourcesPath = ""); -//builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); +builder.Services.AddLocalization(options => options.ResourcesPath = "Resources"); + builder.Services.Configure(options => { var supportedCultures = new[] { new CultureInfo("pt-BR"), - new CultureInfo("es-PY"), - new CultureInfo("es") + new CultureInfo("es-ES") }; - options.DefaultRequestCulture = new RequestCulture("pt-BR", "pt-BR"); + options.DefaultRequestCulture = new RequestCulture("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(); +// 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); +}); // Background Services -builder.Services.AddHostedService(); +builder.Services.AddHostedService(); -// Monitoring Services -if (builder.Configuration.GetValue("ResourceMonitoring:Enabled", true)) -{ - builder.Services.AddHostedService(); -} +// Response Caching +builder.Services.AddResponseCaching(); +builder.Services.AddMemoryCache(); -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(); -}); +builder.Services.AddRazorPages(); var app = builder.Build(); +// 🔥 PRIMEIRA COISA APÓS BUILD - FORWARDED HEADERS + BASE URL OVERRIDE app.UseForwardedHeaders(); -// Configure the HTTP request pipeline +// 🚨 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. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); @@ -240,65 +239,128 @@ 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(); -app.UseSession(); +// Add custom middleware +app.UseMiddleware(); +app.UseMiddleware(); +app.UseMiddleware(); +app.UseMiddleware(); -// Custom middleware -app.UseMiddleware(); +// 🔥 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}"); -// Health check endpoint -app.MapHealthChecks("/health"); + // 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}"); + } + } -//app.MapControllerRoute( -// name: "auth", -// pattern: "signin-{provider}", -// defaults: new { controller = "Account", action = "ExternalLoginCallback" }); + await next(); +}); -//app.MapControllerRoute( -// name: "account", -// pattern: "Account/{action}", -// defaults: new { controller = "Account" }); +app.UseResponseCaching(); -// Language routes (must be first) +// Rotas específicas primeiro app.MapControllerRoute( - name: "localized", - pattern: "{culture:regex(^(pt-BR|es-PY|es)$)}/{controller=Home}/{action=Index}/{id?}"); + 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-]+$" }); -// API routes app.MapControllerRoute( - name: "api", - pattern: "api/{controller}/{action=Index}/{id?}"); + name: "userpage-click", + pattern: "page/click/{pageId}", + defaults: new { controller = "UserPage", action = "RecordClick" }); -// Default fallback route (for development/testing without culture) +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-]+$" }); + +// 🔥 NOVA ROTA: LivePageController para páginas otimizadas de SEO +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-]+$" + }); + +// Rota padrão por último app.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); -try +// Initialize default data +using (var scope = app.Services.CreateScope()) { - Log.Information("Starting QRRapido application"); - app.Run(); -} -catch (Exception ex) -{ - Log.Fatal(ex, "QRRapido application terminated unexpectedly"); -} -finally -{ - Log.CloseAndFlush(); + 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"); + } } + +app.Run(); + +// Make Program accessible for integration tests +public partial class Program { } \ No newline at end of file