using System.Text; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; using TeleWave.Application.Broadcast.Bumpers; using TeleWave.Application.Broadcast.Scheduling; using TeleWave.Application.Common.Interfaces; using TeleWave.Application.Streaming; using TeleWave.Domain.Broadcast.Scheduling; using TeleWave.Infrastructure.Broadcast; using TeleWave.Infrastructure.Identity; using TeleWave.Infrastructure.Library; using TeleWave.Infrastructure.Media; using TeleWave.Infrastructure.Metadata; using TeleWave.Infrastructure.Persistence; using TeleWave.Infrastructure.Settings; using TeleWave.Infrastructure.Streaming; namespace TeleWave.Infrastructure; /// /// Регистрация сервисов инфраструктуры: EF Core (PostgreSQL), Identity/JWT-аутентификация. /// public static class DependencyInjection { public static IServiceCollection AddInfrastructure( this IServiceCollection services, IConfiguration configuration ) { 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) ); var jwtOptions = configuration.GetSection(JwtOptions.SectionName).Get() ?? throw new InvalidOperationException("Секция конфигурации 'Jwt' не задана."); // Fail-fast на подписывающем ключе: этот же ключ подписывает и JWT, и stream-токены // (StreamTokenService), поэтому placeholder/короткий ключ из appsettings.json = полный обход // авторизации (можно сфорджить admin-JWT). Лучше не стартовать вовсе, чем стартовать уязвимым. // HMAC-SHA256 требует ключ не короче размера хеша (32 байта), иначе он ослаблен нулевым паддингом. if (Encoding.UTF8.GetByteCount(jwtOptions.SigningKey) < 32) throw new InvalidOperationException( "Jwt:SigningKey должен быть не короче 32 байт. Задайте криптостойкий секрет через конфигурацию/переменную окружения Jwt__SigningKey." ); if (jwtOptions.SigningKey.Contains("change-me", StringComparison.OrdinalIgnoreCase)) throw new InvalidOperationException( "Jwt:SigningKey использует значение-заглушку из appsettings.json. Переопределите его криптостойким секретом (Jwt__SigningKey)." ); 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), }; }); services.AddAuthorization(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); AddMedia(services, configuration); AddBroadcast(services, configuration); AddMetadata(services, configuration); return services; } /// Метаданные шоу/серий: провайдеры TMDb/OMDb, резолвер, локальное хранилище картинок. private static void AddMetadata(IServiceCollection services, IConfiguration configuration) { services.Configure(configuration.GetSection(MetadataOptions.SectionName)); services.AddHttpClient("metadata", client => client.Timeout = TimeSpan.FromSeconds(15)); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); } /// Планировщик расписания: генератор, источник случайности и фоновый сервис горизонта. private static void AddBroadcast(IServiceCollection services, IConfiguration configuration) { services.Configure( configuration.GetSection(SchedulerOptions.SectionName) ); services.Configure( configuration.GetSection(StreamingOptions.SectionName) ); services.Configure(configuration.GetSection(BumperOptions.SectionName)); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddHostedService(); services.AddHostedService(); } /// Хранилище медиа, обработка ffmpeg, очередь и фоновые сервисы (очередь + inbox-сканер). private static void AddMedia(IServiceCollection services, IConfiguration configuration) { services.Configure(configuration.GetSection(StorageOptions.SectionName)); services.Configure(configuration.GetSection(MediaOptions.SectionName)); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); } }