Files
PnvPanel/backend/src/PnvPanel.Infrastructure/DependencyInjection.cs
T
Leonid Pershin b2ae358250
CI / Backend (build + test) (push) Successful in 1m22s
CI / Frontend (lint + typecheck + build) (push) Successful in 33s
Implement billing functionality and enhance role management
- Introduced billing capabilities, allowing users to request payments for subscription periods (3/6/12 months) with admin approval via Telegram.
- Updated role management to include a `BillingEnabled` property, preventing billing for admin roles.
- Enhanced the `CreateRoleCommand` and `UpdateRoleCommand` to accept billing parameters, ensuring proper handling during role creation and updates.
- Added new endpoints for billing management and integrated billing checks into VPN config creation to enforce payment requirements.
- Updated related services, models, and tests to support the new billing features, ensuring comprehensive coverage and functionality.
- Enhanced documentation to reflect the new billing processes and role management changes.
2026-07-19 01:38:16 +03:00

169 lines
8.3 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.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;
/// <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));
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>();
// Один и тот же экземпляр 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;
}
}