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