Refactor project files for improved readability and structure
- Cleaned up whitespace in Directory.Build.props and Directory.Packages.props for consistency. - Reformatted project file references in PnvPanel.Api.csproj for better clarity. - Enhanced code readability in various endpoint files by adjusting line breaks and indentation. - Standardized method signatures and improved formatting in ResultExtensions and multiple endpoint classes for better maintainability.
This commit is contained in:
@@ -8,7 +8,10 @@ using PnvPanel.Infrastructure.Persistence;
|
||||
|
||||
namespace PnvPanel.Infrastructure.BackgroundJobs;
|
||||
|
||||
public sealed class NodeHealthCheckService(IServiceScopeFactory scopeFactory, ILogger<NodeHealthCheckService> logger) : BackgroundService
|
||||
public sealed class NodeHealthCheckService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<NodeHealthCheckService> logger
|
||||
) : BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan Interval = TimeSpan.FromMinutes(2);
|
||||
|
||||
@@ -25,8 +28,7 @@ public sealed class NodeHealthCheckService(IServiceScopeFactory scopeFactory, IL
|
||||
{
|
||||
logger.LogError(ex, "Node health-check failed");
|
||||
}
|
||||
}
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
} while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
|
||||
private async Task CheckAllAsync(CancellationToken cancellationToken)
|
||||
@@ -46,7 +48,12 @@ public sealed class NodeHealthCheckService(IServiceScopeFactory scopeFactory, IL
|
||||
if (node.Status != newStatus)
|
||||
{
|
||||
node.UpdateStatus(newStatus);
|
||||
await notifier.NotifyNodeStatusChangedAsync(node.Id, newStatus, node.LastSyncAt, cancellationToken);
|
||||
await notifier.NotifyNodeStatusChangedAsync(
|
||||
node.Id,
|
||||
newStatus,
|
||||
node.LastSyncAt,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ namespace PnvPanel.Infrastructure.BackgroundJobs;
|
||||
|
||||
/// <summary>TTL-чистка истории трафика (TrafficRetention__RetentionDays, по умолчанию 30 дней).</summary>
|
||||
public sealed class TrafficRetentionService(
|
||||
IServiceScopeFactory scopeFactory, IOptions<TrafficRetentionOptions> options, ILogger<TrafficRetentionService> logger)
|
||||
: BackgroundService
|
||||
IServiceScopeFactory scopeFactory,
|
||||
IOptions<TrafficRetentionOptions> options,
|
||||
ILogger<TrafficRetentionService> logger
|
||||
) : BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan Interval = TimeSpan.FromHours(24);
|
||||
|
||||
@@ -27,8 +29,7 @@ public sealed class TrafficRetentionService(
|
||||
{
|
||||
logger.LogError(ex, "Traffic history retention failed");
|
||||
}
|
||||
}
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
} while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
|
||||
private async Task CleanupAsync(CancellationToken cancellationToken)
|
||||
@@ -37,7 +38,9 @@ public sealed class TrafficRetentionService(
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
var cutoff = DateTimeOffset.UtcNow.AddDays(-options.Value.RetentionDays);
|
||||
var deleted = await dbContext.TrafficSamples.Where(s => s.Timestamp < cutoff).ExecuteDeleteAsync(cancellationToken);
|
||||
var deleted = await dbContext
|
||||
.TrafficSamples.Where(s => s.Timestamp < cutoff)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
if (deleted > 0)
|
||||
logger.LogInformation("Deleted {Count} expired traffic history records", deleted);
|
||||
|
||||
@@ -13,7 +13,10 @@ namespace PnvPanel.Infrastructure.BackgroundJobs;
|
||||
/// VpnConfig + пишет TrafficSample. Реконсиляция дрейфа: если панель недоступна — просто пропускаем
|
||||
/// эту ноду в этом цикле, не роняем весь сервис и не трогаем локальные данные.
|
||||
/// </summary>
|
||||
public sealed class TrafficSyncService(IServiceScopeFactory scopeFactory, ILogger<TrafficSyncService> logger) : BackgroundService
|
||||
public sealed class TrafficSyncService(
|
||||
IServiceScopeFactory scopeFactory,
|
||||
ILogger<TrafficSyncService> logger
|
||||
) : BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan Interval = TimeSpan.FromMinutes(5);
|
||||
|
||||
@@ -30,8 +33,7 @@ public sealed class TrafficSyncService(IServiceScopeFactory scopeFactory, ILogge
|
||||
{
|
||||
logger.LogError(ex, "Traffic sync failed");
|
||||
}
|
||||
}
|
||||
while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
} while (await timer.WaitForNextTickAsync(stoppingToken));
|
||||
}
|
||||
|
||||
private async Task SyncAllAsync(CancellationToken cancellationToken)
|
||||
@@ -45,18 +47,26 @@ public sealed class TrafficSyncService(IServiceScopeFactory scopeFactory, ILogge
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
var inbounds = await dbContext.Inbounds.Where(i => i.NodeId == node.Id).ToListAsync(cancellationToken);
|
||||
var inbounds = await dbContext
|
||||
.Inbounds.Where(i => i.NodeId == node.Id)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var inbound in inbounds)
|
||||
{
|
||||
var configs = await dbContext.VpnConfigs
|
||||
.Where(c => c.InboundId == inbound.Id && c.Status == ConfigStatus.Active)
|
||||
var configs = await dbContext
|
||||
.VpnConfigs.Where(c =>
|
||||
c.InboundId == inbound.Id && c.Status == ConfigStatus.Active
|
||||
)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
if (configs.Count == 0)
|
||||
continue;
|
||||
|
||||
var trafficResult = await gateway.GetClientTrafficAsync(node, inbound.RemoteInboundId, cancellationToken);
|
||||
var trafficResult = await gateway.GetClientTrafficAsync(
|
||||
node,
|
||||
inbound.RemoteInboundId,
|
||||
cancellationToken
|
||||
);
|
||||
if (!trafficResult.IsSuccess)
|
||||
continue;
|
||||
|
||||
@@ -66,10 +76,22 @@ public sealed class TrafficSyncService(IServiceScopeFactory scopeFactory, ILogge
|
||||
continue;
|
||||
|
||||
config.UpdateTraffic(traffic.UpBytes, traffic.DownBytes);
|
||||
dbContext.TrafficSamples.Add(TrafficSample.Create(config.Id, DateTimeOffset.UtcNow, traffic.UpBytes, traffic.DownBytes));
|
||||
dbContext.TrafficSamples.Add(
|
||||
TrafficSample.Create(
|
||||
config.Id,
|
||||
DateTimeOffset.UtcNow,
|
||||
traffic.UpBytes,
|
||||
traffic.DownBytes
|
||||
)
|
||||
);
|
||||
|
||||
await notifier.NotifyConfigTrafficUpdatedAsync(
|
||||
config.UserId, config.Id, traffic.UpBytes, traffic.DownBytes, cancellationToken);
|
||||
config.UserId,
|
||||
config.Id,
|
||||
traffic.UpBytes,
|
||||
traffic.DownBytes,
|
||||
cancellationToken
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,14 +25,22 @@ namespace PnvPanel.Infrastructure;
|
||||
/// </summary>
|
||||
public static class DependencyInjection
|
||||
{
|
||||
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
||||
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.AddDbContext<AppDbContext>(options =>
|
||||
options.UseNpgsql(
|
||||
configuration["ConnectionStrings:Default"]
|
||||
?? throw new InvalidOperationException(
|
||||
"Строка подключения 'ConnectionStrings:Default' не сконфигурирована."
|
||||
)
|
||||
)
|
||||
);
|
||||
services.AddScoped<IAppDbContext>(sp => sp.GetRequiredService<AppDbContext>());
|
||||
|
||||
services
|
||||
@@ -53,10 +61,13 @@ public static class DependencyInjection
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
services.Configure<JwtOptions>(configuration.GetSection(JwtOptions.SectionName));
|
||||
services.Configure<AdminSeedOptions>(configuration.GetSection(AdminSeedOptions.SectionName));
|
||||
services.Configure<AdminSeedOptions>(
|
||||
configuration.GetSection(AdminSeedOptions.SectionName)
|
||||
);
|
||||
services.Configure<RolesOptions>(configuration.GetSection(RolesOptions.SectionName));
|
||||
|
||||
var jwtOptions = configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>()
|
||||
var jwtOptions =
|
||||
configuration.GetSection(JwtOptions.SectionName).Get<JwtOptions>()
|
||||
?? throw new InvalidOperationException("Секция конфигурации 'Jwt' не задана.");
|
||||
|
||||
services
|
||||
@@ -70,7 +81,9 @@ public static class DependencyInjection
|
||||
ValidateAudience = true,
|
||||
ValidAudience = jwtOptions.Audience,
|
||||
ValidateIssuerSigningKey = true,
|
||||
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)),
|
||||
IssuerSigningKey = new SymmetricSecurityKey(
|
||||
Encoding.UTF8.GetBytes(jwtOptions.SigningKey)
|
||||
),
|
||||
ValidateLifetime = true,
|
||||
ClockSkew = TimeSpan.FromSeconds(30),
|
||||
};
|
||||
@@ -82,7 +95,10 @@ public static class DependencyInjection
|
||||
OnMessageReceived = context =>
|
||||
{
|
||||
var accessToken = context.Request.Query["access_token"];
|
||||
if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs"))
|
||||
if (
|
||||
!string.IsNullOrEmpty(accessToken)
|
||||
&& context.HttpContext.Request.Path.StartsWithSegments("/hubs")
|
||||
)
|
||||
context.Token = accessToken;
|
||||
|
||||
return Task.CompletedTask;
|
||||
@@ -106,7 +122,10 @@ public static class DependencyInjection
|
||||
services.AddSingleton<IXuiConnectionStringBuilder, VmessConnectionStringBuilder>();
|
||||
services.AddSingleton<IXuiConnectionStringBuilder, TrojanConnectionStringBuilder>();
|
||||
services.AddSingleton<IXuiConnectionStringBuilder, ShadowsocksConnectionStringBuilder>();
|
||||
services.AddSingleton<IXuiConnectionStringBuilderResolver, XuiConnectionStringBuilderResolver>();
|
||||
services.AddSingleton<
|
||||
IXuiConnectionStringBuilderResolver,
|
||||
XuiConnectionStringBuilderResolver
|
||||
>();
|
||||
|
||||
services.AddSingleton<ISecretProtector, DataProtectionSecretProtector>();
|
||||
// Singleton: держит кэш per-node XUI-клиентов между запросами (см. XuiPanelGateway).
|
||||
@@ -123,14 +142,18 @@ public static class DependencyInjection
|
||||
services.AddScoped<ICurrentUserSetter>(sp => sp.GetRequiredService<CurrentUser>());
|
||||
services.AddScoped<DbInitializer>();
|
||||
|
||||
services.Configure<TrafficRetentionOptions>(configuration.GetSection(TrafficRetentionOptions.SectionName));
|
||||
services.Configure<TrafficRetentionOptions>(
|
||||
configuration.GetSection(TrafficRetentionOptions.SectionName)
|
||||
);
|
||||
services.AddHostedService<TrafficSyncService>();
|
||||
services.AddHostedService<NodeHealthCheckService>();
|
||||
services.AddHostedService<TrafficRetentionService>();
|
||||
|
||||
services.Configure<TelegramOptions>(configuration.GetSection(TelegramOptions.SectionName));
|
||||
|
||||
services.Configure<FileStorageOptions>(configuration.GetSection(FileStorageOptions.SectionName));
|
||||
services.Configure<FileStorageOptions>(
|
||||
configuration.GetSection(FileStorageOptions.SectionName)
|
||||
);
|
||||
services.AddSingleton<IFileStorage, DiskFileStorage>();
|
||||
|
||||
return services;
|
||||
|
||||
@@ -18,11 +18,8 @@ public class AppRole : IdentityRole<Guid>
|
||||
|
||||
public bool IsSystem { get; set; }
|
||||
|
||||
public AppRole()
|
||||
{
|
||||
}
|
||||
public AppRole() { }
|
||||
|
||||
public AppRole(string name) : base(name)
|
||||
{
|
||||
}
|
||||
public AppRole(string name)
|
||||
: base(name) { }
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ namespace PnvPanel.Infrastructure.Identity;
|
||||
/// В HTTP-запросах читает JWT-claims. В Telegram-боте (нет HttpContext) вызывающая сторона
|
||||
/// заранее задаёт пользователя через ICurrentUserSetter.SetUser(...) в рамках DI-scope апдейта.
|
||||
/// </summary>
|
||||
internal sealed class CurrentUser(IHttpContextAccessor httpContextAccessor) : ICurrentUser, ICurrentUserSetter
|
||||
internal sealed class CurrentUser(IHttpContextAccessor httpContextAccessor)
|
||||
: ICurrentUser,
|
||||
ICurrentUserSetter
|
||||
{
|
||||
private (Guid Id, string Name)? _override;
|
||||
|
||||
@@ -18,7 +20,8 @@ internal sealed class CurrentUser(IHttpContextAccessor httpContextAccessor) : IC
|
||||
|
||||
public string? UserName => _override?.Name ?? Principal?.FindFirstValue(ClaimTypes.Name);
|
||||
|
||||
public bool IsAuthenticated => _override is not null || (Principal?.Identity?.IsAuthenticated ?? false);
|
||||
public bool IsAuthenticated =>
|
||||
_override is not null || (Principal?.Identity?.IsAuthenticated ?? false);
|
||||
|
||||
public void SetUser(Guid userId, string userName) => _override = (userId, userName);
|
||||
|
||||
|
||||
@@ -16,14 +16,28 @@ public sealed class DbInitializer(
|
||||
AppDbContext dbContext,
|
||||
IOptions<AdminSeedOptions> adminSeedOptions,
|
||||
IOptions<RolesOptions> rolesOptions,
|
||||
ILogger<DbInitializer> logger)
|
||||
ILogger<DbInitializer> logger
|
||||
)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true };
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
};
|
||||
|
||||
public async Task SeedAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await EnsureRoleAsync(RoleNames.Admin, AppRole.UnlimitedMaxConfigs, AppRole.UnlimitedMaxIpLimit, isSystem: true);
|
||||
await EnsureRoleAsync(RoleNames.User, rolesOptions.Value.DefaultUserMaxConfigs, rolesOptions.Value.DefaultUserMaxIpLimit, isSystem: true);
|
||||
await EnsureRoleAsync(
|
||||
RoleNames.Admin,
|
||||
AppRole.UnlimitedMaxConfigs,
|
||||
AppRole.UnlimitedMaxIpLimit,
|
||||
isSystem: true
|
||||
);
|
||||
await EnsureRoleAsync(
|
||||
RoleNames.User,
|
||||
rolesOptions.Value.DefaultUserMaxConfigs,
|
||||
rolesOptions.Value.DefaultUserMaxIpLimit,
|
||||
isSystem: true
|
||||
);
|
||||
await SeedAdminAsync();
|
||||
await SeedClientAppsAsync(cancellationToken);
|
||||
}
|
||||
@@ -33,12 +47,18 @@ public sealed class DbInitializer(
|
||||
if (await roleManager.RoleExistsAsync(name))
|
||||
return;
|
||||
|
||||
var role = new AppRole(name) { MaxConfigs = maxConfigs, MaxIpLimit = maxIpLimit, IsSystem = isSystem };
|
||||
var role = new AppRole(name)
|
||||
{
|
||||
MaxConfigs = maxConfigs,
|
||||
MaxIpLimit = maxIpLimit,
|
||||
IsSystem = isSystem,
|
||||
};
|
||||
var result = await roleManager.CreateAsync(role);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to create role '{name}': {string.Join(", ", result.Errors.Select(e => e.Description))}");
|
||||
$"Failed to create role '{name}': {string.Join(", ", result.Errors.Select(e => e.Description))}"
|
||||
);
|
||||
}
|
||||
|
||||
logger.LogInformation("Created system role {RoleName}", name);
|
||||
@@ -47,9 +67,14 @@ public sealed class DbInitializer(
|
||||
private async Task SeedAdminAsync()
|
||||
{
|
||||
var options = adminSeedOptions.Value;
|
||||
if (string.IsNullOrWhiteSpace(options.Username) || string.IsNullOrWhiteSpace(options.Password))
|
||||
if (
|
||||
string.IsNullOrWhiteSpace(options.Username)
|
||||
|| string.IsNullOrWhiteSpace(options.Password)
|
||||
)
|
||||
{
|
||||
logger.LogWarning("AdminSeed__Username/AdminSeed__Password not set — admin account not created");
|
||||
logger.LogWarning(
|
||||
"AdminSeed__Username/AdminSeed__Password not set — admin account not created"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -68,7 +93,8 @@ public sealed class DbInitializer(
|
||||
if (!createResult.Succeeded)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Failed to create admin account: {string.Join(", ", createResult.Errors.Select(e => e.Description))}");
|
||||
$"Failed to create admin account: {string.Join(", ", createResult.Errors.Select(e => e.Description))}"
|
||||
);
|
||||
}
|
||||
|
||||
await userManager.AddToRoleAsync(admin, RoleNames.Admin);
|
||||
@@ -94,12 +120,21 @@ public sealed class DbInitializer(
|
||||
{
|
||||
if (!Enum.TryParse<OsPlatform>(entry.OperatingSystem, ignoreCase: true, out var os))
|
||||
{
|
||||
logger.LogWarning("Unknown OS '{Os}' in client app catalog seed — skipped", entry.OperatingSystem);
|
||||
logger.LogWarning(
|
||||
"Unknown OS '{Os}' in client app catalog seed — skipped",
|
||||
entry.OperatingSystem
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
var app = ClientApp.Create(
|
||||
entry.Name, new Uri(entry.DownloadUrl, UriKind.Absolute), os, entry.Description, iconUrl: null, entry.SortOrder);
|
||||
entry.Name,
|
||||
new Uri(entry.DownloadUrl, UriKind.Absolute),
|
||||
os,
|
||||
entry.Description,
|
||||
iconUrl: null,
|
||||
entry.SortOrder
|
||||
);
|
||||
dbContext.ClientApps.Add(app);
|
||||
}
|
||||
|
||||
@@ -108,5 +143,11 @@ public sealed class DbInitializer(
|
||||
}
|
||||
|
||||
private sealed record ClientAppSeedEntry(
|
||||
string Name, string OperatingSystem, string DownloadUrl, string? Description, int SortOrder, bool IsEnabled);
|
||||
string Name,
|
||||
string OperatingSystem,
|
||||
string DownloadUrl,
|
||||
string? Description,
|
||||
int SortOrder,
|
||||
bool IsEnabled
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,10 @@ namespace PnvPanel.Infrastructure.Identity;
|
||||
|
||||
public static class DbInitializerExtensions
|
||||
{
|
||||
public static async Task SeedDataAsync(this IServiceProvider services, CancellationToken cancellationToken = default)
|
||||
public static async Task SeedDataAsync(
|
||||
this IServiceProvider services,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var initializer = scope.ServiceProvider.GetRequiredService<DbInitializer>();
|
||||
|
||||
@@ -7,10 +7,17 @@ using PnvPanel.Application.Common.Models;
|
||||
|
||||
namespace PnvPanel.Infrastructure.Identity;
|
||||
|
||||
internal sealed class IdentityService(UserManager<AppUser> userManager, SignInManager<AppUser> signInManager, RoleManager<AppRole> roleManager)
|
||||
: IIdentityService
|
||||
internal sealed class IdentityService(
|
||||
UserManager<AppUser> userManager,
|
||||
SignInManager<AppUser> signInManager,
|
||||
RoleManager<AppRole> roleManager
|
||||
) : IIdentityService
|
||||
{
|
||||
public async Task<Result<Guid>> CreateUserAsync(string userName, string password, CancellationToken cancellationToken)
|
||||
public async Task<Result<Guid>> CreateUserAsync(
|
||||
string userName,
|
||||
string password,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = new AppUser
|
||||
{
|
||||
@@ -22,18 +29,27 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
|
||||
if (!createResult.Succeeded)
|
||||
{
|
||||
return createResult.Errors.Any(e => e.Code == nameof(IdentityErrorDescriber.DuplicateUserName))
|
||||
return createResult.Errors.Any(e =>
|
||||
e.Code == nameof(IdentityErrorDescriber.DuplicateUserName)
|
||||
)
|
||||
? Result.Failure<Guid>(AuthErrors.DuplicateUserName)
|
||||
: Result.Failure<Guid>(Error.Validation(
|
||||
"Auth.RegistrationFailed",
|
||||
string.Join("; ", createResult.Errors.Select(e => e.Description))));
|
||||
: Result.Failure<Guid>(
|
||||
Error.Validation(
|
||||
"Auth.RegistrationFailed",
|
||||
string.Join("; ", createResult.Errors.Select(e => e.Description))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
await userManager.AddToRoleAsync(user, RoleNames.User);
|
||||
return Result.Success(user.Id);
|
||||
}
|
||||
|
||||
public async Task<Result<AuthenticatedUser>> ValidateCredentialsAsync(string userName, string password, CancellationToken cancellationToken)
|
||||
public async Task<Result<AuthenticatedUser>> ValidateCredentialsAsync(
|
||||
string userName,
|
||||
string password,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = await userManager.FindByNameAsync(userName);
|
||||
if (user is null)
|
||||
@@ -42,7 +58,11 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
if (user.IsBlocked)
|
||||
return Result.Failure<AuthenticatedUser>(AuthErrors.UserBlocked);
|
||||
|
||||
var checkResult = await signInManager.CheckPasswordSignInAsync(user, password, lockoutOnFailure: true);
|
||||
var checkResult = await signInManager.CheckPasswordSignInAsync(
|
||||
user,
|
||||
password,
|
||||
lockoutOnFailure: true
|
||||
);
|
||||
if (checkResult.IsLockedOut)
|
||||
return Result.Failure<AuthenticatedUser>(AuthErrors.LockedOut);
|
||||
if (!checkResult.Succeeded)
|
||||
@@ -52,7 +72,10 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
return Result.Success(new AuthenticatedUser(user.Id, user.UserName!, roleName));
|
||||
}
|
||||
|
||||
public async Task<CurrentUserProfile?> GetProfileAsync(Guid userId, CancellationToken cancellationToken)
|
||||
public async Task<CurrentUserProfile?> GetProfileAsync(
|
||||
Guid userId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
if (user is null)
|
||||
@@ -60,11 +83,24 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
|
||||
var role = await GetPrimaryRoleAsync(user);
|
||||
return new CurrentUserProfile(
|
||||
user.Id, user.UserName!, role.Id, role.Name!, user.IsActivated, user.IsBlocked, role.MaxConfigs, role.MaxIpLimit,
|
||||
user.SubscriptionToken);
|
||||
user.Id,
|
||||
user.UserName!,
|
||||
role.Id,
|
||||
role.Name!,
|
||||
user.IsActivated,
|
||||
user.IsBlocked,
|
||||
role.MaxConfigs,
|
||||
role.MaxIpLimit,
|
||||
user.SubscriptionToken
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<Result> ChangePasswordAsync(Guid userId, string currentPassword, string newPassword, CancellationToken cancellationToken)
|
||||
public async Task<Result> ChangePasswordAsync(
|
||||
Guid userId,
|
||||
string currentPassword,
|
||||
string newPassword,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
if (user is null)
|
||||
@@ -73,12 +109,19 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
var result = await userManager.ChangePasswordAsync(user, currentPassword, newPassword);
|
||||
return result.Succeeded
|
||||
? Result.Success()
|
||||
: Result.Failure(Error.Validation(
|
||||
"Auth.PasswordChangeFailed",
|
||||
string.Join("; ", result.Errors.Select(e => e.Description))));
|
||||
: Result.Failure(
|
||||
Error.Validation(
|
||||
"Auth.PasswordChangeFailed",
|
||||
string.Join("; ", result.Errors.Select(e => e.Description))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<Result> ChangeUserNameAsync(Guid userId, string newUserName, CancellationToken cancellationToken)
|
||||
public async Task<Result> ChangeUserNameAsync(
|
||||
Guid userId,
|
||||
string newUserName,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
if (user is null)
|
||||
@@ -90,12 +133,19 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
|
||||
return result.Errors.Any(e => e.Code == nameof(IdentityErrorDescriber.DuplicateUserName))
|
||||
? Result.Failure(AuthErrors.DuplicateUserName)
|
||||
: Result.Failure(Error.Validation(
|
||||
"Auth.UserNameChangeFailed",
|
||||
string.Join("; ", result.Errors.Select(e => e.Description))));
|
||||
: Result.Failure(
|
||||
Error.Validation(
|
||||
"Auth.UserNameChangeFailed",
|
||||
string.Join("; ", result.Errors.Select(e => e.Description))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<Result> ActivateUserAsync(Guid userId, Guid activatedBy, CancellationToken cancellationToken)
|
||||
public async Task<Result> ActivateUserAsync(
|
||||
Guid userId,
|
||||
Guid activatedBy,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
if (user is null)
|
||||
@@ -110,13 +160,16 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyDictionary<Guid, string>> GetUserNamesAsync(IReadOnlyCollection<Guid> userIds, CancellationToken cancellationToken)
|
||||
public async Task<IReadOnlyDictionary<Guid, string>> GetUserNamesAsync(
|
||||
IReadOnlyCollection<Guid> userIds,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (userIds.Count == 0)
|
||||
return new Dictionary<Guid, string>();
|
||||
|
||||
return await userManager.Users
|
||||
.Where(u => userIds.Contains(u.Id))
|
||||
return await userManager
|
||||
.Users.Where(u => userIds.Contains(u.Id))
|
||||
.ToDictionaryAsync(u => u.Id, u => u.UserName!, cancellationToken);
|
||||
}
|
||||
|
||||
@@ -129,13 +182,21 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
var result = await userManager.DeleteAsync(user);
|
||||
return result.Succeeded
|
||||
? Result.Success()
|
||||
: Result.Failure(Error.Failure(
|
||||
"Auth.DeleteAccountFailed", string.Join("; ", result.Errors.Select(e => e.Description))));
|
||||
: Result.Failure(
|
||||
Error.Failure(
|
||||
"Auth.DeleteAccountFailed",
|
||||
string.Join("; ", result.Errors.Select(e => e.Description))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<Guid?> FindUserIdBySubscriptionTokenAsync(string token, CancellationToken cancellationToken)
|
||||
public async Task<Guid?> FindUserIdBySubscriptionTokenAsync(
|
||||
string token,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = await userManager.Users.AsNoTracking()
|
||||
var user = await userManager
|
||||
.Users.AsNoTracking()
|
||||
.FirstOrDefaultAsync(u => u.SubscriptionToken == token, cancellationToken);
|
||||
return user?.Id;
|
||||
}
|
||||
@@ -161,7 +222,11 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public async Task<Result> ResetPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken)
|
||||
public async Task<Result> ResetPasswordAsync(
|
||||
Guid userId,
|
||||
string newPassword,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
if (user is null)
|
||||
@@ -172,11 +237,20 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
|
||||
return result.Succeeded
|
||||
? Result.Success()
|
||||
: Result.Failure(Error.Validation(
|
||||
"Auth.PasswordResetFailed", string.Join("; ", result.Errors.Select(e => e.Description))));
|
||||
: Result.Failure(
|
||||
Error.Validation(
|
||||
"Auth.PasswordResetFailed",
|
||||
string.Join("; ", result.Errors.Select(e => e.Description))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<PagedList<UserSummaryDto>> ListUsersAsync(int page, int pageSize, string? search, CancellationToken cancellationToken)
|
||||
public async Task<PagedList<UserSummaryDto>> ListUsersAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
string? search,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var query = userManager.Users.AsNoTracking();
|
||||
if (!string.IsNullOrWhiteSpace(search))
|
||||
@@ -193,7 +267,16 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
foreach (var user in users)
|
||||
{
|
||||
var roleName = await GetPrimaryRoleNameAsync(user);
|
||||
items.Add(new UserSummaryDto(user.Id, user.UserName!, roleName, user.IsActivated, user.IsBlocked, user.ActivatedAt));
|
||||
items.Add(
|
||||
new UserSummaryDto(
|
||||
user.Id,
|
||||
user.UserName!,
|
||||
roleName,
|
||||
user.IsActivated,
|
||||
user.IsBlocked,
|
||||
user.ActivatedAt
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return new PagedList<UserSummaryDto>(items, total, page, pageSize);
|
||||
@@ -206,16 +289,27 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
return new UserStatsDto(total, activated);
|
||||
}
|
||||
|
||||
public async Task<Result> LinkTelegramAsync(Guid userId, long telegramUserId, string? telegramUsername, CancellationToken cancellationToken)
|
||||
public async Task<Result> LinkTelegramAsync(
|
||||
Guid userId,
|
||||
long telegramUserId,
|
||||
string? telegramUsername,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
if (user is null)
|
||||
return Result.Failure(AuthErrors.Unauthorized);
|
||||
|
||||
var alreadyLinked = await userManager.Users.AsNoTracking()
|
||||
var alreadyLinked = await userManager
|
||||
.Users.AsNoTracking()
|
||||
.AnyAsync(u => u.TelegramUserId == telegramUserId && u.Id != userId, cancellationToken);
|
||||
if (alreadyLinked)
|
||||
return Result.Failure(Error.Conflict("Telegram.AlreadyLinked", "Этот Telegram уже привязан к другому аккаунту."));
|
||||
return Result.Failure(
|
||||
Error.Conflict(
|
||||
"Telegram.AlreadyLinked",
|
||||
"Этот Telegram уже привязан к другому аккаунту."
|
||||
)
|
||||
);
|
||||
|
||||
user.TelegramUserId = telegramUserId;
|
||||
user.TelegramUsername = telegramUsername;
|
||||
@@ -237,14 +331,21 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
public async Task<Guid?> FindUserIdByTelegramUserIdAsync(long telegramUserId, CancellationToken cancellationToken)
|
||||
public async Task<Guid?> FindUserIdByTelegramUserIdAsync(
|
||||
long telegramUserId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = await userManager.Users.AsNoTracking()
|
||||
var user = await userManager
|
||||
.Users.AsNoTracking()
|
||||
.FirstOrDefaultAsync(u => u.TelegramUserId == telegramUserId, cancellationToken);
|
||||
return user?.Id;
|
||||
}
|
||||
|
||||
public async Task<TelegramLinkInfo> GetTelegramLinkInfoAsync(Guid userId, CancellationToken cancellationToken)
|
||||
public async Task<TelegramLinkInfo> GetTelegramLinkInfoAsync(
|
||||
Guid userId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
return user?.TelegramUserId is not null
|
||||
@@ -252,9 +353,12 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
: new TelegramLinkInfo(false, null, null);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<long>> GetActivatedLinkedTelegramUserIdsAsync(CancellationToken cancellationToken)
|
||||
public async Task<IReadOnlyCollection<long>> GetActivatedLinkedTelegramUserIdsAsync(
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
return await userManager.Users.AsNoTracking()
|
||||
return await userManager
|
||||
.Users.AsNoTracking()
|
||||
.Where(u => u.TelegramUserId != null && u.IsActivated && !u.IsBlocked)
|
||||
.Select(u => u.TelegramUserId!.Value)
|
||||
.ToListAsync(cancellationToken);
|
||||
@@ -273,5 +377,6 @@ internal sealed class IdentityService(UserManager<AppUser> userManager, SignInMa
|
||||
?? throw new InvalidOperationException($"Роль '{roleName}' не найдена.");
|
||||
}
|
||||
|
||||
private static string GenerateSubscriptionToken() => Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
|
||||
private static string GenerateSubscriptionToken() =>
|
||||
Convert.ToHexString(RandomNumberGenerator.GetBytes(32));
|
||||
}
|
||||
|
||||
@@ -11,7 +11,9 @@ internal sealed class JwtTokenService(IOptions<JwtOptions> options) : IJwtTokenS
|
||||
{
|
||||
private readonly JwtOptions _options = options.Value;
|
||||
|
||||
public (string AccessToken, DateTimeOffset ExpiresAt) GenerateAccessToken(AuthenticatedUser user)
|
||||
public (string AccessToken, DateTimeOffset ExpiresAt) GenerateAccessToken(
|
||||
AuthenticatedUser user
|
||||
)
|
||||
{
|
||||
var expiresAt = DateTimeOffset.UtcNow.AddMinutes(_options.AccessTokenMinutes);
|
||||
|
||||
@@ -32,7 +34,8 @@ internal sealed class JwtTokenService(IOptions<JwtOptions> options) : IJwtTokenS
|
||||
audience: _options.Audience,
|
||||
claims: claims,
|
||||
expires: expiresAt.UtcDateTime,
|
||||
signingCredentials: credentials);
|
||||
signingCredentials: credentials
|
||||
);
|
||||
|
||||
return (new JwtSecurityTokenHandler().WriteToken(token), expiresAt);
|
||||
}
|
||||
|
||||
@@ -9,32 +9,44 @@ using PnvPanel.Infrastructure.Persistence;
|
||||
|
||||
namespace PnvPanel.Infrastructure.Identity;
|
||||
|
||||
internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions<JwtOptions> options) : IRefreshTokenService
|
||||
internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions<JwtOptions> options)
|
||||
: IRefreshTokenService
|
||||
{
|
||||
private readonly JwtOptions _options = options.Value;
|
||||
|
||||
public async Task<IssuedRefreshToken> IssueAsync(Guid userId, CancellationToken cancellationToken)
|
||||
public async Task<IssuedRefreshToken> IssueAsync(
|
||||
Guid userId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var rawToken = GenerateRawToken();
|
||||
var expiresAt = DateTimeOffset.UtcNow.AddDays(_options.RefreshTokenDays);
|
||||
|
||||
dbContext.RefreshTokens.Add(new RefreshToken
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
TokenHash = Hash(rawToken),
|
||||
ExpiresAt = expiresAt,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
dbContext.RefreshTokens.Add(
|
||||
new RefreshToken
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = userId,
|
||||
TokenHash = Hash(rawToken),
|
||||
ExpiresAt = expiresAt,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
}
|
||||
);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
return new IssuedRefreshToken(rawToken, expiresAt);
|
||||
}
|
||||
|
||||
public async Task<Result<RotatedRefreshToken>> RotateAsync(string rawToken, CancellationToken cancellationToken)
|
||||
public async Task<Result<RotatedRefreshToken>> RotateAsync(
|
||||
string rawToken,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var hash = Hash(rawToken);
|
||||
var existing = await dbContext.RefreshTokens.SingleOrDefaultAsync(t => t.TokenHash == hash, cancellationToken);
|
||||
var existing = await dbContext.RefreshTokens.SingleOrDefaultAsync(
|
||||
t => t.TokenHash == hash,
|
||||
cancellationToken
|
||||
);
|
||||
|
||||
if (existing is null)
|
||||
return Result.Failure<RotatedRefreshToken>(AuthErrors.InvalidRefreshToken);
|
||||
@@ -56,14 +68,16 @@ internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions<JwtOp
|
||||
existing.RevokedAt = DateTimeOffset.UtcNow;
|
||||
existing.ReplacedByTokenHash = newHash;
|
||||
|
||||
dbContext.RefreshTokens.Add(new RefreshToken
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = existing.UserId,
|
||||
TokenHash = newHash,
|
||||
ExpiresAt = newExpiresAt,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
});
|
||||
dbContext.RefreshTokens.Add(
|
||||
new RefreshToken
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
UserId = existing.UserId,
|
||||
TokenHash = newHash,
|
||||
ExpiresAt = newExpiresAt,
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
}
|
||||
);
|
||||
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
|
||||
@@ -73,7 +87,10 @@ internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions<JwtOp
|
||||
public async Task RevokeAsync(string rawToken, CancellationToken cancellationToken)
|
||||
{
|
||||
var hash = Hash(rawToken);
|
||||
var existing = await dbContext.RefreshTokens.SingleOrDefaultAsync(t => t.TokenHash == hash, cancellationToken);
|
||||
var existing = await dbContext.RefreshTokens.SingleOrDefaultAsync(
|
||||
t => t.TokenHash == hash,
|
||||
cancellationToken
|
||||
);
|
||||
if (existing is null || existing.RevokedAt is not null)
|
||||
return;
|
||||
|
||||
@@ -83,8 +100,8 @@ internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions<JwtOp
|
||||
|
||||
private async Task RevokeAllForUserAsync(Guid userId, CancellationToken cancellationToken)
|
||||
{
|
||||
var activeTokens = await dbContext.RefreshTokens
|
||||
.Where(t => t.UserId == userId && t.RevokedAt == null)
|
||||
var activeTokens = await dbContext
|
||||
.RefreshTokens.Where(t => t.UserId == userId && t.RevokedAt == null)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
foreach (var token in activeTokens)
|
||||
@@ -93,7 +110,9 @@ internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions<JwtOp
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static string GenerateRawToken() => Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
|
||||
private static string GenerateRawToken() =>
|
||||
Convert.ToBase64String(RandomNumberGenerator.GetBytes(64));
|
||||
|
||||
private static string Hash(string rawToken) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(rawToken)));
|
||||
private static string Hash(string rawToken) =>
|
||||
Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(rawToken)));
|
||||
}
|
||||
|
||||
@@ -7,25 +7,47 @@ using PnvPanel.Application.Common.Models;
|
||||
|
||||
namespace PnvPanel.Infrastructure.Identity;
|
||||
|
||||
internal sealed class RoleService(RoleManager<AppRole> roleManager, UserManager<AppUser> userManager) : IRoleService
|
||||
internal sealed class RoleService(
|
||||
RoleManager<AppRole> roleManager,
|
||||
UserManager<AppUser> userManager
|
||||
) : IRoleService
|
||||
{
|
||||
public async Task<Result<RoleDto>> CreateRoleAsync(string name, int maxConfigs, int maxIpLimit, CancellationToken cancellationToken)
|
||||
public async Task<Result<RoleDto>> CreateRoleAsync(
|
||||
string name,
|
||||
int maxConfigs,
|
||||
int maxIpLimit,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
if (await roleManager.RoleExistsAsync(name))
|
||||
return Result.Failure<RoleDto>(RoleErrors.DuplicateName);
|
||||
|
||||
var role = new AppRole(name) { MaxConfigs = maxConfigs, MaxIpLimit = maxIpLimit, IsSystem = false };
|
||||
var role = new AppRole(name)
|
||||
{
|
||||
MaxConfigs = maxConfigs,
|
||||
MaxIpLimit = maxIpLimit,
|
||||
IsSystem = false,
|
||||
};
|
||||
var result = await roleManager.CreateAsync(role);
|
||||
if (!result.Succeeded)
|
||||
{
|
||||
return Result.Failure<RoleDto>(Error.Validation(
|
||||
"Roles.CreateFailed", string.Join("; ", result.Errors.Select(e => e.Description))));
|
||||
return Result.Failure<RoleDto>(
|
||||
Error.Validation(
|
||||
"Roles.CreateFailed",
|
||||
string.Join("; ", result.Errors.Select(e => e.Description))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return Result.Success(ToDto(role));
|
||||
}
|
||||
|
||||
public async Task<Result<RoleDto>> UpdateRoleAsync(Guid roleId, int maxConfigs, int maxIpLimit, CancellationToken cancellationToken)
|
||||
public async Task<Result<RoleDto>> UpdateRoleAsync(
|
||||
Guid roleId,
|
||||
int maxConfigs,
|
||||
int maxIpLimit,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var role = await roleManager.FindByIdAsync(roleId.ToString());
|
||||
if (role is null)
|
||||
@@ -57,13 +79,17 @@ internal sealed class RoleService(RoleManager<AppRole> roleManager, UserManager<
|
||||
|
||||
public async Task<IReadOnlyList<RoleDto>> ListRolesAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
return await roleManager.Roles
|
||||
.OrderBy(r => r.Name)
|
||||
return await roleManager
|
||||
.Roles.OrderBy(r => r.Name)
|
||||
.Select(r => new RoleDto(r.Id, r.Name!, r.MaxConfigs, r.MaxIpLimit, r.IsSystem))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<Result> ChangeUserRoleAsync(Guid userId, Guid roleId, CancellationToken cancellationToken)
|
||||
public async Task<Result> ChangeUserRoleAsync(
|
||||
Guid userId,
|
||||
Guid roleId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
var user = await userManager.FindByIdAsync(userId.ToString());
|
||||
if (user is null)
|
||||
@@ -81,5 +107,6 @@ internal sealed class RoleService(RoleManager<AppRole> roleManager, UserManager<
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
private static RoleDto ToDto(AppRole role) => new(role.Id, role.Name!, role.MaxConfigs, role.MaxIpLimit, role.IsSystem);
|
||||
private static RoleDto ToDto(AppRole role) =>
|
||||
new(role.Id, role.Name!, role.MaxConfigs, role.MaxIpLimit, role.IsSystem);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,8 @@ namespace PnvPanel.Infrastructure.Persistence;
|
||||
/// (добавляются по мере реализации фич, см. docs/roadmap.md).
|
||||
/// </summary>
|
||||
public class AppDbContext(DbContextOptions<AppDbContext> options)
|
||||
: IdentityDbContext<AppUser, AppRole, Guid>(options), IAppDbContext
|
||||
: IdentityDbContext<AppUser, AppRole, Guid>(options),
|
||||
IAppDbContext
|
||||
{
|
||||
public DbSet<RefreshToken> RefreshTokens => Set<RefreshToken>();
|
||||
|
||||
|
||||
+2
-1
@@ -13,7 +13,8 @@ public class ClientAppConfiguration : IEntityTypeConfiguration<ClientApp>
|
||||
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(100);
|
||||
|
||||
builder.Property(x => x.DownloadUrl)
|
||||
builder
|
||||
.Property(x => x.DownloadUrl)
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasConversion(uri => uri.ToString(), s => new Uri(s, UriKind.Absolute));
|
||||
|
||||
+17
-6
@@ -13,7 +13,8 @@ public class NodeConfiguration : IEntityTypeConfiguration<Node>
|
||||
|
||||
builder.Property(x => x.Name).IsRequired().HasMaxLength(100);
|
||||
|
||||
builder.Property(x => x.BaseAddress)
|
||||
builder
|
||||
.Property(x => x.BaseAddress)
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasConversion(uri => uri.ToString(), s => new Uri(s, UriKind.Absolute));
|
||||
@@ -21,10 +22,20 @@ public class NodeConfiguration : IEntityTypeConfiguration<Node>
|
||||
builder.Property(x => x.Location).HasMaxLength(100);
|
||||
builder.Property(x => x.Status).HasConversion<string>().HasMaxLength(32);
|
||||
|
||||
builder.OwnsOne(x => x.Credentials, credentials =>
|
||||
{
|
||||
credentials.Property(c => c.Username).HasColumnName("CredentialsUsername").IsRequired().HasMaxLength(200);
|
||||
credentials.Property(c => c.ProtectedPassword).HasColumnName("CredentialsProtectedPassword").IsRequired();
|
||||
});
|
||||
builder.OwnsOne(
|
||||
x => x.Credentials,
|
||||
credentials =>
|
||||
{
|
||||
credentials
|
||||
.Property(c => c.Username)
|
||||
.HasColumnName("CredentialsUsername")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200);
|
||||
credentials
|
||||
.Property(c => c.ProtectedPassword)
|
||||
.HasColumnName("CredentialsProtectedPassword")
|
||||
.IsRequired();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -11,9 +11,7 @@ public class RefreshTokenConfiguration : IEntityTypeConfiguration<RefreshToken>
|
||||
builder.ToTable("RefreshTokens");
|
||||
builder.HasKey(x => x.Id);
|
||||
|
||||
builder.Property(x => x.TokenHash)
|
||||
.IsRequired()
|
||||
.HasMaxLength(128);
|
||||
builder.Property(x => x.TokenHash).IsRequired().HasMaxLength(128);
|
||||
|
||||
builder.HasIndex(x => x.TokenHash).IsUnique();
|
||||
builder.HasIndex(x => x.UserId);
|
||||
|
||||
@@ -8,7 +8,10 @@ namespace PnvPanel.Infrastructure.Persistence;
|
||||
/// </summary>
|
||||
public static class MigrationExtensions
|
||||
{
|
||||
public static async Task ApplyMigrationsAsync(this IServiceProvider services, CancellationToken cancellationToken = default)
|
||||
public static async Task ApplyMigrationsAsync(
|
||||
this IServiceProvider services,
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
{
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
|
||||
+2
-8
@@ -8,15 +8,9 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
public partial class InitialCreate : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
protected override void Up(MigrationBuilder migrationBuilder) { }
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
protected override void Down(MigrationBuilder migrationBuilder) { }
|
||||
}
|
||||
}
|
||||
|
||||
+142
-65
@@ -19,14 +19,23 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
MaxConfigs = table.Column<int>(type: "integer", nullable: false),
|
||||
IsSystem = table.Column<bool>(type: "boolean", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true)
|
||||
Name = table.Column<string>(
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true
|
||||
),
|
||||
NormalizedName = table.Column<string>(
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true
|
||||
),
|
||||
ConcurrencyStamp = table.Column<string>(type: "text", nullable: true),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetRoles", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUsers",
|
||||
@@ -34,12 +43,31 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
IsActivated = table.Column<bool>(type: "boolean", nullable: false),
|
||||
ActivatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
ActivatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
),
|
||||
ActivatedBy = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
UserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedUserName = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
Email = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
NormalizedEmail = table.Column<string>(type: "character varying(256)", maxLength: 256, nullable: true),
|
||||
UserName = table.Column<string>(
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true
|
||||
),
|
||||
NormalizedUserName = table.Column<string>(
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true
|
||||
),
|
||||
Email = table.Column<string>(
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true
|
||||
),
|
||||
NormalizedEmail = table.Column<string>(
|
||||
type: "character varying(256)",
|
||||
maxLength: 256,
|
||||
nullable: true
|
||||
),
|
||||
EmailConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
PasswordHash = table.Column<string>(type: "text", nullable: true),
|
||||
SecurityStamp = table.Column<string>(type: "text", nullable: true),
|
||||
@@ -47,14 +75,18 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
PhoneNumber = table.Column<string>(type: "text", nullable: true),
|
||||
PhoneNumberConfirmed = table.Column<bool>(type: "boolean", nullable: false),
|
||||
TwoFactorEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
LockoutEnd = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
),
|
||||
LockoutEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
AccessFailedCount = table.Column<int>(type: "integer", nullable: false)
|
||||
AccessFailedCount = table.Column<int>(type: "integer", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUsers", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "RefreshTokens",
|
||||
@@ -62,26 +94,44 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TokenHash = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
ExpiresAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
RevokedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
ReplacedByTokenHash = table.Column<string>(type: "text", nullable: true)
|
||||
TokenHash = table.Column<string>(
|
||||
type: "character varying(128)",
|
||||
maxLength: 128,
|
||||
nullable: false
|
||||
),
|
||||
ExpiresAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
CreatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
RevokedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
),
|
||||
ReplacedByTokenHash = table.Column<string>(type: "text", nullable: true),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_RefreshTokens", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetRoleClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Id = table
|
||||
.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation(
|
||||
"Npgsql:ValueGenerationStrategy",
|
||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
|
||||
),
|
||||
RoleId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -91,18 +141,24 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserClaims",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Id = table
|
||||
.Column<int>(type: "integer", nullable: false)
|
||||
.Annotation(
|
||||
"Npgsql:ValueGenerationStrategy",
|
||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
|
||||
),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ClaimType = table.Column<string>(type: "text", nullable: true),
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true)
|
||||
ClaimValue = table.Column<string>(type: "text", nullable: true),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -112,8 +168,10 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserLogins",
|
||||
@@ -122,25 +180,30 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||
ProviderKey = table.Column<string>(type: "text", nullable: false),
|
||||
ProviderDisplayName = table.Column<string>(type: "text", nullable: true),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey });
|
||||
table.PrimaryKey(
|
||||
"PK_AspNetUserLogins",
|
||||
x => new { x.LoginProvider, x.ProviderKey }
|
||||
);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserLogins_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserRoles",
|
||||
columns: table => new
|
||||
{
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
RoleId = table.Column<Guid>(type: "uuid", nullable: false)
|
||||
RoleId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
@@ -150,14 +213,17 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
column: x => x.RoleId,
|
||||
principalTable: "AspNetRoles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserRoles_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AspNetUserTokens",
|
||||
@@ -166,94 +232,105 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
LoginProvider = table.Column<string>(type: "text", nullable: false),
|
||||
Name = table.Column<string>(type: "text", nullable: false),
|
||||
Value = table.Column<string>(type: "text", nullable: true)
|
||||
Value = table.Column<string>(type: "text", nullable: true),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name });
|
||||
table.PrimaryKey(
|
||||
"PK_AspNetUserTokens",
|
||||
x => new
|
||||
{
|
||||
x.UserId,
|
||||
x.LoginProvider,
|
||||
x.Name,
|
||||
}
|
||||
);
|
||||
table.ForeignKey(
|
||||
name: "FK_AspNetUserTokens_AspNetUsers_UserId",
|
||||
column: x => x.UserId,
|
||||
principalTable: "AspNetUsers",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Cascade);
|
||||
});
|
||||
onDelete: ReferentialAction.Cascade
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetRoleClaims_RoleId",
|
||||
table: "AspNetRoleClaims",
|
||||
column: "RoleId");
|
||||
column: "RoleId"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "RoleNameIndex",
|
||||
table: "AspNetRoles",
|
||||
column: "NormalizedName",
|
||||
unique: true);
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserClaims_UserId",
|
||||
table: "AspNetUserClaims",
|
||||
column: "UserId");
|
||||
column: "UserId"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserLogins_UserId",
|
||||
table: "AspNetUserLogins",
|
||||
column: "UserId");
|
||||
column: "UserId"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUserRoles_RoleId",
|
||||
table: "AspNetUserRoles",
|
||||
column: "RoleId");
|
||||
column: "RoleId"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "EmailIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedEmail");
|
||||
column: "NormalizedEmail"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UserNameIndex",
|
||||
table: "AspNetUsers",
|
||||
column: "NormalizedUserName",
|
||||
unique: true);
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RefreshTokens_TokenHash",
|
||||
table: "RefreshTokens",
|
||||
column: "TokenHash",
|
||||
unique: true);
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_RefreshTokens_UserId",
|
||||
table: "RefreshTokens",
|
||||
column: "UserId");
|
||||
column: "UserId"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoleClaims");
|
||||
migrationBuilder.DropTable(name: "AspNetRoleClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserClaims");
|
||||
migrationBuilder.DropTable(name: "AspNetUserClaims");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserLogins");
|
||||
migrationBuilder.DropTable(name: "AspNetUserLogins");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserRoles");
|
||||
migrationBuilder.DropTable(name: "AspNetUserRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUserTokens");
|
||||
migrationBuilder.DropTable(name: "AspNetUserTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "RefreshTokens");
|
||||
migrationBuilder.DropTable(name: "RefreshTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetRoles");
|
||||
migrationBuilder.DropTable(name: "AspNetRoles");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "AspNetUsers");
|
||||
migrationBuilder.DropTable(name: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+28
-9
@@ -17,29 +17,48 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Comment = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
Status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Comment = table.Column<string>(
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: true
|
||||
),
|
||||
Status = table.Column<string>(
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false
|
||||
),
|
||||
DecidedBy = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
DecidedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
RejectionReason = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
DecidedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
),
|
||||
RejectionReason = table.Column<string>(
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: true
|
||||
),
|
||||
CreatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ActivationRequests", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_ActivationRequests_UserId_Status",
|
||||
table: "ActivationRequests",
|
||||
columns: new[] { "UserId", "Status" });
|
||||
columns: new[] { "UserId", "Status" }
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ActivationRequests");
|
||||
migrationBuilder.DropTable(name: "ActivationRequests");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+69
-20
@@ -17,56 +17,105 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
NodeId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
RemoteInboundId = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Protocol = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Remark = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
RemoteInboundId = table.Column<string>(
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false
|
||||
),
|
||||
Protocol = table.Column<string>(
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false
|
||||
),
|
||||
Remark = table.Column<string>(
|
||||
type: "character varying(200)",
|
||||
maxLength: 200,
|
||||
nullable: false
|
||||
),
|
||||
Port = table.Column<int>(type: "integer", nullable: false),
|
||||
IsPublished = table.Column<bool>(type: "boolean", nullable: false),
|
||||
DisplayName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
DisplayName = table.Column<string>(
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true
|
||||
),
|
||||
MaxClients = table.Column<int>(type: "integer", nullable: true),
|
||||
AllowedRoleIds = table.Column<Guid[]>(type: "uuid[]", nullable: false),
|
||||
LastSyncAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
LastSyncAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Inbounds", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Nodes",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
BaseAddress = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
CredentialsUsername = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
CredentialsProtectedPassword = table.Column<string>(type: "text", nullable: false),
|
||||
Location = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
Status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Name = table.Column<string>(
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: false
|
||||
),
|
||||
BaseAddress = table.Column<string>(
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: false
|
||||
),
|
||||
CredentialsUsername = table.Column<string>(
|
||||
type: "character varying(200)",
|
||||
maxLength: 200,
|
||||
nullable: false
|
||||
),
|
||||
CredentialsProtectedPassword = table.Column<string>(
|
||||
type: "text",
|
||||
nullable: false
|
||||
),
|
||||
Location = table.Column<string>(
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true
|
||||
),
|
||||
Status = table.Column<string>(
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false
|
||||
),
|
||||
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
LastSyncAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
LastSyncAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
),
|
||||
CreatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Nodes", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Inbounds_NodeId_RemoteInboundId",
|
||||
table: "Inbounds",
|
||||
columns: new[] { "NodeId", "RemoteInboundId" },
|
||||
unique: true);
|
||||
unique: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "Inbounds");
|
||||
migrationBuilder.DropTable(name: "Inbounds");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Nodes");
|
||||
migrationBuilder.DropTable(name: "Nodes");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+83
-28
@@ -16,25 +16,47 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
table: "AspNetUsers",
|
||||
type: "text",
|
||||
nullable: false,
|
||||
defaultValue: "");
|
||||
defaultValue: ""
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "ClientApps",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Name = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
DownloadUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: false),
|
||||
OperatingSystem = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Description = table.Column<string>(type: "character varying(300)", maxLength: 300, nullable: true),
|
||||
IconUrl = table.Column<string>(type: "character varying(500)", maxLength: 500, nullable: true),
|
||||
Name = table.Column<string>(
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: false
|
||||
),
|
||||
DownloadUrl = table.Column<string>(
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: false
|
||||
),
|
||||
OperatingSystem = table.Column<string>(
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false
|
||||
),
|
||||
Description = table.Column<string>(
|
||||
type: "character varying(300)",
|
||||
maxLength: 300,
|
||||
nullable: true
|
||||
),
|
||||
IconUrl = table.Column<string>(
|
||||
type: "character varying(500)",
|
||||
maxLength: 500,
|
||||
nullable: true
|
||||
),
|
||||
SortOrder = table.Column<int>(type: "integer", nullable: false),
|
||||
IsEnabled = table.Column<bool>(type: "boolean", nullable: false)
|
||||
IsEnabled = table.Column<bool>(type: "boolean", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_ClientApps", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "VpnConfigs",
|
||||
@@ -43,53 +65,86 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
InboundId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Label = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
ClientEmail = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
ClientExternalId = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Protocol = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Label = table.Column<string>(
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true
|
||||
),
|
||||
ClientEmail = table.Column<string>(
|
||||
type: "character varying(200)",
|
||||
maxLength: 200,
|
||||
nullable: false
|
||||
),
|
||||
ClientExternalId = table.Column<string>(
|
||||
type: "character varying(200)",
|
||||
maxLength: 200,
|
||||
nullable: false
|
||||
),
|
||||
Protocol = table.Column<string>(
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false
|
||||
),
|
||||
DeviceLimit = table.Column<int>(type: "integer", nullable: false),
|
||||
UsedUpBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||
UsedDownBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||
ExpiresAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
Status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
SubscriptionToken = table.Column<string>(type: "character varying(128)", maxLength: 128, nullable: false),
|
||||
LastSyncAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
ExpiresAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
),
|
||||
Status = table.Column<string>(
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false
|
||||
),
|
||||
SubscriptionToken = table.Column<string>(
|
||||
type: "character varying(128)",
|
||||
maxLength: 128,
|
||||
nullable: false
|
||||
),
|
||||
LastSyncAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
),
|
||||
CreatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_VpnConfigs", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VpnConfigs_InboundId",
|
||||
table: "VpnConfigs",
|
||||
column: "InboundId");
|
||||
column: "InboundId"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VpnConfigs_SubscriptionToken",
|
||||
table: "VpnConfigs",
|
||||
column: "SubscriptionToken",
|
||||
unique: true);
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VpnConfigs_UserId_Status",
|
||||
table: "VpnConfigs",
|
||||
columns: new[] { "UserId", "Status" });
|
||||
columns: new[] { "UserId", "Status" }
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "ClientApps");
|
||||
migrationBuilder.DropTable(name: "ClientApps");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "VpnConfigs");
|
||||
migrationBuilder.DropTable(name: "VpnConfigs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "SubscriptionToken",
|
||||
table: "AspNetUsers");
|
||||
migrationBuilder.DropColumn(name: "SubscriptionToken", table: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-8
@@ -16,29 +16,37 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
name: "TrafficSamples",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Id = table
|
||||
.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation(
|
||||
"Npgsql:ValueGenerationStrategy",
|
||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
|
||||
),
|
||||
ConfigId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Timestamp = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
Timestamp = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
UpBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||
DownBytes = table.Column<long>(type: "bigint", nullable: false)
|
||||
DownBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TrafficSamples", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TrafficSamples_ConfigId_Timestamp",
|
||||
table: "TrafficSamples",
|
||||
columns: new[] { "ConfigId", "Timestamp" });
|
||||
columns: new[] { "ConfigId", "Timestamp" }
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "TrafficSamples");
|
||||
migrationBuilder.DropTable(name: "TrafficSamples");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-15
@@ -17,42 +17,65 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
table: "AspNetUsers",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: false);
|
||||
defaultValue: false
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "AuditLogs",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
|
||||
Id = table
|
||||
.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation(
|
||||
"Npgsql:ValueGenerationStrategy",
|
||||
NpgsqlValueGenerationStrategy.IdentityByDefaultColumn
|
||||
),
|
||||
ActorId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Action = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
TargetType = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||
TargetId = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
Action = table.Column<string>(
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: false
|
||||
),
|
||||
TargetType = table.Column<string>(
|
||||
type: "character varying(50)",
|
||||
maxLength: 50,
|
||||
nullable: false
|
||||
),
|
||||
TargetId = table.Column<string>(
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: false
|
||||
),
|
||||
Metadata = table.Column<string>(type: "jsonb", nullable: true),
|
||||
Source = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
Source = table.Column<string>(
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false
|
||||
),
|
||||
CreatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_AuditLogs", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AuditLogs_CreatedAt",
|
||||
table: "AuditLogs",
|
||||
column: "CreatedAt");
|
||||
column: "CreatedAt"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "AuditLogs");
|
||||
migrationBuilder.DropTable(name: "AuditLogs");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "IsBlocked",
|
||||
table: "AspNetUsers");
|
||||
migrationBuilder.DropColumn(name: "IsBlocked", table: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+55
-32
@@ -15,98 +15,121 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
name: "TelegramLinkedAt",
|
||||
table: "AspNetUsers",
|
||||
type: "timestamp with time zone",
|
||||
nullable: true);
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<long>(
|
||||
name: "TelegramUserId",
|
||||
table: "AspNetUsers",
|
||||
type: "bigint",
|
||||
nullable: true);
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "TelegramUsername",
|
||||
table: "AspNetUsers",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
nullable: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TelegramLinkTokens",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Token = table.Column<string>(type: "character varying(64)", maxLength: 64, nullable: false),
|
||||
Token = table.Column<string>(
|
||||
type: "character varying(64)",
|
||||
maxLength: 64,
|
||||
nullable: false
|
||||
),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
ExpiresAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
ConsumedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
ExpiresAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
ConsumedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TelegramLinkTokens", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TelegramLoginRequests",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Status = table.Column<string>(
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false
|
||||
),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Context = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
ExpiresAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
Context = table.Column<string>(
|
||||
type: "character varying(200)",
|
||||
maxLength: 200,
|
||||
nullable: true
|
||||
),
|
||||
CreatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
ExpiresAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TelegramLoginRequests", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUsers_SubscriptionToken",
|
||||
table: "AspNetUsers",
|
||||
column: "SubscriptionToken",
|
||||
unique: true);
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_AspNetUsers_TelegramUserId",
|
||||
table: "AspNetUsers",
|
||||
column: "TelegramUserId",
|
||||
unique: true);
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TelegramLinkTokens_Token",
|
||||
table: "TelegramLinkTokens",
|
||||
column: "Token",
|
||||
unique: true);
|
||||
unique: true
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "TelegramLinkTokens");
|
||||
migrationBuilder.DropTable(name: "TelegramLinkTokens");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TelegramLoginRequests");
|
||||
migrationBuilder.DropTable(name: "TelegramLoginRequests");
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AspNetUsers_SubscriptionToken",
|
||||
table: "AspNetUsers");
|
||||
table: "AspNetUsers"
|
||||
);
|
||||
|
||||
migrationBuilder.DropIndex(
|
||||
name: "IX_AspNetUsers_TelegramUserId",
|
||||
table: "AspNetUsers");
|
||||
migrationBuilder.DropIndex(name: "IX_AspNetUsers_TelegramUserId", table: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TelegramLinkedAt",
|
||||
table: "AspNetUsers");
|
||||
migrationBuilder.DropColumn(name: "TelegramLinkedAt", table: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TelegramUserId",
|
||||
table: "AspNetUsers");
|
||||
migrationBuilder.DropColumn(name: "TelegramUserId", table: "AspNetUsers");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "TelegramUsername",
|
||||
table: "AspNetUsers");
|
||||
migrationBuilder.DropColumn(name: "TelegramUsername", table: "AspNetUsers");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-4
@@ -10,9 +10,7 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeviceLimit",
|
||||
table: "VpnConfigs");
|
||||
migrationBuilder.DropColumn(name: "DeviceLimit", table: "VpnConfigs");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -23,7 +21,8 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
table: "VpnConfigs",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
defaultValue: 0
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+23
-8
@@ -16,27 +16,42 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Title = table.Column<string>(type: "character varying(200)", maxLength: 200, nullable: false),
|
||||
Body = table.Column<string>(type: "character varying(20000)", maxLength: 20000, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: true)
|
||||
Title = table.Column<string>(
|
||||
type: "character varying(200)",
|
||||
maxLength: 200,
|
||||
nullable: false
|
||||
),
|
||||
Body = table.Column<string>(
|
||||
type: "character varying(20000)",
|
||||
maxLength: 20000,
|
||||
nullable: false
|
||||
),
|
||||
CreatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
UpdatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: true
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NewsPosts", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NewsPosts_CreatedAt",
|
||||
table: "NewsPosts",
|
||||
column: "CreatedAt");
|
||||
column: "CreatedAt"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "NewsPosts");
|
||||
migrationBuilder.DropTable(name: "NewsPosts");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
-5
@@ -18,17 +18,18 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
table: "AspNetRoles",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: -1);
|
||||
defaultValue: -1
|
||||
);
|
||||
|
||||
migrationBuilder.Sql("UPDATE \"AspNetRoles\" SET \"MaxIpLimit\" = 2 WHERE \"Name\" = 'user';");
|
||||
migrationBuilder.Sql(
|
||||
"UPDATE \"AspNetRoles\" SET \"MaxIpLimit\" = 2 WHERE \"Name\" = 'user';"
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "MaxIpLimit",
|
||||
table: "AspNetRoles");
|
||||
migrationBuilder.DropColumn(name: "MaxIpLimit", table: "AspNetRoles");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+66
-24
@@ -17,18 +17,34 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
UserId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Type = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Status = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
|
||||
Type = table.Column<string>(
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false
|
||||
),
|
||||
Status = table.Column<string>(
|
||||
type: "character varying(32)",
|
||||
maxLength: 32,
|
||||
nullable: false
|
||||
),
|
||||
RequestedRoleId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
ProposedRoleName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: true),
|
||||
ProposedRoleName = table.Column<string>(
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: true
|
||||
),
|
||||
ProposedMaxConfigs = table.Column<int>(type: "integer", nullable: true),
|
||||
ProposedMaxIpLimit = table.Column<int>(type: "integer", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
CreatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_SupportTickets", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TicketAttachments",
|
||||
@@ -36,16 +52,32 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
CommentId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
FileName = table.Column<string>(type: "character varying(255)", maxLength: 255, nullable: false),
|
||||
StoredFileName = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
ContentType = table.Column<string>(type: "character varying(100)", maxLength: 100, nullable: false),
|
||||
FileName = table.Column<string>(
|
||||
type: "character varying(255)",
|
||||
maxLength: 255,
|
||||
nullable: false
|
||||
),
|
||||
StoredFileName = table.Column<string>(
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: false
|
||||
),
|
||||
ContentType = table.Column<string>(
|
||||
type: "character varying(100)",
|
||||
maxLength: 100,
|
||||
nullable: false
|
||||
),
|
||||
SizeBytes = table.Column<long>(type: "bigint", nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
CreatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TicketAttachments", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "TicketComments",
|
||||
@@ -54,52 +86,62 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
TicketId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
AuthorId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
Body = table.Column<string>(type: "character varying(4000)", maxLength: 4000, nullable: false),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
|
||||
Body = table.Column<string>(
|
||||
type: "character varying(4000)",
|
||||
maxLength: 4000,
|
||||
nullable: false
|
||||
),
|
||||
CreatedAt = table.Column<DateTimeOffset>(
|
||||
type: "timestamp with time zone",
|
||||
nullable: false
|
||||
),
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_TicketComments", x => x.Id);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SupportTickets_Type_Status",
|
||||
table: "SupportTickets",
|
||||
columns: new[] { "Type", "Status" });
|
||||
columns: new[] { "Type", "Status" }
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_SupportTickets_UserId_Status",
|
||||
table: "SupportTickets",
|
||||
columns: new[] { "UserId", "Status" });
|
||||
columns: new[] { "UserId", "Status" }
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TicketAttachments_CommentId",
|
||||
table: "TicketAttachments",
|
||||
column: "CommentId");
|
||||
column: "CommentId"
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TicketAttachments_StoredFileName",
|
||||
table: "TicketAttachments",
|
||||
column: "StoredFileName",
|
||||
unique: true);
|
||||
unique: true
|
||||
);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_TicketComments_TicketId_CreatedAt",
|
||||
table: "TicketComments",
|
||||
columns: new[] { "TicketId", "CreatedAt" });
|
||||
columns: new[] { "TicketId", "CreatedAt" }
|
||||
);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "SupportTickets");
|
||||
migrationBuilder.DropTable(name: "SupportTickets");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TicketAttachments");
|
||||
migrationBuilder.DropTable(name: "TicketAttachments");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "TicketComments");
|
||||
migrationBuilder.DropTable(name: "TicketComments");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\PnvPanel.Application\PnvPanel.Application.csproj" />
|
||||
</ItemGroup>
|
||||
@@ -22,5 +21,4 @@
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -21,8 +21,8 @@ internal sealed class XuiPanelGateway(
|
||||
IXuiHttpClientFactory httpClientFactory,
|
||||
IXuiConnectionStringBuilderResolver connectionStringResolver,
|
||||
ISecretProtector secretProtector,
|
||||
ILoggerFactory loggerFactory)
|
||||
: IXuiPanelGateway, IDisposable
|
||||
ILoggerFactory loggerFactory
|
||||
) : IXuiPanelGateway, IDisposable
|
||||
{
|
||||
private readonly ConcurrentDictionary<Guid, Lazy<IXuiClient>> _clients = new();
|
||||
|
||||
@@ -30,7 +30,12 @@ internal sealed class XuiPanelGateway(
|
||||
{
|
||||
return XuiBaseUrlValidator.IsAllowed(baseAddress.ToString(), out var reason)
|
||||
? Result.Success()
|
||||
: Result.Failure(Error.Validation("Nodes.BaseAddressNotAllowed", reason ?? "Адрес панели не разрешён."));
|
||||
: Result.Failure(
|
||||
Error.Validation(
|
||||
"Nodes.BaseAddressNotAllowed",
|
||||
reason ?? "Адрес панели не разрешён."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public async Task<NodeProbeResult> ProbeAsync(Node node, CancellationToken cancellationToken)
|
||||
@@ -47,7 +52,10 @@ internal sealed class XuiPanelGateway(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<IReadOnlyList<RemoteInboundInfo>>> ListInboundsAsync(Node node, CancellationToken cancellationToken)
|
||||
public async Task<Result<IReadOnlyList<RemoteInboundInfo>>> ListInboundsAsync(
|
||||
Node node,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -57,7 +65,12 @@ internal sealed class XuiPanelGateway(
|
||||
var mapped = remoteInbounds
|
||||
.Select(i => (Summary: i, Protocol: TryParseProtocol(i.Protocol)))
|
||||
.Where(x => x.Protocol is not null)
|
||||
.Select(x => new RemoteInboundInfo(x.Summary.ExternalId, x.Protocol!.Value, x.Summary.Remark, x.Summary.Port))
|
||||
.Select(x => new RemoteInboundInfo(
|
||||
x.Summary.ExternalId,
|
||||
x.Protocol!.Value,
|
||||
x.Summary.Remark,
|
||||
x.Summary.Port
|
||||
))
|
||||
.ToList();
|
||||
|
||||
return Result.Success<IReadOnlyList<RemoteInboundInfo>>(mapped);
|
||||
@@ -65,13 +78,20 @@ internal sealed class XuiPanelGateway(
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure<IReadOnlyList<RemoteInboundInfo>>(
|
||||
Error.Failure("Xui.Unreachable", $"Нода недоступна: {ex.Message}"));
|
||||
Error.Failure("Xui.Unreachable", $"Нода недоступна: {ex.Message}")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> AddClientAsync(
|
||||
Node node, string inboundRemoteId, VpnProtocol protocol, string clientEmail, string clientName, int limitIp,
|
||||
CancellationToken cancellationToken)
|
||||
Node node,
|
||||
string inboundRemoteId,
|
||||
VpnProtocol protocol,
|
||||
string clientEmail,
|
||||
string clientName,
|
||||
int limitIp,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -79,83 +99,146 @@ internal sealed class XuiPanelGateway(
|
||||
// Наш domain-sentinel RoleQuota.Unlimited (-1) переводим в нативное "без лимита" 3x-ui (0) —
|
||||
// ThreeXui.Net.AddClientRequest.LimitIp обязателен (не nullable), 0 в самом 3x-ui означает unlimited.
|
||||
var request = new AddClientRequest(
|
||||
clientName, clientEmail, ToRemoteProtocol(protocol), limitIp == RoleQuota.Unlimited ? 0 : limitIp, null);
|
||||
clientName,
|
||||
clientEmail,
|
||||
ToRemoteProtocol(protocol),
|
||||
limitIp == RoleQuota.Unlimited ? 0 : limitIp,
|
||||
null
|
||||
);
|
||||
var result = await client.AddClientAsync(inboundRemoteId, request, cancellationToken);
|
||||
return Result.Success(result.ExternalClientId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure<string>(Error.Failure("Xui.AddClientFailed", $"Не удалось создать клиента: {ex.Message}"));
|
||||
return Result.Failure<string>(
|
||||
Error.Failure("Xui.AddClientFailed", $"Не удалось создать клиента: {ex.Message}")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> RemoveClientAsync(
|
||||
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol, CancellationToken cancellationToken)
|
||||
Node node,
|
||||
string inboundRemoteId,
|
||||
string clientExternalId,
|
||||
VpnProtocol protocol,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = GetClient(node);
|
||||
await client.RemoveClientAsync(inboundRemoteId, clientExternalId, ToRemoteProtocol(protocol), cancellationToken);
|
||||
await client.RemoveClientAsync(
|
||||
inboundRemoteId,
|
||||
clientExternalId,
|
||||
ToRemoteProtocol(protocol),
|
||||
cancellationToken
|
||||
);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure(Error.Failure("Xui.RemoveClientFailed", $"Не удалось удалить клиента: {ex.Message}"));
|
||||
return Result.Failure(
|
||||
Error.Failure("Xui.RemoveClientFailed", $"Не удалось удалить клиента: {ex.Message}")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result> UpdateClientAsync(
|
||||
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol,
|
||||
string name, bool enable, CancellationToken cancellationToken)
|
||||
Node node,
|
||||
string inboundRemoteId,
|
||||
string clientExternalId,
|
||||
VpnProtocol protocol,
|
||||
string name,
|
||||
bool enable,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = GetClient(node);
|
||||
// deviceLimit: null — не трогаем то, что уже стоит на клиенте в панели (см. AddClientAsync).
|
||||
var request = new UpdateClientRequest(null, null, enable, name);
|
||||
await client.UpdateClientAsync(inboundRemoteId, clientExternalId, ToRemoteProtocol(protocol), request, cancellationToken);
|
||||
await client.UpdateClientAsync(
|
||||
inboundRemoteId,
|
||||
clientExternalId,
|
||||
ToRemoteProtocol(protocol),
|
||||
request,
|
||||
cancellationToken
|
||||
);
|
||||
return Result.Success();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure(Error.Failure("Xui.UpdateClientFailed", $"Не удалось изменить клиента: {ex.Message}"));
|
||||
return Result.Failure(
|
||||
Error.Failure(
|
||||
"Xui.UpdateClientFailed",
|
||||
$"Не удалось изменить клиента: {ex.Message}"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<string>> BuildConnectionStringAsync(
|
||||
Node node, Inbound inbound, string clientExternalId, string clientName, string publicHost,
|
||||
CancellationToken cancellationToken)
|
||||
Node node,
|
||||
Inbound inbound,
|
||||
string clientExternalId,
|
||||
string clientName,
|
||||
string publicHost,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = GetClient(node);
|
||||
var remoteInbound = await client.GetInboundAsync(inbound.RemoteInboundId, cancellationToken);
|
||||
var remoteInbound = await client.GetInboundAsync(
|
||||
inbound.RemoteInboundId,
|
||||
cancellationToken
|
||||
);
|
||||
if (remoteInbound is null)
|
||||
{
|
||||
return Result.Failure<string>(
|
||||
Error.Failure("Xui.ConnectionStringFailed", "Inbound не найден на панели."));
|
||||
Error.Failure("Xui.ConnectionStringFailed", "Inbound не найден на панели.")
|
||||
);
|
||||
}
|
||||
|
||||
var builder = connectionStringResolver.Resolve(ToRemoteProtocol(inbound.Protocol));
|
||||
if (builder is null)
|
||||
{
|
||||
return Result.Failure<string>(
|
||||
Error.Failure("Xui.ConnectionStringFailed", $"Протокол {inbound.Protocol} не поддерживается."));
|
||||
Error.Failure(
|
||||
"Xui.ConnectionStringFailed",
|
||||
$"Протокол {inbound.Protocol} не поддерживается."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
var request = new XuiConnectionStringRequest(
|
||||
clientExternalId, clientName, inbound.Port, publicHost, node.BaseAddress.ToString(), remoteInbound);
|
||||
clientExternalId,
|
||||
clientName,
|
||||
inbound.Port,
|
||||
publicHost,
|
||||
node.BaseAddress.ToString(),
|
||||
remoteInbound
|
||||
);
|
||||
|
||||
return Result.Success(builder.Build(request));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure<string>(Error.Failure("Xui.ConnectionStringFailed", $"Не удалось построить ссылку: {ex.Message}"));
|
||||
return Result.Failure<string>(
|
||||
Error.Failure(
|
||||
"Xui.ConnectionStringFailed",
|
||||
$"Не удалось построить ссылку: {ex.Message}"
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Result<IReadOnlyDictionary<string, ClientTrafficInfo>>> GetClientTrafficAsync(
|
||||
Node node, string inboundRemoteId, CancellationToken cancellationToken)
|
||||
Node node,
|
||||
string inboundRemoteId,
|
||||
CancellationToken cancellationToken
|
||||
)
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -164,7 +247,8 @@ internal sealed class XuiPanelGateway(
|
||||
if (remoteInbound is null)
|
||||
{
|
||||
return Result.Failure<IReadOnlyDictionary<string, ClientTrafficInfo>>(
|
||||
Error.Failure("Xui.InboundNotFound", "Inbound не найден на панели."));
|
||||
Error.Failure("Xui.InboundNotFound", "Inbound не найден на панели.")
|
||||
);
|
||||
}
|
||||
|
||||
return Result.Success(ParseClientStats(remoteInbound.RawInboundJson));
|
||||
@@ -172,7 +256,8 @@ internal sealed class XuiPanelGateway(
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Failure<IReadOnlyDictionary<string, ClientTrafficInfo>>(
|
||||
Error.Failure("Xui.TrafficFetchFailed", $"Не удалось получить трафик: {ex.Message}"));
|
||||
Error.Failure("Xui.TrafficFetchFailed", $"Не удалось получить трафик: {ex.Message}")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -181,7 +266,9 @@ internal sealed class XuiPanelGateway(
|
||||
/// стандартное поле 3x-ui API "clientStats": [{ "email": "...", "up": N, "down": N }, ...].
|
||||
/// Формат форка может отличаться — при ошибке парсинга просто возвращаем пусто, не валим синхронизацию.
|
||||
/// </summary>
|
||||
private static IReadOnlyDictionary<string, ClientTrafficInfo> ParseClientStats(string? rawInboundJson)
|
||||
private static IReadOnlyDictionary<string, ClientTrafficInfo> ParseClientStats(
|
||||
string? rawInboundJson
|
||||
)
|
||||
{
|
||||
var result = new Dictionary<string, ClientTrafficInfo>();
|
||||
if (string.IsNullOrWhiteSpace(rawInboundJson))
|
||||
@@ -190,12 +277,18 @@ internal sealed class XuiPanelGateway(
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(rawInboundJson);
|
||||
if (!doc.RootElement.TryGetProperty("clientStats", out var clientStats) || clientStats.ValueKind != JsonValueKind.Array)
|
||||
if (
|
||||
!doc.RootElement.TryGetProperty("clientStats", out var clientStats)
|
||||
|| clientStats.ValueKind != JsonValueKind.Array
|
||||
)
|
||||
return result;
|
||||
|
||||
foreach (var stat in clientStats.EnumerateArray())
|
||||
{
|
||||
if (!stat.TryGetProperty("email", out var emailProp) || emailProp.ValueKind != JsonValueKind.String)
|
||||
if (
|
||||
!stat.TryGetProperty("email", out var emailProp)
|
||||
|| emailProp.ValueKind != JsonValueKind.String
|
||||
)
|
||||
continue;
|
||||
|
||||
var email = emailProp.GetString();
|
||||
@@ -221,34 +314,40 @@ internal sealed class XuiPanelGateway(
|
||||
(lazy.Value as IDisposable)?.Dispose();
|
||||
}
|
||||
|
||||
private IXuiClient GetClient(Node node)
|
||||
=> _clients.GetOrAdd(node.Id, _ => new Lazy<IXuiClient>(() => CreateClient(node))).Value;
|
||||
private IXuiClient GetClient(Node node) =>
|
||||
_clients.GetOrAdd(node.Id, _ => new Lazy<IXuiClient>(() => CreateClient(node))).Value;
|
||||
|
||||
private IXuiClient CreateClient(Node node)
|
||||
{
|
||||
var httpClient = httpClientFactory.Create(node.BaseAddress, allowInsecureTls: false, timeout: TimeSpan.FromSeconds(15));
|
||||
var httpClient = httpClientFactory.Create(
|
||||
node.BaseAddress,
|
||||
allowInsecureTls: false,
|
||||
timeout: TimeSpan.FromSeconds(15)
|
||||
);
|
||||
var password = secretProtector.Unprotect(node.Credentials.ProtectedPassword);
|
||||
var logger = loggerFactory.CreateLogger<XuiClient>();
|
||||
return new XuiClient(httpClient, node.Credentials.Username, password, logger);
|
||||
}
|
||||
|
||||
private static VpnProtocol? TryParseProtocol(string raw) => raw.ToLowerInvariant() switch
|
||||
{
|
||||
"vless" => VpnProtocol.Vless,
|
||||
"vmess" => VpnProtocol.Vmess,
|
||||
"trojan" => VpnProtocol.Trojan,
|
||||
"shadowsocks" => VpnProtocol.Shadowsocks,
|
||||
_ => null,
|
||||
};
|
||||
private static VpnProtocol? TryParseProtocol(string raw) =>
|
||||
raw.ToLowerInvariant() switch
|
||||
{
|
||||
"vless" => VpnProtocol.Vless,
|
||||
"vmess" => VpnProtocol.Vmess,
|
||||
"trojan" => VpnProtocol.Trojan,
|
||||
"shadowsocks" => VpnProtocol.Shadowsocks,
|
||||
_ => null,
|
||||
};
|
||||
|
||||
private static string ToRemoteProtocol(VpnProtocol protocol) => protocol switch
|
||||
{
|
||||
VpnProtocol.Vless => "vless",
|
||||
VpnProtocol.Vmess => "vmess",
|
||||
VpnProtocol.Trojan => "trojan",
|
||||
VpnProtocol.Shadowsocks => "shadowsocks",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(protocol)),
|
||||
};
|
||||
private static string ToRemoteProtocol(VpnProtocol protocol) =>
|
||||
protocol switch
|
||||
{
|
||||
VpnProtocol.Vless => "vless",
|
||||
VpnProtocol.Vmess => "vmess",
|
||||
VpnProtocol.Trojan => "trojan",
|
||||
VpnProtocol.Shadowsocks => "shadowsocks",
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(protocol)),
|
||||
};
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user