Refactor VPN configuration handling to remove device limit management
CI / Backend (build + test) (push) Successful in 1m24s
CI / Frontend (lint + typecheck + build) (push) Successful in 30s

- 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:
Leonid Pershin
2026-07-02 23:21:26 +03:00
parent 05d49a8cbd
commit bea2b5fcf7
43 changed files with 933 additions and 157 deletions
@@ -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);
@@ -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);
@@ -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);
@@ -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);
@@ -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));