Files
PnvPanel/backend/src/PnvPanel.Api/Program.cs
T
Leonid Pershin df137ca5a7
CI / Backend (build + test) (push) Successful in 1m18s
CI / Frontend (lint + typecheck + build) (push) Successful in 31s
Refactor project files for improved readability and structure
- Cleaned up whitespace in Directory.Build.props and Directory.Packages.props for consistency.
- Reformatted project file references in PnvPanel.Api.csproj for better clarity.
- Enhanced code readability in various endpoint files by adjusting line breaks and indentation.
- Standardized method signatures and improved formatting in ResultExtensions and multiple endpoint classes for better maintainability.
2026-07-14 07:24:13 +03:00

207 lines
8.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Net;
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 терминируется вне контейнера), но ТОЛЬКО от явно
// перечисленных адресов/сетей прокси — иначе клиент может подделать свой IP/схему напрямую, минуя
// прокси (спуфинг rate-limiting по IP, аудит-лога, Secure-cookie). По умолчанию (без конфигурации)
// остаётся дефолт ASP.NET Core — доверие только loopback; для прод-топологии прокси задаётся через
// ForwardedHeaders__KnownProxies / ForwardedHeaders__KnownNetworks (см. .env.example).
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
foreach (
var proxy in builder
.Configuration.GetSection("ForwardedHeaders:KnownProxies")
.Get<string[]>()
?? []
)
options.KnownProxies.Add(IPAddress.Parse(proxy));
foreach (
var network in builder
.Configuration.GetSection("ForwardedHeaders:KnownNetworks")
.Get<string[]>()
?? []
)
{
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.AddSignalR();
// В Api, не в Infrastructure — реализации нужен IHubContext<PanelHub>, а Hub определён здесь же.
builder.Services.AddSingleton<IRealtimeNotifier, SignalRRealtimeNotifier>();
// Telegram-бот: presentation-адаптер, хостится в процессе Api (long polling). Клиент регистрируем
// всегда (даже с пустым токеном) — TelegramBotHostedService сам решает не стартовать без токена,
// а TelegramNotifier — не слать сообщения. TelegramBotClient(...) при этом валидирует формат токена
// и падает на пустой строке, поэтому при пустом BotToken подставляем синтаксически валидную заглушку —
// реальный HTTP-вызов через неё никогда не происходит (все вызывающие места сами проверяют BotToken).
builder.Services.AddSingleton<ITelegramBotClient>(sp =>
{
var options = sp.GetRequiredService<IOptions<TelegramOptions>>().Value;
var token = string.IsNullOrWhiteSpace(options.BotToken)
? "0:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
: options.BotToken;
if (string.IsNullOrWhiteSpace(options.ProxyUrl))
return new TelegramBotClient(token);
// Прокси (обычно socks5://) для запросов к Bot API — на случай, если Telegram недоступен напрямую
// с сети сервера. Пусто (по умолчанию) — без прокси, прямое подключение (см. .env.example).
var proxyUri = new Uri(options.ProxyUrl);
var proxy = new WebProxy(proxyUri);
if (!string.IsNullOrEmpty(proxyUri.UserInfo))
{
var credentials = proxyUri.UserInfo.Split(':', 2);
proxy.Credentials = new NetworkCredential(
credentials[0],
credentials.Length > 1 ? credentials[1] : string.Empty
);
}
sp.GetRequiredService<ILogger<Program>>()
.LogInformation(
"Telegram bot using proxy {Scheme}://{Host}:{Port}",
proxyUri.Scheme,
proxyUri.Host,
proxyUri.Port
);
var handler = new SocketsHttpHandler { Proxy = proxy, UseProxy = true };
return new TelegramBotClient(token, new HttpClient(handler));
});
// Scoped — зависит от IIdentityService (scoped), не Singleton.
builder.Services.AddScoped<ITelegramNotifier, TelegramNotifier>();
// Singleton — кэширует username бота (getMe) на весь процесс, не из ручного env (см. TelegramBotInfo).
builder.Services.AddSingleton<ITelegramBotInfo, TelegramBotInfo>();
builder.Services.AddSingleton<PnvBotUpdateHandler>();
builder.Services.AddHostedService<TelegramBotHostedService>();
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<AppDbContext>();
var app = builder.Build();
// Без персистентного пути key-ring живёт только в памяти контейнера — после пересоздания
// расшифровать уже сохранённые пароли нод будет невозможно. Предупреждаем громко, не молчим.
if (string.IsNullOrWhiteSpace(builder.Configuration["DataProtection:KeyRingPath"]))
{
app.Logger.LogWarning(
"DataProtection:KeyRingPath is not set — node secret encryption keys are not persistent "
+ "and will be lost when the container is recreated. Mount a volume and set the path in production."
);
}
// Авто-применение миграций и идемпотентный сидинг (роли + админ из 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.MapNewsEndpoints();
app.MapAdminUserEndpoints();
app.MapAdminStatsEndpoints();
app.MapAdminAppEndpoints();
app.MapAdminNewsEndpoints();
app.MapSupportEndpoints();
app.MapAdminSupportEndpoints();
app.MapTelegramEndpoints();
app.MapHub<PanelHub>("/hubs/panel");
// Раздача статики SPA из wwwroot + fallback на index.html для клиентских маршрутов.
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapFallbackToFile("index.html");
app.Run();
/// <summary>Делает неявный класс Program доступным для WebApplicationFactory&lt;Program&gt; в интеграционных тестах.</summary>
public partial class Program;