BCards/src/BCards.Web/Program.cs
Ricardo Carneiro 90cc01d7cf
All checks were successful
BCards Deployment Pipeline / Run Tests (push) Successful in 1s
BCards Deployment Pipeline / PR Validation (push) Has been skipped
BCards Deployment Pipeline / Build and Push Image (push) Successful in 15m39s
BCards Deployment Pipeline / Deploy to Production (ARM - OCI) (push) Successful in 1m17s
BCards Deployment Pipeline / Deploy to Staging (x86 - Local) (push) Has been skipped
BCards Deployment Pipeline / Cleanup Old Resources (push) Has been skipped
BCards Deployment Pipeline / Deployment Summary (push) Successful in 0s
feat: heath checks, seq e logs
2025-08-24 20:00:53 -03:00

492 lines
17 KiB
C#

using BCards.Web.Configuration;
using BCards.Web.Services;
using BCards.Web.Repositories;
using BCards.Web.HealthChecks;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.Google;
using Microsoft.AspNetCore.Authentication.MicrosoftAccount;
using Microsoft.AspNetCore.Localization;
using Microsoft.Extensions.Options;
using MongoDB.Driver;
using System.Globalization;
using Stripe;
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;
var builder = WebApplication.CreateBuilder(args);
// Configure Serilog with environment-specific settings
var isDevelopment = builder.Environment.IsDevelopment();
var hostname = Environment.MachineName;
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)
{
// Development: Log EVERYTHING to console with detailed formatting
loggerConfig
.MinimumLevel.Debug()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("System", LogEventLevel.Information)
.WriteTo.Async(a => a.Console(
outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] [{Hostname}] {Message:lj} {Properties:j}{NewLine}{Exception}"));
// Also send to Seq if configured (for local development with Seq)
var seqUrl = builder.Configuration["Serilog:SeqUrl"];
if (!string.IsNullOrEmpty(seqUrl))
{
var apiKey = builder.Configuration["Serilog:ApiKey"];
loggerConfig.WriteTo.Async(a => a.Seq(seqUrl, apiKey: string.IsNullOrEmpty(apiKey) ? null : apiKey));
}
}
else
{
// Production: Only errors to console, everything to Seq
loggerConfig
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("System", LogEventLevel.Warning)
.WriteTo.Async(a => a.Console(
restrictedToMinimumLevel: LogEventLevel.Error,
outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] [{Hostname}] {Message:lj}{NewLine}{Exception}"));
// Production: Send detailed logs to Seq
var seqUrl = builder.Configuration["Serilog:SeqUrl"];
if (!string.IsNullOrEmpty(seqUrl))
{
var apiKey = builder.Configuration["Serilog:ApiKey"];
loggerConfig.WriteTo.Async(a => a.Seq(seqUrl,
apiKey: string.IsNullOrEmpty(apiKey) ? null : apiKey,
restrictedToMinimumLevel: LogEventLevel.Information));
}
}
Log.Logger = loggerConfig.CreateLogger();
// 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);
// 🔥 CONFIGURAR FORWARDED HEADERS NO BUILDER
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.RequireHeaderSymmetry = false;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
// 🚨 PERMITIR QUALQUER PROXY (NGINX)
options.ForwardLimit = null;
});
// Add services to the container.
builder.Services.AddControllersWithViews()
.AddRazorRuntimeCompilation()
.AddViewLocalization()
.AddDataAnnotationsLocalization();
// MongoDB Configuration
builder.Services.Configure<MongoDbSettings>(
builder.Configuration.GetSection("MongoDb"));
builder.Services.AddSingleton<IMongoClient>(serviceProvider =>
{
var settings = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value;
return new MongoClient(settings.ConnectionString);
});
builder.Services.AddScoped(serviceProvider =>
{
var client = serviceProvider.GetRequiredService<IMongoClient>();
var settings = serviceProvider.GetRequiredService<IOptions<MongoDbSettings>>().Value;
return client.GetDatabase(settings.DatabaseName);
});
// Stripe Configuration
builder.Services.Configure<StripeSettings>(
builder.Configuration.GetSection("Stripe"));
// OAuth Configuration
builder.Services.Configure<GoogleAuthSettings>(
builder.Configuration.GetSection("Authentication:Google"));
builder.Services.Configure<MicrosoftAuthSettings>(
builder.Configuration.GetSection("Authentication:Microsoft"));
// Adicionar configurações
builder.Services.Configure<ModerationSettings>(
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<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;
}
};
});
// Localization
builder.Services.AddLocalization(options => options.ResourcesPath = "Resources");
builder.Services.Configure<RequestLocalizationOptions>(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<IUserRepository, UserRepository>();
builder.Services.AddScoped<IUserPageRepository, UserPageRepository>();
builder.Services.AddScoped<ICategoryRepository, CategoryRepository>();
builder.Services.AddScoped<ISubscriptionRepository, SubscriptionRepository>();
builder.Services.AddSingleton<IModerationAuthService, ModerationAuthService>();
//builder.Services.AddScoped<IModerationAuthService, ModerationAuthService>();
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
builder.Services.AddHostedService<TrialExpirationService>();
// Response Caching
builder.Services.AddResponseCaching();
builder.Services.AddMemoryCache();
builder.Services.AddRazorPages();
// ===== CONFIGURAÇÃO DOS HEALTH CHECKS =====
builder.Services.AddHealthChecks()
// MongoDB Health Check usando configuração existente
.AddCheck<MongoDbHealthCheck>(
name: "mongodb",
failureStatus: HealthStatus.Unhealthy,
timeout: TimeSpan.FromSeconds(10))
// Stripe Health Check
.AddCheck<StripeHealthCheck>(
name: "stripe",
failureStatus: HealthStatus.Degraded, // Stripe não é crítico para funcionalidade básica
timeout: TimeSpan.FromSeconds(15))
// SendGrid Health Check
.AddCheck<SendGridHealthCheck>(
name: "sendgrid",
failureStatus: HealthStatus.Degraded, // Email não é crítico para funcionalidade básica
timeout: TimeSpan.FromSeconds(10))
// External Services (OAuth providers)
.AddCheck<ExternalServicesHealthCheck>(
name: "external_services",
failureStatus: HealthStatus.Degraded, // OAuth pode estar indisponível temporariamente
timeout: TimeSpan.FromSeconds(20))
// System Resources
.AddCheck<SystemResourcesHealthCheck>(
name: "resources",
failureStatus: HealthStatus.Degraded,
timeout: TimeSpan.FromSeconds(5));
// Registrar health checks customizados no DI
builder.Services.AddTransient<MongoDbHealthCheck>();
builder.Services.AddTransient<StripeHealthCheck>();
builder.Services.AddTransient<SendGridHealthCheck>();
builder.Services.AddTransient<ExternalServicesHealthCheck>();
builder.Services.AddTransient<SystemResourcesHealthCheck>();
// HttpClient para External Services Health Check
builder.Services.AddHttpClient<ExternalServicesHealthCheck>(client =>
{
client.Timeout = TimeSpan.FromSeconds(10);
client.DefaultRequestHeaders.Add("User-Agent", "BCards-HealthCheck/1.0");
});
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.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseRequestLocalization();
app.UseAuthentication();
app.UseAuthorization();
// Add custom middleware
app.UseMiddleware<BCards.Web.Middleware.PlanLimitationMiddleware>();
app.UseMiddleware<BCards.Web.Middleware.PageStatusMiddleware>();
app.UseMiddleware<BCards.Web.Middleware.PreviewTokenMiddleware>();
app.UseMiddleware<ModerationAuthMiddleware>();
// 🔥 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}");
// Debug específico para Microsoft signin
if (context.Request.Path.StartsWithSegments("/signin-microsoft"))
{
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(
// 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?}");
// Initialize default data
using (var scope = app.Services.CreateScope())
{
var themeService = scope.ServiceProvider.GetRequiredService<IThemeService>();
var categoryService = scope.ServiceProvider.GetRequiredService<ICategoryService>();
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<ILogger<Program>>();
logger.LogError(ex, "Error initializing default data");
}
}
try
{
Log.Information("BCards application started successfully on {Hostname}", hostname);
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);
Log.CloseAndFlush();
}
// Make Program accessible for integration tests
public partial class Program { }