using System.Net; using System.Text.Json.Serialization; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.RateLimiting; using TeleWave.Api.Common; using TeleWave.Api.Endpoints; using TeleWave.Application; using TeleWave.Infrastructure; using TeleWave.Infrastructure.Identity; using TeleWave.Infrastructure.Persistence; using Scalar.AspNetCore; using Serilog; var builder = WebApplication.CreateBuilder(args); // Загрузка медиа стримится на диск; поднимаем лимит тела запроса Kestrel до максимума загрузки // (иначе дефолтные ~30 МБ рубят большие файлы). Собственный контроль размера — в MediaEndpoints. builder.WebHost.ConfigureKestrel(options => options.Limits.MaxRequestBodySize = builder.Configuration.GetValue("Media:MaxUploadBytes") ?? 20L * 1024 * 1024 * 1024 ); // Структурное логирование (Serilog), конфигурация из appsettings/env. builder.Services.AddSerilog( (services, configuration) => configuration .ReadFrom.Configuration(builder.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext() ); // За внешним прокси доверяем X-Forwarded-* (TLS терминируется вне контейнера), но ТОЛЬКО от явно // перечисленных адресов/сетей прокси — иначе клиент может подделать свой IP/схему напрямую, минуя // прокси. По умолчанию (без конфигурации) остаётся дефолт ASP.NET Core — доверие только loopback; // для прод-топологии прокси задаётся через ForwardedHeaders__KnownProxies/KnownNetworks (см. .env.example). builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; foreach ( var proxy in builder .Configuration.GetSection("ForwardedHeaders:KnownProxies") .Get() ?? [] ) options.KnownProxies.Add(IPAddress.Parse(proxy)); foreach ( var network in builder .Configuration.GetSection("ForwardedHeaders:KnownNetworks") .Get() ?? [] ) { var parts = network.Split('/'); options.KnownIPNetworks.Add( new System.Net.IPNetwork(IPAddress.Parse(parts[0]), int.Parse(parts[1])) ); } }); builder.Services.AddHttpContextAccessor(); builder.Services.AddApplication(); builder.Services.AddInfrastructure(builder.Configuration); builder.Services.AddRateLimiter(options => { options.AddFixedWindowLimiter( RateLimiting.AuthPolicy, limiterOptions => { limiterOptions.PermitLimit = builder.Configuration.GetValue( "RateLimiting:AuthPermitLimit", 20 ); limiterOptions.Window = TimeSpan.FromMinutes(1); limiterOptions.QueueLimit = 0; } ); options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; }); // Энумы сериализуются строками, не числами — самодокументируемый JSON. builder.Services.ConfigureHttpJsonOptions(options => options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()) ); builder.Services.AddProblemDetails(); builder.Services.AddOpenApi(); builder.Services.AddHealthChecks().AddDbContextCheck(); var app = builder.Build(); // Авто-применение миграций и идемпотентный сидинг (роли + админ из env) на старте. await app.Services.ApplyMigrationsAsync(); await app.Services.SeedDataAsync(); app.UseForwardedHeaders(); app.UseSerilogRequestLogging(); app.UseExceptionHandler(); app.UseRateLimiter(); app.UseAuthentication(); app.UseAuthorization(); app.MapOpenApi(); app.MapScalarApiReference(); app.MapHealthChecks("/health"); app.MapAuthEndpoints(); app.MapRoleEndpoints(); app.MapAdminUserEndpoints(); app.MapMediaEndpoints(); // Раздача статики SPA из wwwroot + fallback на index.html для клиентских маршрутов. app.UseDefaultFiles(); app.UseStaticFiles(); app.MapFallbackToFile("index.html"); app.Run(); /// Делает неявный класс Program доступным для WebApplicationFactory<Program> в интеграционных тестах. public partial class Program;