using System.Text;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using PnvPanel.Application.Common.Interfaces;
using PnvPanel.Infrastructure.Apps;
using PnvPanel.Infrastructure.BackgroundJobs;
using PnvPanel.Infrastructure.Identity;
using PnvPanel.Infrastructure.Instructions;
using PnvPanel.Infrastructure.Persistence;
using PnvPanel.Infrastructure.Pricing;
using PnvPanel.Infrastructure.Security;
using PnvPanel.Infrastructure.Storage;
using PnvPanel.Infrastructure.Telegram;
using PnvPanel.Infrastructure.Xui;
using ThreeXui.ConnectionStrings;
using ThreeXui.Http;
namespace PnvPanel.Infrastructure;
///
/// Регистрация сервисов инфраструктуры: EF Core (PostgreSQL), Identity/JWT-аутентификация,
/// XuiPanelGateway, шифрование секретов, (в будущем) SignalR-пуш, фоновые сервисы.
///
public static class DependencyInjection
{
public static IServiceCollection AddInfrastructure(
this IServiceCollection services,
IConfiguration configuration
)
{
// Строка подключения читается лениво внутри лямбды (а не в локальную переменную сразу), иначе
// в тестах WebApplicationFactory.ConfigureAppConfiguration (Testcontainers-порт) не успевает
// примениться до регистрации DbContext — окажется закэширован дефолт из appsettings.json.
services.AddDbContext(options =>
options.UseNpgsql(
configuration["ConnectionStrings:Default"]
?? throw new InvalidOperationException(
"Строка подключения 'ConnectionStrings:Default' не сконфигурирована."
)
)
);
services.AddScoped(sp => sp.GetRequiredService());
services
.AddIdentityCore(options =>
{
options.User.RequireUniqueEmail = false;
options.Password.RequiredLength = 8;
options.Password.RequireDigit = true;
options.Password.RequireUppercase = true;
options.Password.RequireNonAlphanumeric = false;
options.Lockout.MaxFailedAccessAttempts = 5;
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
options.Lockout.AllowedForNewUsers = true;
})
.AddRoles()
.AddEntityFrameworkStores()
.AddSignInManager()
.AddDefaultTokenProviders();
services.Configure(configuration.GetSection(JwtOptions.SectionName));
services.Configure(
configuration.GetSection(AdminSeedOptions.SectionName)
);
services.Configure(configuration.GetSection(RolesOptions.SectionName));
var jwtOptions =
configuration.GetSection(JwtOptions.SectionName).Get()
?? throw new InvalidOperationException("Секция конфигурации 'Jwt' не задана.");
services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = jwtOptions.Issuer,
ValidateAudience = true,
ValidAudience = jwtOptions.Audience,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtOptions.SigningKey)
),
ValidateLifetime = true,
ClockSkew = TimeSpan.FromSeconds(30),
};
// SignalR JS-клиент не может выставить заголовок Authorization на WebSocket-хендшейке —
// передаёт токен через query-string (?access_token=...), только для /hubs.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
if (
!string.IsNullOrEmpty(accessToken)
&& context.HttpContext.Request.Path.StartsWithSegments("/hubs")
)
context.Token = accessToken;
return Task.CompletedTask;
},
};
});
services.AddAuthorization();
// Шифрование секретов нод at-rest (ASP.NET Core Data Protection). Key-ring — на постоянном
// томе (DataProtection__KeyRingPath), иначе секреты станут нечитаемы при пересоздании контейнера.
var keyRingPath = configuration["DataProtection:KeyRingPath"];
var dataProtectionBuilder = services.AddDataProtection();
if (!string.IsNullOrWhiteSpace(keyRingPath))
dataProtectionBuilder.PersistKeysToFileSystem(new DirectoryInfo(keyRingPath));
services.AddSingleton();
// Построители connection string (по одному на протокол) + резолвер по имени протокола.
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton<
IXuiConnectionStringBuilderResolver,
XuiConnectionStringBuilderResolver
>();
services.AddSingleton();
// Singleton: держит кэш per-node XUI-клиентов между запросами (см. XuiPanelGateway).
services.AddSingleton();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
services.AddScoped();
// Один и тот же экземпляр CurrentUser на scope — и как ICurrentUser (чтение), и как
// ICurrentUserSetter (запись, только для Telegram-бота, см. TelegramBotHostedService).
services.AddScoped();
services.AddScoped(sp => sp.GetRequiredService());
services.AddScoped(sp => sp.GetRequiredService());
services.AddScoped();
services.Configure(
configuration.GetSection(TrafficRetentionOptions.SectionName)
);
services.AddHostedService();
services.AddHostedService();
services.AddHostedService();
services.AddHostedService();
services.Configure(configuration.GetSection(TelegramOptions.SectionName));
services.Configure(
configuration.GetSection(FileStorageOptions.SectionName)
);
services.AddSingleton();
return services;
}
}