382 lines
12 KiB
C#
382 lines
12 KiB
C#
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.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.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 WebOptimizer;
|
||
|
||
// 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);
|
||
|
||
// 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.AddWebOptimizer(pipelines =>
|
||
{
|
||
pipelines.AddCssBundle(
|
||
"/css/app.min.css",
|
||
"css/site.css",
|
||
"css/qrrapido-theme.css");
|
||
|
||
pipelines.AddJavaScriptBundle(
|
||
"/js/app.min.js",
|
||
"js/test.js",
|
||
"js/simple-opcacity.js",
|
||
"js/qr-speed-generator.js",
|
||
"js/language-switcher.js",
|
||
"js/theme-toggle.js",
|
||
"js/cookie-consent.js");
|
||
});
|
||
|
||
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>();
|
||
|
||
// ✅ DataProtection compartilhado via FileSystem (ADICIONAR APÓS O CACHE)
|
||
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<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.UseWebOptimizer();
|
||
|
||
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(^(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();
|
||
}
|