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
@@ -5,10 +5,25 @@ namespace PnvPanel.Application.Common.Interfaces;
public sealed record AuthenticatedUser(Guid Id, string UserName, string Role);
public sealed record CurrentUserProfile(
Guid Id, string UserName, Guid RoleId, string Role, bool IsActivated, bool IsBlocked, int MaxConfigs, int MaxIpLimit,
string SubscriptionToken);
Guid Id,
string UserName,
Guid RoleId,
string Role,
bool IsActivated,
bool IsBlocked,
int MaxConfigs,
int MaxIpLimit,
string SubscriptionToken
);
public sealed record UserSummaryDto(Guid Id, string UserName, string Role, bool IsActivated, bool IsBlocked, DateTimeOffset? ActivatedAt);
public sealed record UserSummaryDto(
Guid Id,
string UserName,
string Role,
bool IsActivated,
bool IsBlocked,
DateTimeOffset? ActivatedAt
);
public sealed record UserStatsDto(int Total, int Activated);
@@ -16,30 +31,57 @@ public sealed record TelegramLinkInfo(bool IsLinked, long? TelegramUserId, strin
public interface IIdentityService
{
Task<Result<Guid>> CreateUserAsync(string userName, string password, CancellationToken cancellationToken);
Task<Result<Guid>> CreateUserAsync(
string userName,
string password,
CancellationToken cancellationToken
);
Task<Result<AuthenticatedUser>> ValidateCredentialsAsync(string userName, string password, CancellationToken cancellationToken);
Task<Result<AuthenticatedUser>> ValidateCredentialsAsync(
string userName,
string password,
CancellationToken cancellationToken
);
Task<CurrentUserProfile?> GetProfileAsync(Guid userId, CancellationToken cancellationToken);
Task<Result> ChangePasswordAsync(Guid userId, string currentPassword, string newPassword, CancellationToken cancellationToken);
Task<Result> ChangePasswordAsync(
Guid userId,
string currentPassword,
string newPassword,
CancellationToken cancellationToken
);
Task<Result> ChangeUserNameAsync(Guid userId, string newUserName, CancellationToken cancellationToken);
Task<Result> ChangeUserNameAsync(
Guid userId,
string newUserName,
CancellationToken cancellationToken
);
/// <summary>
/// Помечает пользователя активированным. Изменение не коммитится немедленно (в отличие от
/// CreateUserAsync/ChangePasswordAsync) — оно попадает в трекер того же DbContext и сохраняется
/// вместе с изменением ActivationRequest одной транзакцией через UnitOfWorkBehavior.
/// </summary>
Task<Result> ActivateUserAsync(Guid userId, Guid activatedBy, CancellationToken cancellationToken);
Task<Result> ActivateUserAsync(
Guid userId,
Guid activatedBy,
CancellationToken cancellationToken
);
Task<IReadOnlyDictionary<Guid, string>> GetUserNamesAsync(IReadOnlyCollection<Guid> userIds, CancellationToken cancellationToken);
Task<IReadOnlyDictionary<Guid, string>> GetUserNamesAsync(
IReadOnlyCollection<Guid> userIds,
CancellationToken cancellationToken
);
/// <summary>Удаляет аккаунт (самоудаление). Конфиги должны быть отозваны заранее вызывающей стороной.</summary>
Task<Result> DeleteUserAsync(Guid userId, CancellationToken cancellationToken);
/// <summary>Для агрегированной подписки /sub/{userToken} (все активные конфиги пользователя).</summary>
Task<Guid?> FindUserIdBySubscriptionTokenAsync(string token, CancellationToken cancellationToken);
Task<Guid?> FindUserIdBySubscriptionTokenAsync(
string token,
CancellationToken cancellationToken
);
/// <summary>Блокировка: вход запрещён (см. ValidateCredentialsAsync). Конфиги гасит вызывающая сторона.</summary>
Task<Result> BlockUserAsync(Guid userId, CancellationToken cancellationToken);
@@ -47,21 +89,43 @@ public interface IIdentityService
Task<Result> UnblockUserAsync(Guid userId, CancellationToken cancellationToken);
/// <summary>Сброс пароля админом — для пользователей без привязанного Telegram (M7).</summary>
Task<Result> ResetPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken);
Task<Result> ResetPasswordAsync(
Guid userId,
string newPassword,
CancellationToken cancellationToken
);
Task<PagedList<UserSummaryDto>> ListUsersAsync(int page, int pageSize, string? search, CancellationToken cancellationToken);
Task<PagedList<UserSummaryDto>> ListUsersAsync(
int page,
int pageSize,
string? search,
CancellationToken cancellationToken
);
Task<UserStatsDto> GetUserStatsAsync(CancellationToken cancellationToken);
Task<Result> LinkTelegramAsync(Guid userId, long telegramUserId, string? telegramUsername, CancellationToken cancellationToken);
Task<Result> LinkTelegramAsync(
Guid userId,
long telegramUserId,
string? telegramUsername,
CancellationToken cancellationToken
);
Task<Result> UnlinkTelegramAsync(Guid userId, CancellationToken cancellationToken);
Task<Guid?> FindUserIdByTelegramUserIdAsync(long telegramUserId, CancellationToken cancellationToken);
Task<Guid?> FindUserIdByTelegramUserIdAsync(
long telegramUserId,
CancellationToken cancellationToken
);
Task<TelegramLinkInfo> GetTelegramLinkInfoAsync(Guid userId, CancellationToken cancellationToken);
Task<TelegramLinkInfo> GetTelegramLinkInfoAsync(
Guid userId,
CancellationToken cancellationToken
);
/// <summary>Telegram ID активных (активированных, не заблокированных) пользователей с привязкой —
/// для рассылки уведомлений вроде публикации новости.</summary>
Task<IReadOnlyCollection<long>> GetActivatedLinkedTelegramUserIdsAsync(CancellationToken cancellationToken);
Task<IReadOnlyCollection<long>> GetActivatedLinkedTelegramUserIdsAsync(
CancellationToken cancellationToken
);
}
@@ -12,21 +12,53 @@ namespace PnvPanel.Application.Common.Interfaces;
public interface IRealtimeNotifier
{
Task NotifyConfigTrafficUpdatedAsync(
Guid userId, Guid configId, long usedUpBytes, long usedDownBytes, CancellationToken cancellationToken);
Guid userId,
Guid configId,
long usedUpBytes,
long usedDownBytes,
CancellationToken cancellationToken
);
Task NotifyConfigStatusChangedAsync(Guid userId, Guid configId, ConfigStatus status, CancellationToken cancellationToken);
Task NotifyConfigStatusChangedAsync(
Guid userId,
Guid configId,
ConfigStatus status,
CancellationToken cancellationToken
);
Task NotifyNodeStatusChangedAsync(Guid nodeId, NodeStatus status, DateTimeOffset? lastSyncAt, CancellationToken cancellationToken);
Task NotifyNodeStatusChangedAsync(
Guid nodeId,
NodeStatus status,
DateTimeOffset? lastSyncAt,
CancellationToken cancellationToken
);
Task NotifyActivationRequestedAsync(
Guid requestId, Guid userId, string userName, string? comment, DateTimeOffset createdAt, CancellationToken cancellationToken);
Guid requestId,
Guid userId,
string userName,
string? comment,
DateTimeOffset createdAt,
CancellationToken cancellationToken
);
Task NotifyUserActivatedAsync(Guid userId, CancellationToken cancellationToken);
/// <summary>Единственное широковещательное событие (всем подключенным клиентам), а не по группе.</summary>
Task NotifyNewsPublishedAsync(Guid postId, string title, DateTimeOffset createdAt, CancellationToken cancellationToken);
Task NotifyNewsPublishedAsync(
Guid postId,
string title,
DateTimeOffset createdAt,
CancellationToken cancellationToken
);
Task NotifyTicketCreatedAsync(Guid ticketId, Guid userId, string userName, TicketType type, CancellationToken cancellationToken);
Task NotifyTicketCreatedAsync(
Guid ticketId,
Guid userId,
string userName,
TicketType type,
CancellationToken cancellationToken
);
/// <summary>Новый комментарий или смена статуса — пушится автору тикета (не всем участникам треда).</summary>
Task NotifyTicketUpdatedAsync(Guid ticketId, Guid userId, CancellationToken cancellationToken);
@@ -10,7 +10,10 @@ public interface IRefreshTokenService
{
Task<IssuedRefreshToken> IssueAsync(Guid userId, CancellationToken cancellationToken);
Task<Result<RotatedRefreshToken>> RotateAsync(string rawToken, CancellationToken cancellationToken);
Task<Result<RotatedRefreshToken>> RotateAsync(
string rawToken,
CancellationToken cancellationToken
);
Task RevokeAsync(string rawToken, CancellationToken cancellationToken);
}
@@ -6,9 +6,19 @@ public sealed record RoleDto(Guid Id, string Name, int MaxConfigs, int MaxIpLimi
public interface IRoleService
{
Task<Result<RoleDto>> CreateRoleAsync(string name, int maxConfigs, int maxIpLimit, CancellationToken cancellationToken);
Task<Result<RoleDto>> CreateRoleAsync(
string name,
int maxConfigs,
int maxIpLimit,
CancellationToken cancellationToken
);
Task<Result<RoleDto>> UpdateRoleAsync(Guid roleId, int maxConfigs, int maxIpLimit, CancellationToken cancellationToken);
Task<Result<RoleDto>> UpdateRoleAsync(
Guid roleId,
int maxConfigs,
int maxIpLimit,
CancellationToken cancellationToken
);
Task<Result> DeleteRoleAsync(Guid roleId, CancellationToken cancellationToken);
@@ -10,7 +10,11 @@ namespace PnvPanel.Application.Common.Interfaces;
public interface ITelegramNotifier
{
Task NotifyAdminsActivationRequestedAsync(
Guid requestId, string userName, string? comment, CancellationToken cancellationToken);
Guid requestId,
string userName,
string? comment,
CancellationToken cancellationToken
);
/// <summary>Личное сообщение пользователю, если у него привязан Telegram (иначе no-op).
/// Если задан PublicSiteUrl — добавляет кнопку-ссылку на сайт.</summary>
@@ -18,11 +22,21 @@ public interface ITelegramNotifier
/// <summary>Баг-репорт/предложение — только кнопка-ссылка на сайт (переписка и картинки — там),
/// без инлайн-действий.</summary>
Task NotifyAdminsBugReportCreatedAsync(Guid ticketId, string userName, string message, CancellationToken cancellationToken);
Task NotifyAdminsBugReportCreatedAsync(
Guid ticketId,
string userName,
string message,
CancellationToken cancellationToken
);
/// <summary>Заявка на роль — инлайн-кнопки «Одобрить/Отклонить», решается полностью в Telegram.</summary>
Task NotifyAdminsRoleRequestCreatedAsync(
Guid ticketId, string userName, string roleDescription, string justification, CancellationToken cancellationToken);
Guid ticketId,
string userName,
string roleDescription,
string justification,
CancellationToken cancellationToken
);
/// <summary>Рассылка о публикации новости всем активированным пользователям с привязанным Telegram
/// (кнопка-ссылка на сайт, где новость показана целиком).</summary>
@@ -30,5 +44,10 @@ public interface ITelegramNotifier
/// <summary>Пользователь переоткрыл решённый тикет — только кнопка-ссылка на сайт, без
/// инлайн-действий (аналогично баг-репортам).</summary>
Task NotifyAdminsTicketReopenedAsync(Guid ticketId, string userName, TicketType type, CancellationToken cancellationToken);
Task NotifyAdminsTicketReopenedAsync(
Guid ticketId,
string userName,
TicketType type,
CancellationToken cancellationToken
);
}
@@ -4,7 +4,12 @@ using PnvPanel.Domain.Nodes;
namespace PnvPanel.Application.Common.Interfaces;
public sealed record RemoteInboundInfo(string RemoteInboundId, VpnProtocol Protocol, string Remark, int Port);
public sealed record RemoteInboundInfo(
string RemoteInboundId,
VpnProtocol Protocol,
string Remark,
int Port
);
public sealed record NodeProbeResult(bool IsReachable, string? ErrorMessage);
@@ -20,7 +25,10 @@ public interface IXuiPanelGateway
Task<NodeProbeResult> ProbeAsync(Node node, CancellationToken cancellationToken);
Task<Result<IReadOnlyList<RemoteInboundInfo>>> ListInboundsAsync(Node node, CancellationToken cancellationToken);
Task<Result<IReadOnlyList<RemoteInboundInfo>>> ListInboundsAsync(
Node node,
CancellationToken cancellationToken
);
void InvalidateClient(Guid nodeId);
@@ -30,25 +38,49 @@ public interface IXuiPanelGateway
/// -1 (RoleQuota.Unlimited) означает без лимита — гейтвей сам переводит его в нативное значение 3x-ui.
/// </summary>
Task<Result<string>> AddClientAsync(
Node node, string inboundRemoteId, VpnProtocol protocol, string clientEmail, string clientName, int limitIp,
CancellationToken cancellationToken);
Node node,
string inboundRemoteId,
VpnProtocol protocol,
string clientEmail,
string clientName,
int limitIp,
CancellationToken cancellationToken
);
Task<Result> RemoveClientAsync(
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol,
CancellationToken cancellationToken);
Node node,
string inboundRemoteId,
string clientExternalId,
VpnProtocol protocol,
CancellationToken cancellationToken
);
Task<Result> UpdateClientAsync(
Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol,
string name, bool enable, CancellationToken cancellationToken);
Node node,
string inboundRemoteId,
string clientExternalId,
VpnProtocol protocol,
string name,
bool enable,
CancellationToken cancellationToken
);
Task<Result<string>> BuildConnectionStringAsync(
Node node, Inbound inbound, string clientExternalId, string clientName, string publicHost,
CancellationToken cancellationToken);
Node node,
Inbound inbound,
string clientExternalId,
string clientName,
string publicHost,
CancellationToken cancellationToken
);
/// <summary>
/// Трафик по клиентам инбаунда, ключ — ClientEmail. ThreeXui.Net не даёт типизированного метода
/// для этого — извлекается из сырого clientStats[] в RawInboundJson (стандартное поле 3x-ui API).
/// </summary>
Task<Result<IReadOnlyDictionary<string, ClientTrafficInfo>>> GetClientTrafficAsync(
Node node, string inboundRemoteId, CancellationToken cancellationToken);
Node node,
string inboundRemoteId,
CancellationToken cancellationToken
);
}