Files
PnvPanel/backend/src/PnvPanel.Infrastructure/DependencyInjection.cs
T
Leonid Pershin fad03c2834
CI / Backend (build + test) (push) Failing after 1m23s
CI / Frontend (lint + typecheck + build) (push) Successful in 34s
Enhance user plan management and update related endpoints
- 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.
2026-07-23 22:52:20 +03:00

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