Refactor project files for improved readability and structure
CI / Backend (build + test) (push) Successful in 1m18s
CI / Frontend (lint + typecheck + build) (push) Successful in 31s

- 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:
Leonid Pershin
2026-07-14 07:24:13 +03:00
parent 9d5424bb9c
commit df137ca5a7
285 changed files with 6911 additions and 2063 deletions
@@ -4,18 +4,25 @@ namespace PnvPanel.Application.Configs;
public static class ConfigErrors
{
public static readonly Error InboundNotAvailable =
Error.NotFound("Configs.InboundNotAvailable", "Инбаунд недоступен.");
public static readonly Error InboundNotAvailable = Error.NotFound(
"Configs.InboundNotAvailable",
"Инбаунд недоступен."
);
public static readonly Error InboundNotAllowedForRole =
Error.Forbidden("Configs.InboundNotAllowedForRole", "Ваша роль не даёт доступ к этому инбаунду.");
public static readonly Error InboundNotAllowedForRole = Error.Forbidden(
"Configs.InboundNotAllowedForRole",
"Ваша роль не даёт доступ к этому инбаунду."
);
public static readonly Error NodeDisabled =
Error.Forbidden("Configs.NodeDisabled", "Сервер временно недоступен для новых конфигов.");
public static readonly Error NodeDisabled = Error.Forbidden(
"Configs.NodeDisabled",
"Сервер временно недоступен для новых конфигов."
);
public static readonly Error QuotaExceeded =
Error.Conflict("Configs.QuotaExceeded", "Достигнут лимит конфигов для вашей роли.");
public static readonly Error QuotaExceeded = Error.Conflict(
"Configs.QuotaExceeded",
"Достигнут лимит конфигов для вашей роли."
);
public static readonly Error NotFound =
Error.NotFound("Configs.NotFound", "Конфиг не найден.");
public static readonly Error NotFound = Error.NotFound("Configs.NotFound", "Конфиг не найден.");
}
@@ -3,4 +3,6 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Configs.Create;
public sealed record CreateVpnConfigCommand(Guid InboundId, string? Label) : ICommand<Result<VpnConfigDto>>, IRequiresActivation;
public sealed record CreateVpnConfigCommand(Guid InboundId, string? Label)
: ICommand<Result<VpnConfigDto>>,
IRequiresActivation;
@@ -8,10 +8,16 @@ using PnvPanel.Domain.Configs;
namespace PnvPanel.Application.Configs.Create;
public sealed class CreateVpnConfigCommandHandler(
IAppDbContext dbContext, IIdentityService identityService, IXuiPanelGateway gateway, ICurrentUser currentUser)
: ICommandHandler<CreateVpnConfigCommand, Result<VpnConfigDto>>
IAppDbContext dbContext,
IIdentityService identityService,
IXuiPanelGateway gateway,
ICurrentUser currentUser
) : ICommandHandler<CreateVpnConfigCommand, Result<VpnConfigDto>>
{
public async Task<Result<VpnConfigDto>> Handle(CreateVpnConfigCommand command, CancellationToken cancellationToken)
public async Task<Result<VpnConfigDto>> Handle(
CreateVpnConfigCommand command,
CancellationToken cancellationToken
)
{
if (currentUser.UserId is not { } userId)
return Result.Failure<VpnConfigDto>(AuthErrors.Unauthorized);
@@ -20,7 +26,8 @@ public sealed class CreateVpnConfigCommandHandler(
if (profile is null)
return Result.Failure<VpnConfigDto>(AuthErrors.Unauthorized);
var inbound = await dbContext.Inbounds.AsNoTracking()
var inbound = await dbContext
.Inbounds.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == command.InboundId, cancellationToken);
if (inbound is null || !inbound.IsPublished)
@@ -29,19 +36,32 @@ public sealed class CreateVpnConfigCommandHandler(
if (!inbound.AllowedRoleIds.Contains(profile.RoleId))
return Result.Failure<VpnConfigDto>(ConfigErrors.InboundNotAllowedForRole);
var node = await dbContext.Nodes.AsNoTracking().FirstOrDefaultAsync(n => n.Id == inbound.NodeId, cancellationToken);
var node = await dbContext
.Nodes.AsNoTracking()
.FirstOrDefaultAsync(n => n.Id == inbound.NodeId, cancellationToken);
if (node is null || !node.IsEnabled)
return Result.Failure<VpnConfigDto>(ConfigErrors.NodeDisabled);
var config = VpnConfig.Create(userId, inbound.Id, inbound.Protocol, command.Label);
var reserveResult = await ReserveQuotaSlotAsync(userId, profile.MaxConfigs, config, cancellationToken);
var reserveResult = await ReserveQuotaSlotAsync(
userId,
profile.MaxConfigs,
config,
cancellationToken
);
if (!reserveResult.IsSuccess)
return Result.Failure<VpnConfigDto>(reserveResult.Error);
var addResult = await gateway.AddClientAsync(
node, inbound.RemoteInboundId, inbound.Protocol, config.ClientEmail,
config.Label ?? config.ClientEmail, profile.MaxIpLimit, cancellationToken);
node,
inbound.RemoteInboundId,
inbound.Protocol,
config.ClientEmail,
config.Label ?? config.ClientEmail,
profile.MaxIpLimit,
cancellationToken
);
if (!addResult.IsSuccess)
{
@@ -64,15 +84,26 @@ public sealed class CreateVpnConfigCommandHandler(
/// НЕ на время внешнего HTTP-вызова к 3x-ui — иначе рискуем держать соединение к БД открытым
/// на секунды под внешним I/O.
/// </summary>
private async Task<Result> ReserveQuotaSlotAsync(Guid userId, int maxConfigs, VpnConfig config, CancellationToken cancellationToken)
private async Task<Result> ReserveQuotaSlotAsync(
Guid userId,
int maxConfigs,
VpnConfig config,
CancellationToken cancellationToken
)
{
await using var transaction = await dbContext.Database.BeginTransactionAsync(cancellationToken);
await using var transaction = await dbContext.Database.BeginTransactionAsync(
cancellationToken
);
await dbContext.Database.ExecuteSqlInterpolatedAsync(
$"SELECT pg_advisory_xact_lock(hashtext({userId.ToString()}))", cancellationToken);
$"SELECT pg_advisory_xact_lock(hashtext({userId.ToString()}))",
cancellationToken
);
var activeCount = await dbContext.VpnConfigs
.CountAsync(c => c.UserId == userId && c.Status == ConfigStatus.Active, cancellationToken);
var activeCount = await dbContext.VpnConfigs.CountAsync(
c => c.UserId == userId && c.Status == ConfigStatus.Active,
cancellationToken
);
if (maxConfigs != RoleQuota.Unlimited && activeCount >= maxConfigs)
{
@@ -3,4 +3,6 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Configs.Edit;
public sealed record EditVpnConfigCommand(Guid ConfigId, string? Label) : ICommand<Result<VpnConfigDto>>, IRequiresActivation;
public sealed record EditVpnConfigCommand(Guid ConfigId, string? Label)
: ICommand<Result<VpnConfigDto>>,
IRequiresActivation;
@@ -6,20 +6,29 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Configs.Edit;
public sealed class EditVpnConfigCommandHandler(IAppDbContext dbContext, IXuiPanelGateway gateway, ICurrentUser currentUser)
: ICommandHandler<EditVpnConfigCommand, Result<VpnConfigDto>>
public sealed class EditVpnConfigCommandHandler(
IAppDbContext dbContext,
IXuiPanelGateway gateway,
ICurrentUser currentUser
) : ICommandHandler<EditVpnConfigCommand, Result<VpnConfigDto>>
{
public async Task<Result<VpnConfigDto>> Handle(EditVpnConfigCommand command, CancellationToken cancellationToken)
public async Task<Result<VpnConfigDto>> Handle(
EditVpnConfigCommand command,
CancellationToken cancellationToken
)
{
if (currentUser.UserId is not { } userId)
return Result.Failure<VpnConfigDto>(AuthErrors.Unauthorized);
var config = await dbContext.VpnConfigs
.FirstOrDefaultAsync(c => c.Id == command.ConfigId && c.UserId == userId, cancellationToken);
var config = await dbContext.VpnConfigs.FirstOrDefaultAsync(
c => c.Id == command.ConfigId && c.UserId == userId,
cancellationToken
);
if (config is null)
return Result.Failure<VpnConfigDto>(ConfigErrors.NotFound);
var inbound = await dbContext.Inbounds.AsNoTracking()
var inbound = await dbContext
.Inbounds.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == config.InboundId, cancellationToken);
if (inbound is null)
return Result.Failure<VpnConfigDto>(ConfigErrors.InboundNotAvailable);
@@ -28,12 +37,20 @@ public sealed class EditVpnConfigCommandHandler(IAppDbContext dbContext, IXuiPan
{
config.Rename(command.Label);
var node = await dbContext.Nodes.AsNoTracking().FirstOrDefaultAsync(n => n.Id == inbound.NodeId, cancellationToken);
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, enable: true, cancellationToken);
node,
inbound.RemoteInboundId,
config.ClientExternalId,
config.Protocol,
config.Label ?? config.ClientEmail,
enable: true,
cancellationToken
);
}
}
@@ -3,7 +3,9 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Configs.GetConfigLink;
public sealed record GetConfigLinkQuery(Guid ConfigId) : IQuery<Result<ConfigLinkDto>>, IRequiresActivation;
public sealed record GetConfigLinkQuery(Guid ConfigId)
: IQuery<Result<ConfigLinkDto>>,
IRequiresActivation;
/// <summary>SubscriptionToken — Api-слой строит из него абсолютный URL (знает scheme/host запроса).</summary>
public sealed record ConfigLinkDto(string ConnectionString, string SubscriptionToken);
@@ -6,29 +6,49 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Configs.GetConfigLink;
public sealed class GetConfigLinkQueryHandler(IAppDbContext dbContext, IXuiPanelGateway gateway, ICurrentUser currentUser)
: IQueryHandler<GetConfigLinkQuery, Result<ConfigLinkDto>>
public sealed class GetConfigLinkQueryHandler(
IAppDbContext dbContext,
IXuiPanelGateway gateway,
ICurrentUser currentUser
) : IQueryHandler<GetConfigLinkQuery, Result<ConfigLinkDto>>
{
public async Task<Result<ConfigLinkDto>> Handle(GetConfigLinkQuery query, CancellationToken cancellationToken)
public async Task<Result<ConfigLinkDto>> Handle(
GetConfigLinkQuery query,
CancellationToken cancellationToken
)
{
if (currentUser.UserId is not { } userId)
return Result.Failure<ConfigLinkDto>(AuthErrors.Unauthorized);
var config = await dbContext.VpnConfigs.AsNoTracking()
.FirstOrDefaultAsync(c => c.Id == query.ConfigId && c.UserId == userId, cancellationToken);
var config = await dbContext
.VpnConfigs.AsNoTracking()
.FirstOrDefaultAsync(
c => c.Id == query.ConfigId && c.UserId == userId,
cancellationToken
);
if (config is null)
return Result.Failure<ConfigLinkDto>(ConfigErrors.NotFound);
var inbound = await dbContext.Inbounds.AsNoTracking().FirstOrDefaultAsync(i => i.Id == config.InboundId, cancellationToken);
var inbound = await dbContext
.Inbounds.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == config.InboundId, cancellationToken);
if (inbound is null)
return Result.Failure<ConfigLinkDto>(ConfigErrors.InboundNotAvailable);
var node = await dbContext.Nodes.AsNoTracking().FirstOrDefaultAsync(n => n.Id == inbound.NodeId, cancellationToken);
var node = await dbContext
.Nodes.AsNoTracking()
.FirstOrDefaultAsync(n => n.Id == inbound.NodeId, cancellationToken);
if (node is null)
return Result.Failure<ConfigLinkDto>(ConfigErrors.NodeDisabled);
var linkResult = await gateway.BuildConnectionStringAsync(
node, inbound, config.ClientExternalId, config.Label ?? config.ClientEmail, node.BaseAddress.Host, cancellationToken);
node,
inbound,
config.ClientExternalId,
config.Label ?? config.ClientEmail,
node.BaseAddress.Host,
cancellationToken
);
if (!linkResult.IsSuccess)
return Result.Failure<ConfigLinkDto>(linkResult.Error);
@@ -7,10 +7,16 @@ using PnvPanel.Domain.Configs;
namespace PnvPanel.Application.Configs.GetMyConfigs;
public sealed class GetMyConfigsQueryHandler(IAppDbContext dbContext, IIdentityService identityService, ICurrentUser currentUser)
: IQueryHandler<GetMyConfigsQuery, Result<GetMyConfigsResult>>
public sealed class GetMyConfigsQueryHandler(
IAppDbContext dbContext,
IIdentityService identityService,
ICurrentUser currentUser
) : IQueryHandler<GetMyConfigsQuery, Result<GetMyConfigsResult>>
{
public async Task<Result<GetMyConfigsResult>> Handle(GetMyConfigsQuery query, CancellationToken cancellationToken)
public async Task<Result<GetMyConfigsResult>> Handle(
GetMyConfigsQuery query,
CancellationToken cancellationToken
)
{
if (currentUser.UserId is not { } userId)
return Result.Failure<GetMyConfigsResult>(AuthErrors.Unauthorized);
@@ -19,9 +25,15 @@ public sealed class GetMyConfigsQueryHandler(IAppDbContext dbContext, IIdentityS
if (profile is null)
return Result.Failure<GetMyConfigsResult>(AuthErrors.Unauthorized);
var rows = await dbContext.VpnConfigs.AsNoTracking()
var rows = await dbContext
.VpnConfigs.AsNoTracking()
.Where(c => c.UserId == userId && c.Status != ConfigStatus.Revoked)
.Join(dbContext.Inbounds.AsNoTracking(), c => c.InboundId, i => i.Id, (c, i) => new { Config = c, Inbound = i })
.Join(
dbContext.Inbounds.AsNoTracking(),
c => c.InboundId,
i => i.Id,
(c, i) => new { Config = c, Inbound = i }
)
.OrderByDescending(x => x.Config.CreatedAt)
.ToListAsync(cancellationToken);
@@ -3,7 +3,9 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Configs.GetMySubscription;
public sealed record GetMySubscriptionQuery : IQuery<Result<MySubscriptionDto>>, IRequiresActivation;
public sealed record GetMySubscriptionQuery
: IQuery<Result<MySubscriptionDto>>,
IRequiresActivation;
/// <summary>SubscriptionToken — Api-слой строит из него абсолютный URL (знает scheme/host запроса), см. GetConfigLinkQuery.</summary>
public sealed record MySubscriptionDto(string SubscriptionToken);
@@ -5,10 +5,15 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Configs.GetMySubscription;
public sealed class GetMySubscriptionQueryHandler(IIdentityService identityService, ICurrentUser currentUser)
: IQueryHandler<GetMySubscriptionQuery, Result<MySubscriptionDto>>
public sealed class GetMySubscriptionQueryHandler(
IIdentityService identityService,
ICurrentUser currentUser
) : IQueryHandler<GetMySubscriptionQuery, Result<MySubscriptionDto>>
{
public async Task<Result<MySubscriptionDto>> Handle(GetMySubscriptionQuery query, CancellationToken cancellationToken)
public async Task<Result<MySubscriptionDto>> Handle(
GetMySubscriptionQuery query,
CancellationToken cancellationToken
)
{
if (currentUser.UserId is not { } userId)
return Result.Failure<MySubscriptionDto>(AuthErrors.Unauthorized);
@@ -4,7 +4,9 @@ using PnvPanel.Domain.Inbounds;
namespace PnvPanel.Application.Configs.ListAvailableInbounds;
public sealed record ListAvailableInboundsQuery : IQuery<Result<IReadOnlyList<AvailableInboundDto>>>, IRequiresActivation;
public sealed record ListAvailableInboundsQuery
: IQuery<Result<IReadOnlyList<AvailableInboundDto>>>,
IRequiresActivation;
/// <summary>Витринная карточка инбаунда для выбора при создании конфига — без деталей 3x-ui.</summary>
public sealed record AvailableInboundDto(Guid InboundId, string DisplayName, VpnProtocol Protocol);
@@ -6,10 +6,16 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Configs.ListAvailableInbounds;
public sealed class ListAvailableInboundsQueryHandler(IAppDbContext dbContext, IIdentityService identityService, ICurrentUser currentUser)
: IQueryHandler<ListAvailableInboundsQuery, Result<IReadOnlyList<AvailableInboundDto>>>
public sealed class ListAvailableInboundsQueryHandler(
IAppDbContext dbContext,
IIdentityService identityService,
ICurrentUser currentUser
) : IQueryHandler<ListAvailableInboundsQuery, Result<IReadOnlyList<AvailableInboundDto>>>
{
public async Task<Result<IReadOnlyList<AvailableInboundDto>>> Handle(ListAvailableInboundsQuery query, CancellationToken cancellationToken)
public async Task<Result<IReadOnlyList<AvailableInboundDto>>> Handle(
ListAvailableInboundsQuery query,
CancellationToken cancellationToken
)
{
if (currentUser.UserId is not { } userId)
return Result.Failure<IReadOnlyList<AvailableInboundDto>>(AuthErrors.Unauthorized);
@@ -18,10 +24,18 @@ public sealed class ListAvailableInboundsQueryHandler(IAppDbContext dbContext, I
if (profile is null)
return Result.Failure<IReadOnlyList<AvailableInboundDto>>(AuthErrors.Unauthorized);
var enabledNodeIds = dbContext.Nodes.AsNoTracking().Where(n => n.IsEnabled).Select(n => n.Id);
var enabledNodeIds = dbContext
.Nodes.AsNoTracking()
.Where(n => n.IsEnabled)
.Select(n => n.Id);
var inbounds = await dbContext.Inbounds.AsNoTracking()
.Where(i => i.IsPublished && enabledNodeIds.Contains(i.NodeId) && i.AllowedRoleIds.Contains(profile.RoleId))
var inbounds = await dbContext
.Inbounds.AsNoTracking()
.Where(i =>
i.IsPublished
&& enabledNodeIds.Contains(i.NodeId)
&& i.AllowedRoleIds.Contains(profile.RoleId)
)
.OrderBy(i => i.DisplayName ?? i.Remark)
.Select(i => new AvailableInboundDto(i.Id, i.DisplayName ?? i.Remark, i.Protocol))
.ToListAsync(cancellationToken);
@@ -8,32 +8,55 @@ using PnvPanel.Domain.Configs;
namespace PnvPanel.Application.Configs.Revoke;
public sealed class RevokeVpnConfigCommandHandler(
IAppDbContext dbContext, IXuiPanelGateway gateway, IRealtimeNotifier notifier, ICurrentUser currentUser)
: ICommandHandler<RevokeVpnConfigCommand, Result>
IAppDbContext dbContext,
IXuiPanelGateway gateway,
IRealtimeNotifier notifier,
ICurrentUser currentUser
) : ICommandHandler<RevokeVpnConfigCommand, Result>
{
public async Task<Result> Handle(RevokeVpnConfigCommand command, CancellationToken cancellationToken)
public async Task<Result> Handle(
RevokeVpnConfigCommand command,
CancellationToken cancellationToken
)
{
if (currentUser.UserId is not { } userId)
return Result.Failure(AuthErrors.Unauthorized);
var config = await dbContext.VpnConfigs
.FirstOrDefaultAsync(c => c.Id == command.ConfigId && c.UserId == userId, cancellationToken);
var config = await dbContext.VpnConfigs.FirstOrDefaultAsync(
c => c.Id == command.ConfigId && c.UserId == userId,
cancellationToken
);
if (config is null)
return Result.Failure(ConfigErrors.NotFound);
if (config.Status == ConfigStatus.Revoked)
return Result.Success();
var inbound = await dbContext.Inbounds.AsNoTracking().FirstOrDefaultAsync(i => i.Id == config.InboundId, cancellationToken);
var inbound = await dbContext
.Inbounds.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == config.InboundId, cancellationToken);
var node = inbound is null
? null
: await dbContext.Nodes.AsNoTracking().FirstOrDefaultAsync(n => n.Id == inbound.NodeId, cancellationToken);
: await dbContext
.Nodes.AsNoTracking()
.FirstOrDefaultAsync(n => n.Id == inbound.NodeId, cancellationToken);
if (inbound is not null && node is not null)
await gateway.RemoveClientAsync(node, inbound.RemoteInboundId, config.ClientExternalId, config.Protocol, cancellationToken);
await gateway.RemoveClientAsync(
node,
inbound.RemoteInboundId,
config.ClientExternalId,
config.Protocol,
cancellationToken
);
config.Revoke();
await notifier.NotifyConfigStatusChangedAsync(userId, config.Id, config.Status, cancellationToken);
await notifier.NotifyConfigStatusChangedAsync(
userId,
config.Id,
config.Status,
cancellationToken
);
return Result.Success();
}
@@ -3,4 +3,6 @@ using PnvPanel.Application.Common.Models;
namespace PnvPanel.Application.Configs.Rotate;
public sealed record RotateVpnConfigCommand(Guid ConfigId) : ICommand<Result<VpnConfigDto>>, IRequiresActivation;
public sealed record RotateVpnConfigCommand(Guid ConfigId)
: ICommand<Result<VpnConfigDto>>,
IRequiresActivation;
@@ -8,28 +8,39 @@ using PnvPanel.Domain.Configs;
namespace PnvPanel.Application.Configs.Rotate;
public sealed class RotateVpnConfigCommandHandler(
IAppDbContext dbContext, IXuiPanelGateway gateway, IIdentityService identityService, ICurrentUser currentUser)
: ICommandHandler<RotateVpnConfigCommand, Result<VpnConfigDto>>
IAppDbContext dbContext,
IXuiPanelGateway gateway,
IIdentityService identityService,
ICurrentUser currentUser
) : ICommandHandler<RotateVpnConfigCommand, Result<VpnConfigDto>>
{
public async Task<Result<VpnConfigDto>> Handle(RotateVpnConfigCommand command, CancellationToken cancellationToken)
public async Task<Result<VpnConfigDto>> Handle(
RotateVpnConfigCommand command,
CancellationToken cancellationToken
)
{
if (currentUser.UserId is not { } userId)
return Result.Failure<VpnConfigDto>(AuthErrors.Unauthorized);
var config = await dbContext.VpnConfigs
.FirstOrDefaultAsync(c => c.Id == command.ConfigId && c.UserId == userId, cancellationToken);
var config = await dbContext.VpnConfigs.FirstOrDefaultAsync(
c => c.Id == command.ConfigId && c.UserId == userId,
cancellationToken
);
if (config is null)
return Result.Failure<VpnConfigDto>(ConfigErrors.NotFound);
if (config.Status != ConfigStatus.Active)
return Result.Failure<VpnConfigDto>(ConfigErrors.NotFound);
var inbound = await dbContext.Inbounds.AsNoTracking()
var inbound = await dbContext
.Inbounds.AsNoTracking()
.FirstOrDefaultAsync(i => i.Id == config.InboundId, cancellationToken);
if (inbound is null)
return Result.Failure<VpnConfigDto>(ConfigErrors.InboundNotAvailable);
var node = await dbContext.Nodes.AsNoTracking().FirstOrDefaultAsync(n => n.Id == inbound.NodeId, cancellationToken);
var node = await dbContext
.Nodes.AsNoTracking()
.FirstOrDefaultAsync(n => n.Id == inbound.NodeId, cancellationToken);
if (node is null)
return Result.Failure<VpnConfigDto>(ConfigErrors.NodeDisabled);
@@ -39,8 +50,14 @@ public sealed class RotateVpnConfigCommandHandler(
var newClientEmail = VpnConfig.GenerateClientEmail(userId);
var addResult = await gateway.AddClientAsync(
node, inbound.RemoteInboundId, config.Protocol, newClientEmail,
config.Label ?? newClientEmail, profile.MaxIpLimit, cancellationToken);
node,
inbound.RemoteInboundId,
config.Protocol,
newClientEmail,
config.Label ?? newClientEmail,
profile.MaxIpLimit,
cancellationToken
);
if (!addResult.IsSuccess)
return Result.Failure<VpnConfigDto>(addResult.Error);
@@ -51,7 +68,13 @@ public sealed class RotateVpnConfigCommandHandler(
// Старого клиента удаляем ПОСЛЕ коммита нового состояния: если удаление не выйдет,
// у пользователя просто останется лишний нерабочий-для-него клиент в панели — не критично.
await gateway.RemoveClientAsync(node, inbound.RemoteInboundId, oldClientExternalId, config.Protocol, cancellationToken);
await gateway.RemoveClientAsync(
node,
inbound.RemoteInboundId,
oldClientExternalId,
config.Protocol,
cancellationToken
);
return Result.Success(VpnConfigDto.FromDomain(config, inbound));
}
@@ -10,10 +10,29 @@ namespace PnvPanel.Application.Configs;
/// (сверка конфига с записью в панели у админа).
/// </summary>
public sealed record VpnConfigDto(
Guid Id, string? Label, string ClientEmail, VpnProtocol Protocol, string Location,
long UsedUpBytes, long UsedDownBytes, DateTimeOffset? ExpiresAt, ConfigStatus Status, DateTimeOffset CreatedAt)
Guid Id,
string? Label,
string ClientEmail,
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.ClientEmail, config.Protocol, inbound.DisplayName ?? inbound.Remark,
config.UsedUpBytes, config.UsedDownBytes, config.ExpiresAt, config.Status, config.CreatedAt);
public static VpnConfigDto FromDomain(VpnConfig config, Inbound inbound) =>
new(
config.Id,
config.Label,
config.ClientEmail,
config.Protocol,
inbound.DisplayName ?? inbound.Remark,
config.UsedUpBytes,
config.UsedDownBytes,
config.ExpiresAt,
config.Status,
config.CreatedAt
);
}