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:
@@ -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);
|
||||
|
||||
+28
-8
@@ -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
-1
@@ -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);
|
||||
|
||||
+8
-3
@@ -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);
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
+20
-6
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user