QrRapido/Program.cs
Ricardo Carneiro bdf78ed418
All checks were successful
Deploy QR Rapido / test (push) Successful in 45s
Deploy QR Rapido / build-and-push (push) Successful in 16m19s
Deploy QR Rapido / deploy-staging (push) Has been skipped
Deploy QR Rapido / deploy-production (push) Successful in 2m12s
fix: restaurar carregamento de Docker Secrets no Program.cs
A linha builder.Configuration.AddDockerSecrets() foi perdida durante
o merge, causando HTTP 500 em produção porque os secrets não são
carregados e valores como "LOADED_FROM_DOCKER_SECRET" são usados
diretamente na conexão MongoDB/Stripe/OAuth.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 10:26:57 -03:00

389 lines
13 KiB
C#
Raw Blame History

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;
// Fix for WSL path issues - disable StaticWebAssets completely
var options = new WebApplicationOptions
{
Args = args,
ContentRootPath = Directory.GetCurrentDirectory(),
WebRootPath = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot")
};
// Disable StaticWebAssets for WSL compatibility
Environment.SetEnvironmentVariable("ASPNETCORE_HOSTINGSTARTUP__STATICWEBASSETS__ENABLED", "false");
var builder = WebApplication.CreateBuilder(options);
// Add Docker Secrets as configuration source (for Swarm deployments)
// Secrets override values from appsettings.json
builder.Configuration.AddDockerSecrets();
// 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<73>o na mem<65>ria
builder.Services.AddSession(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
options.IdleTimeout = TimeSpan.FromMinutes(30); // Tempo de expira<72><61>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<IMongoClient>(serviceProvider =>
{
return new MongoClient(mongoConnectionString);
});
builder.Services.AddScoped<MongoDbContext>();
}
catch
{
// MongoDB not available - services will handle gracefully
builder.Services.AddScoped<MongoDbContext>();
}
}
else
{
// Development mode without MongoDB
builder.Services.AddScoped<MongoDbContext>();
}
builder.Services.AddMemoryCache();
builder.Services.AddSingleton<IDistributedCache, MemoryDistributedCacheWrapper>();
builder.Services.Configure<AdsConfigurationOptions>(builder.Configuration.GetSection("Ads"));
builder.Services.AddScoped<IAdSlotConfigurationProvider, ConfigurationAdSlotProvider>();
// ✅ 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<RequestLocalizationOptions>(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<IQRCodeService, QRRapidoService>();
builder.Services.AddScoped<IUserService, UserService>();
builder.Services.AddScoped<IPlanService, QRRapidoApp.Services.PlanService>();
builder.Services.AddScoped<IMarkdownService, MarkdownService>();
builder.Services.AddScoped<AdDisplayService>();
builder.Services.AddScoped<StripeService>();
builder.Services.AddScoped<LogoReadabilityAnalyzer>();
// Background Services
builder.Services.AddHostedService<HistoryCleanupService>();
// Monitoring Services
if (builder.Configuration.GetValue<bool>("ResourceMonitoring:Enabled", true))
{
builder.Services.AddHostedService<ResourceMonitoringService>();
}
//if (builder.Configuration.GetValue<bool>("MongoDbMonitoring:Enabled", true))
//{
// builder.Services.AddHostedService<MongoDbMonitoringService>();
//}
// 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<MongoDbHealthCheck>();
builder.Services.AddScoped<SeqHealthCheck>();
builder.Services.AddScoped<ResourceHealthCheck>();
builder.Services.AddScoped<ExternalServicesHealthCheck>();
builder.Services.AddHealthChecks()
.AddCheck<MongoDbHealthCheck>("mongodb")
.AddCheck<ResourceHealthCheck>("resources")
.AddCheck<ExternalServicesHealthCheck>("external_services");
builder.Services.Configure<ForwardedHeadersOptions>(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<KestrelServerOptions>(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<LanguageRedirectionMiddleware>();
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<LastLoginUpdateMiddleware>();
// 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();
}