- Added new configuration options for user plans in `.env.example`, including `Plans__MaxCustomConfigCount` and `Plans__MinCustomConfigCount`. - Introduced `MapPlanEndpoints` in `Program.cs` to handle plan-related API routes. - Implemented `SetUserPlan` endpoint in `RoleEndpoints` to allow admins to assign plans to users. - Removed deprecated role request approval endpoints from `AdminSupportEndpoints`. - Updated `ITelegramNotifier` and related classes to reflect changes in role request handling and payment notifications. - Refactored role management commands to remove `MaxConfigs` and focus on `MaxIpLimit` and billing settings. - Enhanced billing request handling to accommodate plan changes instead of role changes. - Updated various interfaces and command handlers to support new plan management features.
173 lines
8.5 KiB
C#
173 lines
8.5 KiB
C#
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.Application.Plans;
|
|
using PnvPanel.Infrastructure.Apps;
|
|
using PnvPanel.Infrastructure.BackgroundJobs;
|
|
using PnvPanel.Infrastructure.Identity;
|
|
using PnvPanel.Infrastructure.Instructions;
|
|
using PnvPanel.Infrastructure.Persistence;
|
|
using PnvPanel.Infrastructure.Plans;
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Регистрация сервисов инфраструктуры: EF Core (PostgreSQL), Identity/JWT-аутентификация,
|
|
/// XuiPanelGateway, шифрование секретов, (в будущем) SignalR-пуш, фоновые сервисы.
|
|
/// </summary>
|
|
public static class DependencyInjection
|
|
{
|
|
public static IServiceCollection AddInfrastructure(
|
|
this IServiceCollection services,
|
|
IConfiguration configuration
|
|
)
|
|
{
|
|
// Строка подключения читается лениво внутри лямбды (а не в локальную переменную сразу), иначе
|
|
// в тестах WebApplicationFactory.ConfigureAppConfiguration (Testcontainers-порт) не успевает
|
|
// примениться до регистрации DbContext — окажется закэширован дефолт из appsettings.json.
|
|
services.AddDbContext<AppDbContext>(options =>
|
|
options.UseNpgsql(
|
|
configuration["ConnectionStrings:Default"]
|
|
?? throw new InvalidOperationException(
|
|
"Строка подключения 'ConnectionStrings:Default' не сконфигурирована."
|
|
)
|
|
)
|
|
);
|
|
services.AddScoped<IAppDbContext>(sp => sp.GetRequiredService<AppDbContext>());
|
|
|
|
services
|
|
.AddIdentityCore<AppUser>(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<AppRole>()
|
|
.AddEntityFrameworkStores<AppDbContext>()
|
|
.AddSignInManager()
|
|
.AddDefaultTokenProviders();
|
|
|
|
services.Configure<JwtOptions>(configuration.GetSection(JwtOptions.SectionName));
|
|
services.Configure<AdminSeedOptions>(
|
|
configuration.GetSection(AdminSeedOptions.SectionName)
|
|
);
|
|
services.Configure<RolesOptions>(configuration.GetSection(RolesOptions.SectionName));
|
|
services.Configure<PlansOptions>(configuration.GetSection(PlansOptions.SectionName));
|
|
|
|
var jwtOptions =
|
|
configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>()
|
|
?? 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<IXuiHttpClientFactory, XuiHttpClientFactory>();
|
|
|
|
// Построители connection string (по одному на протокол) + резолвер по имени протокола.
|
|
services.AddSingleton<IXuiConnectionStringBuilder, VlessConnectionStringBuilder>();
|
|
services.AddSingleton<IXuiConnectionStringBuilder, VmessConnectionStringBuilder>();
|
|
services.AddSingleton<IXuiConnectionStringBuilder, TrojanConnectionStringBuilder>();
|
|
services.AddSingleton<IXuiConnectionStringBuilder, ShadowsocksConnectionStringBuilder>();
|
|
services.AddSingleton<
|
|
IXuiConnectionStringBuilderResolver,
|
|
XuiConnectionStringBuilderResolver
|
|
>();
|
|
|
|
services.AddSingleton<ISecretProtector, DataProtectionSecretProtector>();
|
|
// Singleton: держит кэш per-node XUI-клиентов между запросами (см. XuiPanelGateway).
|
|
services.AddSingleton<IXuiPanelGateway, XuiPanelGateway>();
|
|
|
|
services.AddScoped<IIdentityService, IdentityService>();
|
|
services.AddScoped<IJwtTokenService, JwtTokenService>();
|
|
services.AddScoped<IRefreshTokenService, RefreshTokenService>();
|
|
services.AddScoped<IRoleService, RoleService>();
|
|
services.AddScoped<IClientAppCatalogSeeder, ClientAppCatalogSeeder>();
|
|
services.AddScoped<IInstructionIntroSeeder, InstructionIntroSeeder>();
|
|
services.AddScoped<IPricingSettingsSeeder, PricingSettingsSeeder>();
|
|
services.AddScoped<IPlanSeeder, PlanSeeder>();
|
|
// Один и тот же экземпляр CurrentUser на scope — и как ICurrentUser (чтение), и как
|
|
// ICurrentUserSetter (запись, только для Telegram-бота, см. TelegramBotHostedService).
|
|
services.AddScoped<CurrentUser>();
|
|
services.AddScoped<ICurrentUser>(sp => sp.GetRequiredService<CurrentUser>());
|
|
services.AddScoped<ICurrentUserSetter>(sp => sp.GetRequiredService<CurrentUser>());
|
|
services.AddScoped<DbInitializer>();
|
|
|
|
services.Configure<TrafficRetentionOptions>(
|
|
configuration.GetSection(TrafficRetentionOptions.SectionName)
|
|
);
|
|
services.AddHostedService<TrafficSyncService>();
|
|
services.AddHostedService<NodeHealthCheckService>();
|
|
services.AddHostedService<TrafficRetentionService>();
|
|
services.AddHostedService<BillingService>();
|
|
|
|
services.Configure<TelegramOptions>(configuration.GetSection(TelegramOptions.SectionName));
|
|
|
|
services.Configure<FileStorageOptions>(
|
|
configuration.GetSection(FileStorageOptions.SectionName)
|
|
);
|
|
services.AddSingleton<IFileStorage, DiskFileStorage>();
|
|
|
|
return services;
|
|
}
|
|
}
|