using System.Text.Json.Serialization; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.RateLimiting; using Microsoft.Extensions.Options; using PnvPanel.Api.Common; using PnvPanel.Api.Endpoints; using PnvPanel.Api.Hubs; using PnvPanel.Api.Telegram; using PnvPanel.Application; using PnvPanel.Application.Common.Interfaces; using PnvPanel.Infrastructure; using PnvPanel.Infrastructure.Identity; using PnvPanel.Infrastructure.Persistence; using PnvPanel.Infrastructure.Telegram; using Scalar.AspNetCore; using Serilog; using Telegram.Bot; var builder = WebApplication.CreateBuilder(args); // Структурное логирование (Serilog), конфигурация из appsettings/env. builder.Services.AddSerilog((services, configuration) => configuration .ReadFrom.Configuration(builder.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext()); // За внешним прокси доверяем X-Forwarded-* (TLS терминируется вне контейнера). builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; options.KnownIPNetworks.Clear(); options.KnownProxies.Clear(); }); builder.Services.AddHttpContextAccessor(); builder.Services.AddApplication(); builder.Services.AddInfrastructure(builder.Configuration); builder.Services.AddSignalR(); // В Api, не в Infrastructure — реализации нужен IHubContext, а Hub определён здесь же. builder.Services.AddSingleton(); // Telegram-бот: presentation-адаптер, хостится в процессе Api (long polling). Клиент регистрируем // всегда (даже с пустым токеном) — TelegramBotHostedService сам решает не стартовать без токена, // а TelegramNotifier — не слать сообщения. TelegramBotClient(...) при этом валидирует формат токена // и падает на пустой строке, поэтому при пустом BotToken подставляем синтаксически валидную заглушку — // реальный HTTP-вызов через неё никогда не происходит (все вызывающие места сами проверяют BotToken). builder.Services.AddSingleton(sp => { var options = sp.GetRequiredService>(); var token = options.Value.BotToken; return new TelegramBotClient(string.IsNullOrWhiteSpace(token) ? "0:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" : token); }); // Scoped — зависит от IIdentityService (scoped), не Singleton. builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddRateLimiter(options => { options.AddFixedWindowLimiter(RateLimiting.AuthPolicy, limiterOptions => { // Настраиваемо через конфиг, чтобы интеграционные тесты (общий TestServer/host на весь // collection, все запросы — от одного "клиента") могли поднять лимит и не ловить 429. limiterOptions.PermitLimit = builder.Configuration.GetValue("RateLimiting:AuthPermitLimit", 20); limiterOptions.Window = TimeSpan.FromMinutes(1); limiterOptions.QueueLimit = 0; }); options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; }); // Энумы сериализуются строками ("Vless", "Active", ...), не числами — самодокументируемый JSON, // корректные строковые литералы при генерации TS-типов из OpenAPI-схемы (см. docs/frontend.md). 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(); // OpenAPI-схема (/openapi/v1.json) + современный UI Scalar (/scalar). app.MapOpenApi(); app.MapScalarApiReference(); app.MapHealthChecks("/health"); app.MapAuthEndpoints(); app.MapActivationEndpoints(); app.MapRoleEndpoints(); app.MapNodeEndpoints(); app.MapInboundEndpoints(); app.MapConfigEndpoints(); app.MapSubscriptionEndpoints(); app.MapAppEndpoints(); app.MapAdminUserEndpoints(); app.MapAdminStatsEndpoints(); app.MapAdminAppEndpoints(); app.MapTelegramEndpoints(); app.MapHub("/hubs/panel"); // Раздача статики SPA из wwwroot + fallback на index.html для клиентских маршрутов. app.UseDefaultFiles(); app.UseStaticFiles(); app.MapFallbackToFile("index.html"); app.Run(); /// Делает неявный класс Program доступным для WebApplicationFactory<Program> в интеграционных тестах. public partial class Program;