Refactor VPN configuration handling to remove device limit management
- Updated the VPN configuration commands and handlers to eliminate the device limit parameter, simplifying the configuration process. - Adjusted related API documentation to reflect the removal of device limit management, clarifying that this setting is now handled directly in the 3x-ui by node administrators. - Enhanced the overall codebase by removing unnecessary device limit references across various components, ensuring a cleaner and more maintainable code structure.
This commit is contained in:
@@ -44,14 +44,14 @@ public static class ConfigEndpoints
|
||||
|
||||
private static async Task<IResult> CreateConfig(CreateConfigBody body, ISender sender, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = new CreateVpnConfigCommand(body.InboundId, body.Label, body.DeviceLimit);
|
||||
var command = new CreateVpnConfigCommand(body.InboundId, body.Label);
|
||||
var result = await sender.Send(command, cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
|
||||
private static async Task<IResult> EditConfig(Guid id, EditConfigBody body, ISender sender, CancellationToken cancellationToken)
|
||||
{
|
||||
var command = new EditVpnConfigCommand(id, body.Label, body.DeviceLimit);
|
||||
var command = new EditVpnConfigCommand(id, body.Label);
|
||||
var result = await sender.Send(command, cancellationToken);
|
||||
return result.ToHttpResult();
|
||||
}
|
||||
@@ -89,9 +89,9 @@ public static class ConfigEndpoints
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record CreateConfigBody(Guid InboundId, string? Label, int? DeviceLimit);
|
||||
public sealed record CreateConfigBody(Guid InboundId, string? Label);
|
||||
|
||||
public sealed record EditConfigBody(string? Label, int? DeviceLimit);
|
||||
public sealed record EditConfigBody(string? Label);
|
||||
|
||||
public sealed record ConfigLinkResponseDto(string ConnectionString, string SubscriptionUrl);
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ public sealed class BlockUserCommandHandler(
|
||||
{
|
||||
var updateResult = await gateway.UpdateClientAsync(
|
||||
node, inbound.RemoteInboundId, config.ClientExternalId, config.Protocol,
|
||||
config.Label ?? config.ClientEmail, config.DeviceLimit, enable: false, cancellationToken);
|
||||
config.Label ?? config.ClientEmail, enable: false, cancellationToken);
|
||||
|
||||
if (!updateResult.IsSuccess)
|
||||
{
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class UnblockUserCommandHandler(
|
||||
{
|
||||
var updateResult = await gateway.UpdateClientAsync(
|
||||
node, inbound.RemoteInboundId, config.ClientExternalId, config.Protocol,
|
||||
config.Label ?? config.ClientEmail, config.DeviceLimit, enable: true, cancellationToken);
|
||||
config.Label ?? config.ClientEmail, enable: true, cancellationToken);
|
||||
|
||||
if (!updateResult.IsSuccess)
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ public interface IXuiPanelGateway
|
||||
/// <summary>Возвращает ClientExternalId, присвоенный панелью (UUID для VLESS/VMess, пароль для Trojan/Shadowsocks).</summary>
|
||||
Task<Result<string>> AddClientAsync(
|
||||
Node node, string inboundRemoteId, VpnProtocol protocol, string clientEmail, string clientName,
|
||||
int deviceLimit, CancellationToken cancellationToken);
|
||||
CancellationToken cancellationToken);
|
||||
|
||||
Task<Result> RemoveClientAsync(
|
||||
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol,
|
||||
@@ -35,7 +35,7 @@ public interface IXuiPanelGateway
|
||||
|
||||
Task<Result> UpdateClientAsync(
|
||||
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol,
|
||||
string name, int deviceLimit, bool enable, CancellationToken cancellationToken);
|
||||
string name, bool enable, CancellationToken cancellationToken);
|
||||
|
||||
Task<Result<string>> BuildConnectionStringAsync(
|
||||
Node node, Inbound inbound, string clientExternalId, string clientName, string publicHost,
|
||||
|
||||
@@ -3,4 +3,4 @@ using PnvPanel.Application.Common.Models;
|
||||
|
||||
namespace PnvPanel.Application.Configs.Create;
|
||||
|
||||
public sealed record CreateVpnConfigCommand(Guid InboundId, string? Label, int? DeviceLimit) : ICommand<Result<VpnConfigDto>>;
|
||||
public sealed record CreateVpnConfigCommand(Guid InboundId, string? Label) : ICommand<Result<VpnConfigDto>>;
|
||||
|
||||
@@ -36,7 +36,7 @@ public sealed class CreateVpnConfigCommandHandler(
|
||||
if (node is null || !node.IsEnabled)
|
||||
return Result.Failure<VpnConfigDto>(ConfigErrors.NodeDisabled);
|
||||
|
||||
var config = VpnConfig.Create(userId, inbound.Id, inbound.Protocol, command.Label, command.DeviceLimit ?? 0);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, inbound.Protocol, command.Label);
|
||||
|
||||
var reserveResult = await ReserveQuotaSlotAsync(userId, profile.MaxConfigs, config, cancellationToken);
|
||||
if (!reserveResult.IsSuccess)
|
||||
@@ -44,7 +44,7 @@ public sealed class CreateVpnConfigCommandHandler(
|
||||
|
||||
var addResult = await gateway.AddClientAsync(
|
||||
node, inbound.RemoteInboundId, inbound.Protocol, config.ClientEmail,
|
||||
config.Label ?? config.ClientEmail, config.DeviceLimit, cancellationToken);
|
||||
config.Label ?? config.ClientEmail, cancellationToken);
|
||||
|
||||
if (!addResult.IsSuccess)
|
||||
{
|
||||
|
||||
@@ -8,6 +8,5 @@ public sealed class CreateVpnConfigCommandValidator : AbstractValidator<CreateVp
|
||||
{
|
||||
RuleFor(x => x.InboundId).NotEmpty();
|
||||
RuleFor(x => x.Label).MaximumLength(100);
|
||||
RuleFor(x => x.DeviceLimit).GreaterThanOrEqualTo(0).When(x => x.DeviceLimit.HasValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ using PnvPanel.Application.Common.Models;
|
||||
|
||||
namespace PnvPanel.Application.Configs.Edit;
|
||||
|
||||
public sealed record EditVpnConfigCommand(Guid ConfigId, string? Label, int? DeviceLimit) : ICommand<Result<VpnConfigDto>>;
|
||||
public sealed record EditVpnConfigCommand(Guid ConfigId, string? Label) : ICommand<Result<VpnConfigDto>>;
|
||||
|
||||
@@ -25,18 +25,15 @@ public sealed class EditVpnConfigCommandHandler(IAppDbContext dbContext, IXuiPan
|
||||
return Result.Failure<VpnConfigDto>(ConfigErrors.InboundNotAvailable);
|
||||
|
||||
if (command.Label is not null)
|
||||
config.Rename(command.Label);
|
||||
|
||||
if (command.DeviceLimit is { } deviceLimit)
|
||||
{
|
||||
config.SetDeviceLimit(deviceLimit);
|
||||
config.Rename(command.Label);
|
||||
|
||||
var node = await dbContext.Nodes.AsNoTracking().FirstOrDefaultAsync(n => n.Id == inbound.NodeId, cancellationToken);
|
||||
if (node is not null)
|
||||
{
|
||||
await gateway.UpdateClientAsync(
|
||||
node, inbound.RemoteInboundId, config.ClientExternalId, config.Protocol,
|
||||
config.Label ?? config.ClientEmail, deviceLimit, enable: true, cancellationToken);
|
||||
config.Label ?? config.ClientEmail, enable: true, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,5 @@ public sealed class EditVpnConfigCommandValidator : AbstractValidator<EditVpnCon
|
||||
public EditVpnConfigCommandValidator()
|
||||
{
|
||||
RuleFor(x => x.Label).MaximumLength(100);
|
||||
RuleFor(x => x.DeviceLimit).GreaterThanOrEqualTo(0).When(x => x.DeviceLimit.HasValue);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ public sealed class RotateVpnConfigCommandHandler(IAppDbContext dbContext, IXuiP
|
||||
var newClientEmail = VpnConfig.GenerateClientEmail(userId);
|
||||
var addResult = await gateway.AddClientAsync(
|
||||
node, inbound.RemoteInboundId, config.Protocol, newClientEmail,
|
||||
config.Label ?? newClientEmail, config.DeviceLimit, cancellationToken);
|
||||
config.Label ?? newClientEmail, cancellationToken);
|
||||
|
||||
if (!addResult.IsSuccess)
|
||||
return Result.Failure<VpnConfigDto>(addResult.Error);
|
||||
|
||||
@@ -8,10 +8,10 @@ namespace PnvPanel.Application.Configs;
|
||||
/// детали 3x-ui в этот DTO не попадают (см. domain-model.md).
|
||||
/// </summary>
|
||||
public sealed record VpnConfigDto(
|
||||
Guid Id, string? Label, VpnProtocol Protocol, string Location, int DeviceLimit,
|
||||
Guid Id, string? Label, VpnProtocol Protocol, string Location,
|
||||
long UsedUpBytes, long UsedDownBytes, DateTimeOffset? ExpiresAt, ConfigStatus Status, DateTimeOffset CreatedAt)
|
||||
{
|
||||
public static VpnConfigDto FromDomain(VpnConfig config, Inbound inbound) => new(
|
||||
config.Id, config.Label, config.Protocol, inbound.DisplayName ?? inbound.Remark, config.DeviceLimit,
|
||||
config.Id, config.Label, config.Protocol, inbound.DisplayName ?? inbound.Remark,
|
||||
config.UsedUpBytes, config.UsedDownBytes, config.ExpiresAt, config.Status, config.CreatedAt);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ public sealed class VpnConfig : Entity
|
||||
public string ClientEmail { get; private set; } = string.Empty;
|
||||
public string ClientExternalId { get; private set; } = string.Empty;
|
||||
public VpnProtocol Protocol { get; private set; }
|
||||
public int DeviceLimit { get; private set; }
|
||||
public long UsedUpBytes { get; private set; }
|
||||
public long UsedDownBytes { get; private set; }
|
||||
public DateTimeOffset? ExpiresAt { get; private set; }
|
||||
@@ -31,7 +30,7 @@ public sealed class VpnConfig : Entity
|
||||
{
|
||||
}
|
||||
|
||||
public static VpnConfig Create(Guid userId, Guid inboundId, VpnProtocol protocol, string? label, int deviceLimit)
|
||||
public static VpnConfig Create(Guid userId, Guid inboundId, VpnProtocol protocol, string? label)
|
||||
{
|
||||
return new VpnConfig
|
||||
{
|
||||
@@ -42,7 +41,6 @@ public sealed class VpnConfig : Entity
|
||||
ClientEmail = GenerateClientEmail(userId),
|
||||
ClientExternalId = string.Empty,
|
||||
Label = label,
|
||||
DeviceLimit = deviceLimit,
|
||||
Status = ConfigStatus.Active,
|
||||
SubscriptionToken = GenerateToken(),
|
||||
CreatedAt = DateTimeOffset.UtcNow,
|
||||
@@ -54,8 +52,6 @@ public sealed class VpnConfig : Entity
|
||||
|
||||
public void Rename(string? label) => Label = label;
|
||||
|
||||
public void SetDeviceLimit(int deviceLimit) => DeviceLimit = deviceLimit;
|
||||
|
||||
public void Rotate(string newClientEmail, string newClientExternalId)
|
||||
{
|
||||
EnsureActive("перевыпустить");
|
||||
|
||||
+744
@@ -0,0 +1,744 @@
|
||||
// <auto-generated />
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||
using PnvPanel.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20260702201236_RemoveDeviceLimit")]
|
||||
partial class RemoveDeviceLimit
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||
{
|
||||
#pragma warning disable 612, 618
|
||||
modelBuilder
|
||||
.HasAnnotation("ProductVersion", "10.0.9")
|
||||
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||
|
||||
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetRoleClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.Property<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("integer");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||
|
||||
b.Property<string>("ClaimType")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ClaimValue")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserClaims", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderKey")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("ProviderDisplayName")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("LoginProvider", "ProviderKey");
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("AspNetUserLogins", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid>("RoleId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("UserId", "RoleId");
|
||||
|
||||
b.HasIndex("RoleId");
|
||||
|
||||
b.ToTable("AspNetUserRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("LoginProvider")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Value")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("UserId", "LoginProvider", "Name");
|
||||
|
||||
b.ToTable("AspNetUserTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Domain.Activation.ActivationRequest", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Comment")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("DecidedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("DecidedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("RejectionReason")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId", "Status");
|
||||
|
||||
b.ToTable("ActivationRequests", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Domain.Apps.ClientApp", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Description")
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)");
|
||||
|
||||
b.Property<string>("DownloadUrl")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<string>("IconUrl")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("OperatingSystem")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<int>("SortOrder")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("ClientApps", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Domain.Audit.AuditLog", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Action")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<Guid?>("ActorId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Metadata")
|
||||
.HasColumnType("jsonb");
|
||||
|
||||
b.Property<string>("Source")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("TargetId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("TargetType")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CreatedAt");
|
||||
|
||||
b.ToTable("AuditLogs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Domain.Configs.TrafficSample", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<Guid>("ConfigId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<long>("DownBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTimeOffset>("Timestamp")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long>("UpBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ConfigId", "Timestamp");
|
||||
|
||||
b.ToTable("TrafficSamples", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Domain.Configs.VpnConfig", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ClientEmail")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("ClientExternalId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset?>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid>("InboundId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Label")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastSyncAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Protocol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("SubscriptionToken")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<long>("UsedDownBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("UsedUpBytes")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("InboundId");
|
||||
|
||||
b.HasIndex("SubscriptionToken")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId", "Status");
|
||||
|
||||
b.ToTable("VpnConfigs", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Domain.Inbounds.Inbound", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.PrimitiveCollection<Guid[]>("AllowedRoleIds")
|
||||
.IsRequired()
|
||||
.HasColumnType("uuid[]");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<bool>("IsPublished")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastSyncAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int?>("MaxClients")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<Guid>("NodeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("Port")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Protocol")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<string>("Remark")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("RemoteInboundId")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NodeId", "RemoteInboundId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Inbounds", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Domain.Nodes.Node", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("BaseAddress")
|
||||
.IsRequired()
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("character varying(500)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<bool>("IsEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LastSyncAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Location")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.IsRequired()
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("character varying(100)");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("Nodes", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Domain.Telegram.TelegramLinkToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset?>("ConsumedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Token")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("character varying(64)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Token")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("TelegramLinkTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Domain.Telegram.TelegramLoginRequest", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("Context")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(32)
|
||||
.HasColumnType("character varying(32)");
|
||||
|
||||
b.Property<Guid?>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.ToTable("TelegramLoginRequests", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Infrastructure.Identity.AppRole", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("IsSystem")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<int>("MaxConfigs")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Name")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("RoleNameIndex");
|
||||
|
||||
b.ToTable("AspNetRoles", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Infrastructure.Identity.AppUser", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<int>("AccessFailedCount")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset?>("ActivatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<Guid?>("ActivatedBy")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<string>("ConcurrencyStamp")
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Email")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<bool>("EmailConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsActivated")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("IsBlocked")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<bool>("LockoutEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("NormalizedEmail")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("NormalizedUserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PhoneNumber")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("PhoneNumberConfirmed")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("SecurityStamp")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("SubscriptionToken")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset?>("TelegramLinkedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<long?>("TelegramUserId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("TelegramUsername")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<bool>("TwoFactorEnabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("UserName")
|
||||
.HasMaxLength(256)
|
||||
.HasColumnType("character varying(256)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("NormalizedEmail")
|
||||
.HasDatabaseName("EmailIndex");
|
||||
|
||||
b.HasIndex("NormalizedUserName")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UserNameIndex");
|
||||
|
||||
b.HasIndex("SubscriptionToken")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("TelegramUserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("AspNetUsers", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Infrastructure.Identity.RefreshToken", b =>
|
||||
{
|
||||
b.Property<Guid>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<DateTimeOffset>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ReplacedByTokenHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<DateTimeOffset?>("RevokedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("TokenHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(128)
|
||||
.HasColumnType("character varying(128)");
|
||||
|
||||
b.Property<Guid>("UserId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("TokenHash")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("UserId");
|
||||
|
||||
b.ToTable("RefreshTokens", (string)null);
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("PnvPanel.Infrastructure.Identity.AppRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("PnvPanel.Infrastructure.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("PnvPanel.Infrastructure.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("PnvPanel.Infrastructure.Identity.AppRole", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("RoleId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("PnvPanel.Infrastructure.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<System.Guid>", b =>
|
||||
{
|
||||
b.HasOne("PnvPanel.Infrastructure.Identity.AppUser", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("UserId")
|
||||
.OnDelete(DeleteBehavior.Cascade)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("PnvPanel.Domain.Nodes.Node", b =>
|
||||
{
|
||||
b.OwnsOne("PnvPanel.Domain.Nodes.NodeCredentials", "Credentials", b1 =>
|
||||
{
|
||||
b1.Property<Guid>("NodeId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b1.Property<string>("ProtectedPassword")
|
||||
.IsRequired()
|
||||
.HasColumnType("text")
|
||||
.HasColumnName("CredentialsProtectedPassword");
|
||||
|
||||
b1.Property<string>("Username")
|
||||
.IsRequired()
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)")
|
||||
.HasColumnName("CredentialsUsername");
|
||||
|
||||
b1.HasKey("NodeId");
|
||||
|
||||
b1.ToTable("Nodes");
|
||||
|
||||
b1.WithOwner()
|
||||
.HasForeignKey("NodeId");
|
||||
});
|
||||
|
||||
b.Navigation("Credentials")
|
||||
.IsRequired();
|
||||
});
|
||||
#pragma warning restore 612, 618
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class RemoveDeviceLimit : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "DeviceLimit",
|
||||
table: "VpnConfigs");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<int>(
|
||||
name: "DeviceLimit",
|
||||
table: "VpnConfigs",
|
||||
type: "integer",
|
||||
nullable: false,
|
||||
defaultValue: 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
-3
@@ -293,9 +293,6 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<int>("DeviceLimit")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<DateTimeOffset?>("ExpiresAt")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
|
||||
@@ -71,12 +71,14 @@ internal sealed class XuiPanelGateway(
|
||||
|
||||
public async Task<Result<string>> AddClientAsync(
|
||||
Node node, string inboundRemoteId, VpnProtocol protocol, string clientEmail, string clientName,
|
||||
int deviceLimit, CancellationToken cancellationToken)
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = GetClient(node);
|
||||
var request = new AddClientRequest(clientName, clientEmail, ToRemoteProtocol(protocol), deviceLimit, null);
|
||||
// Лимит устройств (limitIp) панелью больше не управляется — задаётся, если нужно,
|
||||
// напрямую в 3x-ui администратором ноды. Новый клиент всегда создаётся без лимита.
|
||||
var request = new AddClientRequest(clientName, clientEmail, ToRemoteProtocol(protocol), 0, null);
|
||||
var result = await client.AddClientAsync(inboundRemoteId, request, cancellationToken);
|
||||
return Result.Success(result.ExternalClientId);
|
||||
}
|
||||
@@ -103,12 +105,13 @@ internal sealed class XuiPanelGateway(
|
||||
|
||||
public async Task<Result> UpdateClientAsync(
|
||||
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol,
|
||||
string name, int deviceLimit, bool enable, CancellationToken cancellationToken)
|
||||
string name, bool enable, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var client = GetClient(node);
|
||||
var request = new UpdateClientRequest(deviceLimit, null, enable, name);
|
||||
// deviceLimit: null — не трогаем то, что уже стоит на клиенте в панели (см. AddClientAsync).
|
||||
var request = new UpdateClientRequest(null, null, enable, name);
|
||||
await client.UpdateClientAsync(inboundRemoteId, clientExternalId, ToRemoteProtocol(protocol), request, cancellationToken);
|
||||
return Result.Success();
|
||||
}
|
||||
|
||||
+7
-7
@@ -36,7 +36,7 @@ public class BlockUserCommandHandlerTests
|
||||
Assert.Equal(failure, result.Error);
|
||||
await _gateway.DidNotReceive().UpdateClientAsync(
|
||||
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
|
||||
Arg.Any<string>(), Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>());
|
||||
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -48,7 +48,7 @@ public class BlockUserCommandHandlerTests
|
||||
|
||||
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
|
||||
var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config", 0);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config");
|
||||
|
||||
dbContext.Nodes.Add(node);
|
||||
dbContext.Inbounds.Add(inbound);
|
||||
@@ -59,7 +59,7 @@ public class BlockUserCommandHandlerTests
|
||||
_currentUser.UserId.Returns(adminId);
|
||||
_gateway.UpdateClientAsync(
|
||||
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
|
||||
Arg.Any<string>(), Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Result.Success());
|
||||
|
||||
var handler = new BlockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _telegramNotifier, _currentUser, _logger);
|
||||
@@ -71,7 +71,7 @@ public class BlockUserCommandHandlerTests
|
||||
|
||||
await _gateway.Received(1).UpdateClientAsync(
|
||||
Arg.Is<Node>(n => n.Id == node.Id), inbound.RemoteInboundId, config.ClientExternalId, config.Protocol,
|
||||
"my-config", config.DeviceLimit, enable: false, Arg.Any<CancellationToken>());
|
||||
"my-config", enable: false, Arg.Any<CancellationToken>());
|
||||
await _notifier.Received(1).NotifyConfigStatusChangedAsync(userId, config.Id, ConfigStatus.Disabled, Arg.Any<CancellationToken>());
|
||||
|
||||
var audit = Assert.Single(dbContext.AuditLogs.Local);
|
||||
@@ -95,7 +95,7 @@ public class BlockUserCommandHandlerTests
|
||||
Assert.True(result.IsSuccess);
|
||||
await _gateway.DidNotReceive().UpdateClientAsync(
|
||||
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
|
||||
Arg.Any<string>(), Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>());
|
||||
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -107,7 +107,7 @@ public class BlockUserCommandHandlerTests
|
||||
|
||||
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
|
||||
var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config", 0);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config");
|
||||
|
||||
dbContext.Nodes.Add(node);
|
||||
dbContext.Inbounds.Add(inbound);
|
||||
@@ -118,7 +118,7 @@ public class BlockUserCommandHandlerTests
|
||||
_currentUser.UserId.Returns(adminId);
|
||||
_gateway.UpdateClientAsync(
|
||||
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
|
||||
Arg.Any<string>(), Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Result.Failure(Error.Failure("Xui.UpdateClientFailed", "Нода недоступна.")));
|
||||
|
||||
var handler = new BlockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _telegramNotifier, _currentUser, _logger);
|
||||
|
||||
+5
-5
@@ -28,7 +28,7 @@ public class UnblockUserCommandHandlerTests
|
||||
|
||||
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
|
||||
var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config", 0);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config");
|
||||
config.Disable();
|
||||
|
||||
dbContext.Nodes.Add(node);
|
||||
@@ -40,7 +40,7 @@ public class UnblockUserCommandHandlerTests
|
||||
_identityService.UnblockUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Success());
|
||||
_gateway.UpdateClientAsync(
|
||||
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
|
||||
Arg.Any<string>(), Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Result.Success());
|
||||
|
||||
var handler = new UnblockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _currentUser, _logger);
|
||||
@@ -51,7 +51,7 @@ public class UnblockUserCommandHandlerTests
|
||||
Assert.Equal(ConfigStatus.Active, config.Status);
|
||||
await _gateway.Received(1).UpdateClientAsync(
|
||||
node, inbound.RemoteInboundId, config.ClientExternalId, config.Protocol,
|
||||
Arg.Any<string>(), config.DeviceLimit, true, Arg.Any<CancellationToken>());
|
||||
Arg.Any<string>(), true, Arg.Any<CancellationToken>());
|
||||
await _notifier.Received(1).NotifyConfigStatusChangedAsync(userId, config.Id, ConfigStatus.Active, Arg.Any<CancellationToken>());
|
||||
Assert.Single(dbContext.AuditLogs.Local);
|
||||
Assert.Equal("UserUnblocked", dbContext.AuditLogs.Local.Single().Action);
|
||||
@@ -84,7 +84,7 @@ public class UnblockUserCommandHandlerTests
|
||||
|
||||
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
|
||||
var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config", 0);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config");
|
||||
config.Disable();
|
||||
|
||||
dbContext.Nodes.Add(node);
|
||||
@@ -96,7 +96,7 @@ public class UnblockUserCommandHandlerTests
|
||||
_identityService.UnblockUserAsync(userId, Arg.Any<CancellationToken>()).Returns(Result.Success());
|
||||
_gateway.UpdateClientAsync(
|
||||
Arg.Any<Node>(), Arg.Any<string>(), Arg.Any<string>(), Arg.Any<VpnProtocol>(),
|
||||
Arg.Any<string>(), Arg.Any<int>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
Arg.Any<string>(), Arg.Any<bool>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Result.Failure(Error.Failure("Xui.UpdateClientFailed", "Нода недоступна.")));
|
||||
|
||||
var handler = new UnblockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _currentUser, _logger);
|
||||
|
||||
+3
-3
@@ -23,10 +23,10 @@ public class GetMyConfigsQueryHandlerTests
|
||||
var inbound = Inbound.FromRemote(Guid.NewGuid(), "1", VpnProtocol.Vless, "remark", 443);
|
||||
inbound.Publish("My inbound", [], null);
|
||||
|
||||
var activeConfig = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "Active", 0);
|
||||
var revokedConfig = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "Revoked", 0);
|
||||
var activeConfig = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "Active");
|
||||
var revokedConfig = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "Revoked");
|
||||
revokedConfig.Revoke();
|
||||
var otherUsersConfig = VpnConfig.Create(otherUserId, inbound.Id, VpnProtocol.Vless, "Other", 0);
|
||||
var otherUsersConfig = VpnConfig.Create(otherUserId, inbound.Id, VpnProtocol.Vless, "Other");
|
||||
|
||||
dbContext.Inbounds.Add(inbound);
|
||||
dbContext.VpnConfigs.AddRange(activeConfig, revokedConfig, otherUsersConfig);
|
||||
|
||||
+2
-2
@@ -23,7 +23,7 @@ public class RevokeVpnConfigCommandHandlerTests
|
||||
|
||||
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
|
||||
var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, null, 0);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, null);
|
||||
config.AssignRemoteClient("external-id");
|
||||
|
||||
dbContext.Nodes.Add(node);
|
||||
@@ -49,7 +49,7 @@ public class RevokeVpnConfigCommandHandlerTests
|
||||
var userId = Guid.NewGuid();
|
||||
|
||||
var inbound = Inbound.FromRemote(Guid.NewGuid(), "1", VpnProtocol.Vless, "remark", 443);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, null, 0);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, null);
|
||||
config.Revoke();
|
||||
|
||||
dbContext.Inbounds.Add(inbound);
|
||||
|
||||
+6
-6
@@ -24,7 +24,7 @@ public class RotateVpnConfigCommandHandlerTests
|
||||
|
||||
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
|
||||
var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config", 0);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config");
|
||||
config.AssignRemoteClient("old-external-id");
|
||||
|
||||
dbContext.Nodes.Add(node);
|
||||
@@ -34,7 +34,7 @@ public class RotateVpnConfigCommandHandlerTests
|
||||
|
||||
_gateway.AddClientAsync(
|
||||
Arg.Any<Node>(), inbound.RemoteInboundId, config.Protocol, Arg.Any<string>(),
|
||||
Arg.Any<string>(), config.DeviceLimit, Arg.Any<CancellationToken>())
|
||||
Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Result.Success("new-external-id"));
|
||||
|
||||
var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, FakeCurrentUser.Authenticated(userId));
|
||||
@@ -69,7 +69,7 @@ public class RotateVpnConfigCommandHandlerTests
|
||||
var otherUserId = Guid.NewGuid();
|
||||
|
||||
var inbound = Inbound.FromRemote(Guid.NewGuid(), "1", VpnProtocol.Vless, "remark", 443);
|
||||
var config = VpnConfig.Create(ownerId, inbound.Id, VpnProtocol.Vless, null, 0);
|
||||
var config = VpnConfig.Create(ownerId, inbound.Id, VpnProtocol.Vless, null);
|
||||
|
||||
dbContext.Inbounds.Add(inbound);
|
||||
dbContext.VpnConfigs.Add(config);
|
||||
@@ -90,7 +90,7 @@ public class RotateVpnConfigCommandHandlerTests
|
||||
var userId = Guid.NewGuid();
|
||||
|
||||
var inbound = Inbound.FromRemote(Guid.NewGuid(), "1", VpnProtocol.Vless, "remark", 443);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, null, 0);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, null);
|
||||
config.Revoke();
|
||||
|
||||
dbContext.Inbounds.Add(inbound);
|
||||
@@ -113,7 +113,7 @@ public class RotateVpnConfigCommandHandlerTests
|
||||
|
||||
var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null);
|
||||
var inbound = Inbound.FromRemote(node.Id, "1", VpnProtocol.Vless, "remark", 443);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, null, 0);
|
||||
var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, null);
|
||||
config.AssignRemoteClient("old-external-id");
|
||||
|
||||
dbContext.Nodes.Add(node);
|
||||
@@ -124,7 +124,7 @@ public class RotateVpnConfigCommandHandlerTests
|
||||
var gatewayError = Error.Failure("Xui.Unreachable", "Панель недоступна.");
|
||||
_gateway.AddClientAsync(
|
||||
Arg.Any<Node>(), inbound.RemoteInboundId, config.Protocol, Arg.Any<string>(),
|
||||
Arg.Any<string>(), config.DeviceLimit, Arg.Any<CancellationToken>())
|
||||
Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.Returns(Result.Failure<string>(gatewayError));
|
||||
|
||||
var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, FakeCurrentUser.Authenticated(userId));
|
||||
|
||||
@@ -13,13 +13,12 @@ public class VpnConfigTests
|
||||
var userId = Guid.NewGuid();
|
||||
var inboundId = Guid.NewGuid();
|
||||
|
||||
var config = VpnConfig.Create(userId, inboundId, VpnProtocol.Vless, "My device", deviceLimit: 3);
|
||||
var config = VpnConfig.Create(userId, inboundId, VpnProtocol.Vless, "My device");
|
||||
|
||||
Assert.Equal(userId, config.UserId);
|
||||
Assert.Equal(inboundId, config.InboundId);
|
||||
Assert.Equal(VpnProtocol.Vless, config.Protocol);
|
||||
Assert.Equal("My device", config.Label);
|
||||
Assert.Equal(3, config.DeviceLimit);
|
||||
Assert.Equal(ConfigStatus.Active, config.Status);
|
||||
Assert.Equal(string.Empty, config.ClientExternalId);
|
||||
Assert.False(string.IsNullOrWhiteSpace(config.ClientEmail));
|
||||
@@ -32,8 +31,8 @@ public class VpnConfigTests
|
||||
public void Create_GeneratesUniqueSubscriptionTokensAndClientEmails()
|
||||
{
|
||||
var userId = Guid.NewGuid();
|
||||
var a = VpnConfig.Create(userId, Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var b = VpnConfig.Create(userId, Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var a = VpnConfig.Create(userId, Guid.NewGuid(), VpnProtocol.Vless, null);
|
||||
var b = VpnConfig.Create(userId, Guid.NewGuid(), VpnProtocol.Vless, null);
|
||||
|
||||
Assert.NotEqual(a.SubscriptionToken, b.SubscriptionToken);
|
||||
Assert.NotEqual(a.ClientEmail, b.ClientEmail);
|
||||
@@ -42,7 +41,7 @@ public class VpnConfigTests
|
||||
[Fact]
|
||||
public void AssignRemoteClient_SetsClientExternalId()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Trojan, null, 1);
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Trojan, null);
|
||||
|
||||
config.AssignRemoteClient("some-remote-password");
|
||||
|
||||
@@ -52,7 +51,7 @@ public class VpnConfigTests
|
||||
[Fact]
|
||||
public void Rotate_WhenActive_ChangesEmailExternalIdAndToken()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null);
|
||||
config.AssignRemoteClient("old-id");
|
||||
var oldToken = config.SubscriptionToken;
|
||||
var oldEmail = config.ClientEmail;
|
||||
@@ -70,7 +69,7 @@ public class VpnConfigTests
|
||||
[InlineData(ConfigStatus.Disabled)]
|
||||
public void Rotate_WhenNotActive_Throws(ConfigStatus status)
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null);
|
||||
MoveToStatus(config, status);
|
||||
|
||||
Assert.Throws<DomainException>(() => config.Rotate("e", "i"));
|
||||
@@ -79,7 +78,7 @@ public class VpnConfigTests
|
||||
[Fact]
|
||||
public void Revoke_WhenActive_SetsRevokedStatus()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null);
|
||||
|
||||
config.Revoke();
|
||||
|
||||
@@ -89,7 +88,7 @@ public class VpnConfigTests
|
||||
[Fact]
|
||||
public void Revoke_WhenAlreadyRevoked_Throws()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null);
|
||||
config.Revoke();
|
||||
|
||||
Assert.Throws<DomainException>(() => config.Revoke());
|
||||
@@ -98,7 +97,7 @@ public class VpnConfigTests
|
||||
[Fact]
|
||||
public void Disable_WhenActive_SetsDisabled()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null);
|
||||
|
||||
config.Disable();
|
||||
|
||||
@@ -108,7 +107,7 @@ public class VpnConfigTests
|
||||
[Fact]
|
||||
public void Disable_WhenRevoked_DoesNotChangeStatus()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null);
|
||||
config.Revoke();
|
||||
|
||||
config.Disable();
|
||||
@@ -119,7 +118,7 @@ public class VpnConfigTests
|
||||
[Fact]
|
||||
public void Enable_WhenDisabled_ReturnsToActive()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null);
|
||||
config.Disable();
|
||||
|
||||
config.Enable();
|
||||
@@ -130,7 +129,7 @@ public class VpnConfigTests
|
||||
[Fact]
|
||||
public void Enable_WhenRevoked_DoesNotResurrect()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null);
|
||||
config.Revoke();
|
||||
|
||||
config.Enable();
|
||||
@@ -141,7 +140,7 @@ public class VpnConfigTests
|
||||
[Fact]
|
||||
public void UpdateTraffic_SetsBytesAndLastSyncAt()
|
||||
{
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null, 1);
|
||||
var config = VpnConfig.Create(Guid.NewGuid(), Guid.NewGuid(), VpnProtocol.Vless, null);
|
||||
|
||||
config.UpdateTraffic(100, 200);
|
||||
|
||||
|
||||
@@ -87,7 +87,6 @@ public class ConfigQuotaTests(PnvPanelWebApplicationFactory factory)
|
||||
{
|
||||
inboundId = inbound.Id,
|
||||
label = $"device-{i}",
|
||||
deviceLimit = (int?)null,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ public sealed class FakeXuiPanelGateway : IXuiPanelGateway
|
||||
|
||||
public Task<Result<string>> AddClientAsync(
|
||||
Node node, string inboundRemoteId, VpnProtocol protocol, string clientEmail, string clientName,
|
||||
int deviceLimit, CancellationToken cancellationToken)
|
||||
CancellationToken cancellationToken)
|
||||
=> Task.FromResult(Result.Success(Guid.NewGuid().ToString()));
|
||||
|
||||
public Task<Result> RemoveClientAsync(
|
||||
@@ -41,7 +41,7 @@ public sealed class FakeXuiPanelGateway : IXuiPanelGateway
|
||||
|
||||
public Task<Result> UpdateClientAsync(
|
||||
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol,
|
||||
string name, int deviceLimit, bool enable, CancellationToken cancellationToken)
|
||||
string name, bool enable, CancellationToken cancellationToken)
|
||||
=> Task.FromResult(Result.Success());
|
||||
|
||||
public Task<Result<string>> BuildConnectionStringAsync(
|
||||
|
||||
Reference in New Issue
Block a user