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; } }