From df137ca5a7195a31b7c56f92ab0ed267bfd0b111 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Tue, 14 Jul 2026 07:24:13 +0300 Subject: [PATCH] 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. --- backend/Directory.Build.props | 2 - backend/Directory.Packages.props | 12 +- .../PnvPanel.Api/Common/ResultExtensions.cs | 8 +- .../Endpoints/ActivationEndpoints.cs | 45 +- .../Endpoints/AdminAppEndpoints.cs | 39 +- .../Endpoints/AdminNewsEndpoints.cs | 26 +- .../Endpoints/AdminStatsEndpoints.cs | 7 +- .../Endpoints/AdminSupportEndpoints.cs | 71 +- .../Endpoints/AdminUserEndpoints.cs | 84 ++- .../PnvPanel.Api/Endpoints/AuthEndpoints.cs | 119 +++- .../PnvPanel.Api/Endpoints/ConfigEndpoints.cs | 66 +- .../Endpoints/InboundEndpoints.cs | 26 +- .../PnvPanel.Api/Endpoints/NewsEndpoints.cs | 7 +- .../PnvPanel.Api/Endpoints/NodeEndpoints.cs | 53 +- .../PnvPanel.Api/Endpoints/RoleEndpoints.cs | 45 +- .../Endpoints/SubscriptionEndpoints.cs | 14 +- .../Endpoints/SupportEndpoints.cs | 91 ++- .../Endpoints/TelegramEndpoints.cs | 78 ++- .../Hubs/SignalRRealtimeNotifier.cs | 152 +++-- backend/src/PnvPanel.Api/PnvPanel.Api.csproj | 8 +- backend/src/PnvPanel.Api/Program.cs | 79 ++- .../Telegram/PnvBotUpdateHandler.cs | 612 ++++++++++++++---- .../Telegram/TelegramBotHostedService.cs | 8 +- .../PnvPanel.Api/Telegram/TelegramBotInfo.cs | 5 +- .../PnvPanel.Api/Telegram/TelegramNotifier.cs | 155 ++++- .../Activation/ActivationErrors.cs | 18 +- .../GetActivationStatusQueryHandler.cs | 18 +- .../Activation/RequestActivationCommand.cs | 3 +- .../RequestActivationCommandHandler.cs | 38 +- .../Activation/ActivationRequestAdminDto.cs | 3 +- .../ApproveActivationCommandHandler.cs | 44 +- .../Activation/ListActivationRequestsQuery.cs | 7 +- .../ListActivationRequestsQueryHandler.cs | 30 +- .../RejectActivationCommandHandler.cs | 29 +- .../Admin/Apps/AdminAppDto.cs | 25 +- .../Admin/Apps/AppErrors.cs | 5 +- .../Admin/Apps/CreateAppCommand.cs | 9 +- .../Admin/Apps/CreateAppCommandHandler.cs | 17 +- .../Admin/Apps/DeleteAppCommandHandler.cs | 8 +- .../Admin/Apps/ListAdminAppsQueryHandler.cs | 18 +- .../Admin/Apps/UpdateAppCommand.cs | 12 +- .../Admin/Apps/UpdateAppCommandHandler.cs | 23 +- .../Admin/Audit/ListAuditLogsQuery.cs | 14 +- .../Admin/Audit/ListAuditLogsQueryHandler.cs | 22 +- .../Admin/Configs/AdminVpnConfigDto.cs | 17 +- .../Admin/Configs/ListAllConfigsQuery.cs | 8 +- .../Configs/ListAllConfigsQueryHandler.cs | 75 ++- .../Admin/Inbounds/InboundDto.cs | 32 +- .../Admin/Inbounds/InboundErrors.cs | 5 +- .../Inbounds/ListInboundsQueryHandler.cs | 12 +- .../Admin/Inbounds/PublishInboundCommand.cs | 8 +- .../Inbounds/PublishInboundCommandHandler.cs | 23 +- .../Admin/News/CreatePostCommandHandler.cs | 19 +- .../Admin/News/DeletePostCommandHandler.cs | 8 +- .../Admin/News/ListAdminNewsQuery.cs | 3 +- .../Admin/News/ListAdminNewsQueryHandler.cs | 11 +- .../Admin/News/UpdatePostCommand.cs | 3 +- .../Admin/News/UpdatePostCommandHandler.cs | 13 +- .../Admin/Nodes/DeleteNodeCommandHandler.cs | 28 +- .../Admin/Nodes/ListNodesQueryHandler.cs | 13 +- .../Admin/Nodes/NodeDto.cs | 25 +- .../Admin/Nodes/NodeErrors.cs | 5 +- .../Admin/Nodes/ProbeNodeCommandHandler.cs | 14 +- .../Admin/Nodes/RegisterNodeCommand.cs | 9 +- .../Admin/Nodes/RegisterNodeCommandHandler.cs | 29 +- .../Admin/Nodes/SyncNodeCommandHandler.cs | 30 +- .../Admin/Nodes/UpdateNodeCommand.cs | 9 +- .../Admin/Nodes/UpdateNodeCommandHandler.cs | 38 +- .../Admin/Roles/CreateRoleCommand.cs | 3 +- .../Admin/Roles/CreateRoleCommandHandler.cs | 15 +- .../Admin/Roles/CreateRoleCommandValidator.cs | 5 +- .../Admin/Roles/DeleteRoleCommandHandler.cs | 7 +- .../Admin/Roles/ListRolesQueryHandler.cs | 6 +- .../Admin/Roles/RoleErrors.cs | 15 +- .../Admin/Roles/UpdateRoleCommand.cs | 3 +- .../Admin/Roles/UpdateRoleCommandHandler.cs | 15 +- .../Admin/Stats/GetStatsQuery.cs | 13 +- .../Admin/Stats/GetStatsQueryHandler.cs | 42 +- .../ApproveRoleRequestCommandHandler.cs | 49 +- .../Support/CloseTicketCommandHandler.cs | 35 +- .../Support/GetTicketAdminQueryHandler.cs | 23 +- .../Admin/Support/ListAllTicketsQuery.cs | 8 +- .../Support/ListAllTicketsQueryHandler.cs | 26 +- .../RejectRoleRequestCommandHandler.cs | 35 +- .../Support/ResolveTicketCommandHandler.cs | 35 +- .../Admin/Users/BlockUserCommandHandler.cs | 65 +- .../Users/ChangeUserRoleCommandHandler.cs | 31 +- .../Admin/Users/DeleteUserCommandHandler.cs | 47 +- .../Users/ForceRevokeConfigCommandHandler.cs | 59 +- .../Admin/Users/GetUserConfigsQueryHandler.cs | 15 +- .../Admin/Users/ListUsersQuery.cs | 3 +- .../Admin/Users/ListUsersQueryHandler.cs | 12 +- .../Users/ResetUserPasswordCommandHandler.cs | 30 +- .../Admin/Users/UnblockUserCommandHandler.cs | 67 +- .../Admin/Users/UserErrors.cs | 9 +- .../PnvPanel.Application/Apps/ClientAppDto.cs | 8 +- .../Apps/ListAppsQuery.cs | 4 +- .../Apps/ListAppsQueryHandler.cs | 22 +- .../PnvPanel.Application/Auth/AuthErrors.cs | 42 +- .../PnvPanel.Application/Auth/AuthResult.cs | 3 +- .../ChangePassword/ChangePasswordCommand.cs | 3 +- .../ChangePasswordCommandHandler.cs | 13 +- .../ChangeUserNameCommandHandler.cs | 6 +- .../Auth/CurrentUserDto.cs | 8 +- .../DeleteMyAccountCommandHandler.cs | 33 +- .../Auth/Login/LoginCommandHandler.cs | 42 +- .../Auth/Me/GetCurrentUserQueryHandler.cs | 26 +- .../Auth/Refresh/RefreshCommandHandler.cs | 46 +- .../Auth/Register/RegisterCommand.cs | 3 +- .../Auth/Register/RegisterCommandHandler.cs | 11 +- .../Common/Behaviors/LoggingBehavior.cs | 11 +- .../Behaviors/RequireActivationBehavior.cs | 12 +- .../Common/Behaviors/ResultFailureFactory.cs | 3 +- .../Common/Behaviors/UnitOfWorkBehavior.cs | 6 +- .../Common/Behaviors/ValidationBehavior.cs | 14 +- .../Common/Interfaces/IIdentityService.cs | 96 ++- .../Common/Interfaces/IRealtimeNotifier.cs | 44 +- .../Common/Interfaces/IRefreshTokenService.cs | 5 +- .../Common/Interfaces/IRoleService.cs | 14 +- .../Common/Interfaces/ITelegramNotifier.cs | 27 +- .../Common/Interfaces/IXuiPanelGateway.cs | 54 +- .../Common/Messaging/ICommandHandler.cs | 3 +- .../Common/Messaging/IPipelineBehavior.cs | 6 +- .../Common/Messaging/IQueryHandler.cs | 3 +- .../Common/Messaging/ISender.cs | 10 +- .../Common/Messaging/Sender.cs | 26 +- .../Common/Models/Error.cs | 23 +- .../Common/Models/PagedListExtensions.cs | 11 +- .../Common/Models/Result.cs | 14 +- .../Configs/ConfigErrors.cs | 27 +- .../Configs/Create/CreateVpnConfigCommand.cs | 4 +- .../Create/CreateVpnConfigCommandHandler.cs | 57 +- .../Configs/Edit/EditVpnConfigCommand.cs | 4 +- .../Edit/EditVpnConfigCommandHandler.cs | 35 +- .../GetConfigLink/GetConfigLinkQuery.cs | 4 +- .../GetConfigLinkQueryHandler.cs | 36 +- .../GetMyConfigs/GetMyConfigsQueryHandler.cs | 22 +- .../GetMySubscriptionQuery.cs | 4 +- .../GetMySubscriptionQueryHandler.cs | 11 +- .../ListAvailableInboundsQuery.cs | 4 +- .../ListAvailableInboundsQueryHandler.cs | 26 +- .../Revoke/RevokeVpnConfigCommandHandler.cs | 41 +- .../Configs/Rotate/RotateVpnConfigCommand.cs | 4 +- .../Rotate/RotateVpnConfigCommandHandler.cs | 43 +- .../Configs/VpnConfigDto.cs | 29 +- .../DependencyInjection.cs | 17 +- .../News/ListNewsQuery.cs | 4 +- .../News/ListNewsQueryHandler.cs | 11 +- .../PnvPanel.Application/News/NewsPostDto.cs | 11 +- .../PnvPanel.Application.csproj | 2 - .../GetConfigSubscriptionQueryHandler.cs | 26 +- .../GetUserSubscriptionQueryHandler.cs | 27 +- .../Subscriptions/SubscriptionAssembler.cs | 20 +- .../Subscriptions/SubscriptionDto.cs | 6 +- .../Subscriptions/SubscriptionErrors.cs | 5 +- .../AddComment/AddTicketCommentCommand.cs | 7 +- .../AddTicketCommentCommandHandler.cs | 37 +- .../CreateBugReportTicketCommand.cs | 6 +- .../CreateBugReportTicketCommandHandler.cs | 62 +- .../CreateBugReportTicketCommandValidator.cs | 3 +- .../CreateRoleRequestTicketCommand.cs | 8 +- .../CreateRoleRequestTicketCommandHandler.cs | 72 ++- ...CreateRoleRequestTicketCommandValidator.cs | 28 +- .../GetAttachment/GetTicketAttachmentQuery.cs | 4 +- .../GetTicketAttachmentQueryHandler.cs | 25 +- .../Support/GetTicket/GetTicketQuery.cs | 4 +- .../GetTicket/GetTicketQueryHandler.cs | 28 +- .../ListMyTickets/ListMyTicketsQuery.cs | 8 +- .../ListMyTicketsQueryHandler.cs | 27 +- .../ListSelectableRolesQuery.cs | 4 +- .../ListSelectableRolesQueryHandler.cs | 9 +- .../Reopen/ReopenTicketCommandHandler.cs | 25 +- .../Support/SupportErrors.cs | 67 +- .../Support/TicketAttachmentDto.cs | 7 +- .../Support/TicketAttachmentPersistence.cs | 16 +- .../Support/TicketAttachmentUpload.cs | 7 +- .../Support/TicketAttachmentValidation.cs | 9 +- .../Support/TicketAuthorization.cs | 3 +- .../Support/TicketCommentDto.cs | 9 +- .../Support/TicketDetailDto.cs | 17 +- .../Support/TicketMapping.cs | 73 ++- .../Support/TicketSummaryDto.cs | 10 +- .../Bot/ApproveTelegramLoginCommand.cs | 3 +- .../Bot/ApproveTelegramLoginCommandHandler.cs | 21 +- .../Telegram/Bot/LinkTelegramCommand.cs | 6 +- .../Bot/LinkTelegramCommandHandler.cs | 22 +- .../Bot/RejectTelegramLoginCommand.cs | 3 +- .../Bot/RejectTelegramLoginCommandHandler.cs | 21 +- .../Telegram/CreateLinkTokenCommandHandler.cs | 5 +- .../CreateLoginRequestCommandHandler.cs | 5 +- .../Telegram/GetLoginRequestStatusQuery.cs | 3 +- .../GetLoginRequestStatusQueryHandler.cs | 38 +- .../RegisterViaTelegramCommandHandler.cs | 26 +- .../Telegram/TelegramErrors.cs | 24 +- .../Telegram/UnlinkTelegramCommandHandler.cs | 6 +- .../Activation/ActivationRequest.cs | 4 +- backend/src/PnvPanel.Domain/Apps/ClientApp.cs | 23 +- backend/src/PnvPanel.Domain/Audit/AuditLog.cs | 12 +- backend/src/PnvPanel.Domain/Common/Entity.cs | 3 +- .../PnvPanel.Domain/Configs/TrafficSample.cs | 11 +- .../src/PnvPanel.Domain/Configs/VpnConfig.cs | 7 +- .../src/PnvPanel.Domain/Inbounds/Inbound.cs | 18 +- backend/src/PnvPanel.Domain/News/NewsPost.cs | 4 +- backend/src/PnvPanel.Domain/Nodes/Node.cs | 11 +- .../PnvPanel.Domain/Nodes/NodeCredentials.cs | 3 +- .../PnvPanel.Domain/PnvPanel.Domain.csproj | 2 - .../PnvPanel.Domain/Support/SupportTicket.cs | 11 +- .../Support/TicketAttachment.cs | 12 +- .../PnvPanel.Domain/Support/TicketComment.cs | 4 +- .../Telegram/TelegramLinkToken.cs | 7 +- .../Telegram/TelegramLoginRequest.cs | 4 +- .../BackgroundJobs/NodeHealthCheckService.cs | 15 +- .../BackgroundJobs/TrafficRetentionService.cs | 13 +- .../BackgroundJobs/TrafficSyncService.cs | 40 +- .../DependencyInjection.cs | 45 +- .../Identity/AppRole.cs | 9 +- .../Identity/CurrentUser.cs | 7 +- .../Identity/DbInitializer.cs | 65 +- .../Identity/DbInitializerExtensions.cs | 5 +- .../Identity/IdentityService.cs | 189 ++++-- .../Identity/JwtTokenService.cs | 7 +- .../Identity/RefreshTokenService.cs | 69 +- .../Identity/RoleService.cs | 47 +- .../Persistence/AppDbContext.cs | 3 +- .../Configurations/ClientAppConfiguration.cs | 3 +- .../Configurations/NodeConfiguration.cs | 23 +- .../RefreshTokenConfiguration.cs | 4 +- .../Persistence/MigrationExtensions.cs | 5 +- .../20260701161345_InitialCreate.cs | 10 +- .../Migrations/20260701171444_AddIdentity.cs | 207 ++++-- .../20260701175024_AddActivationRequests.cs | 37 +- .../20260701181642_AddNodesAndInbounds.cs | 89 ++- .../20260701185508_AddConfigsAndApps.cs | 111 +++- .../20260701194500_AddTrafficSamples.cs | 24 +- .../20260701195456_AddAuditAndBlocking.cs | 53 +- .../Migrations/20260701205836_AddTelegram.cs | 87 ++- .../20260702201236_RemoveDeviceLimit.cs | 7 +- .../Migrations/20260703114324_AddNewsPosts.cs | 31 +- .../20260713035342_AddRoleMaxIpLimit.cs | 11 +- .../20260714031703_AddSupportTickets.cs | 90 ++- .../PnvPanel.Infrastructure.csproj | 2 - .../Xui/XuiPanelGateway.cs | 197 ++++-- .../ApproveActivationCommandHandlerTests.cs | 68 +- .../RejectActivationCommandHandlerTests.cs | 15 +- .../RequestActivationCommandHandlerTests.cs | 70 +- .../ListAllConfigsQueryHandlerTests.cs | 45 +- .../Nodes/RegisterNodeCommandHandlerTests.cs | 50 +- .../ApproveRoleRequestCommandHandlerTests.cs | 72 ++- .../Users/BlockUserCommandHandlerTests.cs | 161 ++++- .../ChangeUserRoleCommandHandlerTests.cs | 22 +- .../Users/DeleteUserCommandHandlerTests.cs | 90 ++- .../Users/UnblockUserCommandHandlerTests.cs | 116 +++- .../Auth/ChangePasswordCommandHandlerTests.cs | 50 +- .../Auth/GetCurrentUserQueryHandlerTests.cs | 29 +- .../Auth/LoginCommandHandlerTests.cs | 53 +- .../Auth/RefreshCommandHandlerTests.cs | 64 +- .../RequireActivationBehaviorTests.cs | 70 +- .../GetMyConfigsQueryHandlerTests.cs | 41 +- .../RevokeVpnConfigCommandHandlerTests.cs | 86 ++- .../RotateVpnConfigCommandHandlerTests.cs | 131 +++- .../PnvPanel.Application.Tests.csproj | 2 - .../AddTicketCommentCommandHandlerTests.cs | 93 ++- ...reateBugReportTicketCommandHandlerTests.cs | 57 +- ...ateRoleRequestTicketCommandHandlerTests.cs | 65 +- .../ReopenTicketCommandHandlerTests.cs | 25 +- ...ApproveTelegramLoginCommandHandlerTests.cs | 44 +- .../Bot/LinkTelegramCommandHandlerTests.cs | 46 +- .../RejectTelegramLoginCommandHandlerTests.cs | 35 +- .../CreateLinkTokenCommandHandlerTests.cs | 5 +- .../GetLoginRequestStatusQueryHandlerTests.cs | 59 +- .../UnlinkTelegramCommandHandlerTests.cs | 31 +- .../TestSupport/FakeCurrentUser.cs | 4 +- .../Apps/ClientAppTests.cs | 28 +- .../Audit/AuditLogTests.cs | 18 +- .../PnvPanel.Domain.Tests/Nodes/NodeTests.cs | 18 +- .../PnvPanel.Domain.Tests.csproj | 2 - .../Activation/ActivationFlowTests.cs | 36 +- .../Admin/NodeInboundCrudTests.cs | 83 ++- .../Auth/AuthFlowTests.cs | 44 +- .../Configs/ConfigQuotaTests.cs | 91 ++- .../PnvPanel.IntegrationTests.csproj | 2 - .../TestSupport/AuthTestHelper.cs | 42 +- .../TestSupport/FakeXuiPanelGateway.cs | 60 +- .../TestSupport/HttpClientJsonExtensions.cs | 25 +- .../PnvPanelWebApplicationFactory.cs | 30 +- 285 files changed, 6911 insertions(+), 2063 deletions(-) diff --git a/backend/Directory.Build.props b/backend/Directory.Build.props index cb652c6..579af1f 100644 --- a/backend/Directory.Build.props +++ b/backend/Directory.Build.props @@ -1,5 +1,4 @@ - net10.0 latest @@ -18,5 +17,4 @@ --> $(NoWarn);CA1711;CA1716;CA1848;CA1873 - diff --git a/backend/Directory.Packages.props b/backend/Directory.Packages.props index 259c422..c246b5e 100644 --- a/backend/Directory.Packages.props +++ b/backend/Directory.Packages.props @@ -10,7 +10,10 @@ - + @@ -22,7 +25,10 @@ - + @@ -37,4 +43,4 @@ не имеющие реляционных аналогов; InMemory игнорирует HasColumnType и не требует их маппинга. --> - \ No newline at end of file + diff --git a/backend/src/PnvPanel.Api/Common/ResultExtensions.cs b/backend/src/PnvPanel.Api/Common/ResultExtensions.cs index 86f780d..41f3273 100644 --- a/backend/src/PnvPanel.Api/Common/ResultExtensions.cs +++ b/backend/src/PnvPanel.Api/Common/ResultExtensions.cs @@ -4,11 +4,11 @@ namespace PnvPanel.Api.Common; public static class ResultExtensions { - public static IResult ToHttpResult(this Result result) - => result.IsSuccess ? Results.NoContent() : ToProblem(result.Error); + public static IResult ToHttpResult(this Result result) => + result.IsSuccess ? Results.NoContent() : ToProblem(result.Error); - public static IResult ToHttpResult(this Result result) - => result.IsSuccess ? Results.Ok(result.Value) : ToProblem(result.Error); + public static IResult ToHttpResult(this Result result) => + result.IsSuccess ? Results.Ok(result.Value) : ToProblem(result.Error); private static IResult ToProblem(Error error) { diff --git a/backend/src/PnvPanel.Api/Endpoints/ActivationEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/ActivationEndpoints.cs index d2f8469..01a6c80 100644 --- a/backend/src/PnvPanel.Api/Endpoints/ActivationEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/ActivationEndpoints.cs @@ -27,40 +27,69 @@ public static class ActivationEndpoints return app; } - private static async Task GetStatus(ISender sender, CancellationToken cancellationToken) + private static async Task GetStatus( + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new GetActivationStatusQuery(), cancellationToken); return result.ToHttpResult(); } - private static async Task RequestActivation(RequestActivationCommand command, ISender sender, CancellationToken cancellationToken) + private static async Task RequestActivation( + RequestActivationCommand command, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } private static async Task ListRequests( - [AsParameters] ListActivationRequestsRequest request, ISender sender, CancellationToken cancellationToken) + [AsParameters] ListActivationRequestsRequest request, + ISender sender, + CancellationToken cancellationToken + ) { - var query = new ListActivationRequestsQuery(request.StatusFilter, request.Page, request.PageSize); + var query = new ListActivationRequestsQuery( + request.StatusFilter, + request.Page, + request.PageSize + ); var result = await sender.Send(query, cancellationToken); return result.ToHttpResult(); } - private static async Task Approve(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task Approve( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ApproveActivationCommand(id), cancellationToken); return result.ToHttpResult(); } private static async Task Reject( - Guid id, RejectActivationBody body, ISender sender, CancellationToken cancellationToken) + Guid id, + RejectActivationBody body, + ISender sender, + CancellationToken cancellationToken + ) { - var result = await sender.Send(new RejectActivationCommand(id, body.Reason), cancellationToken); + var result = await sender.Send( + new RejectActivationCommand(id, body.Reason), + cancellationToken + ); return result.ToHttpResult(); } } -public sealed record ListActivationRequestsRequest(ActivationStatus? StatusFilter, int Page = 1, int PageSize = 20); +public sealed record ListActivationRequestsRequest( + ActivationStatus? StatusFilter, + int Page = 1, + int PageSize = 20 +); public sealed record RejectActivationBody(string? Reason); diff --git a/backend/src/PnvPanel.Api/Endpoints/AdminAppEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/AdminAppEndpoints.cs index 738803a..744d0bc 100644 --- a/backend/src/PnvPanel.Api/Endpoints/AdminAppEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/AdminAppEndpoints.cs @@ -28,21 +28,42 @@ public static class AdminAppEndpoints return result.ToHttpResult(); } - private static async Task CreateApp(CreateAppCommand command, ISender sender, CancellationToken cancellationToken) + private static async Task CreateApp( + CreateAppCommand command, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task UpdateApp(Guid id, UpdateAppBody body, ISender sender, CancellationToken cancellationToken) + private static async Task UpdateApp( + Guid id, + UpdateAppBody body, + ISender sender, + CancellationToken cancellationToken + ) { var command = new UpdateAppCommand( - id, body.Name, body.DownloadUrl, body.OperatingSystem, body.Description, body.IconUrl, body.SortOrder, body.IsEnabled); + id, + body.Name, + body.DownloadUrl, + body.OperatingSystem, + body.Description, + body.IconUrl, + body.SortOrder, + body.IsEnabled + ); var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task DeleteApp(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task DeleteApp( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new DeleteAppCommand(id), cancellationToken); return result.ToHttpResult(); @@ -50,5 +71,11 @@ public static class AdminAppEndpoints } public sealed record UpdateAppBody( - string Name, string DownloadUrl, OsPlatform OperatingSystem, string? Description, string? IconUrl, - int SortOrder, bool IsEnabled); + string Name, + string DownloadUrl, + OsPlatform OperatingSystem, + string? Description, + string? IconUrl, + int SortOrder, + bool IsEnabled +); diff --git a/backend/src/PnvPanel.Api/Endpoints/AdminNewsEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/AdminNewsEndpoints.cs index b47f06c..eb49554 100644 --- a/backend/src/PnvPanel.Api/Endpoints/AdminNewsEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/AdminNewsEndpoints.cs @@ -23,26 +23,44 @@ public static class AdminNewsEndpoints return app; } - private static async Task ListAdminNews(int page, int pageSize, ISender sender, CancellationToken cancellationToken) + private static async Task ListAdminNews( + int page, + int pageSize, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ListAdminNewsQuery(page, pageSize), cancellationToken); return result.ToHttpResult(); } - private static async Task CreatePost(CreatePostCommand command, ISender sender, CancellationToken cancellationToken) + private static async Task CreatePost( + CreatePostCommand command, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task UpdatePost(Guid id, UpdatePostBody body, ISender sender, CancellationToken cancellationToken) + private static async Task UpdatePost( + Guid id, + UpdatePostBody body, + ISender sender, + CancellationToken cancellationToken + ) { var command = new UpdatePostCommand(id, body.Title, body.Body); var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task DeletePost(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task DeletePost( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new DeletePostCommand(id), cancellationToken); return result.ToHttpResult(); diff --git a/backend/src/PnvPanel.Api/Endpoints/AdminStatsEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/AdminStatsEndpoints.cs index d02dc5f..a585d49 100644 --- a/backend/src/PnvPanel.Api/Endpoints/AdminStatsEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/AdminStatsEndpoints.cs @@ -27,7 +27,12 @@ public static class AdminStatsEndpoints return result.ToHttpResult(); } - private static async Task GetAudit(int page, int pageSize, ISender sender, CancellationToken cancellationToken) + private static async Task GetAudit( + int page, + int pageSize, + ISender sender, + CancellationToken cancellationToken + ) { var query = new ListAuditLogsQuery(page <= 0 ? 1 : page, pageSize <= 0 ? 50 : pageSize); var result = await sender.Send(query, cancellationToken); diff --git a/backend/src/PnvPanel.Api/Endpoints/AdminSupportEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/AdminSupportEndpoints.cs index ae92934..20acc4c 100644 --- a/backend/src/PnvPanel.Api/Endpoints/AdminSupportEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/AdminSupportEndpoints.cs @@ -19,59 +19,104 @@ public static class AdminSupportEndpoints admin.MapGet("/tickets", ListTickets).Produces>(); admin.MapGet("/tickets/{id:guid}", GetTicket).Produces(); - admin.MapPost("/tickets/{id:guid}/comments", AddComment).DisableAntiforgery().Produces(); - admin.MapPost("/tickets/{id:guid}/resolve", Resolve).Produces(StatusCodes.Status204NoContent); + admin + .MapPost("/tickets/{id:guid}/comments", AddComment) + .DisableAntiforgery() + .Produces(); + admin + .MapPost("/tickets/{id:guid}/resolve", Resolve) + .Produces(StatusCodes.Status204NoContent); admin.MapPost("/tickets/{id:guid}/close", Close).Produces(StatusCodes.Status204NoContent); - admin.MapPost("/tickets/{id:guid}/approve", ApproveRoleRequest).Produces(StatusCodes.Status204NoContent); - admin.MapPost("/tickets/{id:guid}/reject", RejectRoleRequest).Produces(StatusCodes.Status204NoContent); + admin + .MapPost("/tickets/{id:guid}/approve", ApproveRoleRequest) + .Produces(StatusCodes.Status204NoContent); + admin + .MapPost("/tickets/{id:guid}/reject", RejectRoleRequest) + .Produces(StatusCodes.Status204NoContent); return app; } private static async Task ListTickets( - [AsParameters] ListTicketsRequest request, ISender sender, CancellationToken cancellationToken) + [AsParameters] ListTicketsRequest request, + ISender sender, + CancellationToken cancellationToken + ) { - var query = new ListAllTicketsQuery(request.Type, request.Status, request.Page, request.PageSize); + var query = new ListAllTicketsQuery( + request.Type, + request.Status, + request.Page, + request.PageSize + ); var result = await sender.Send(query, cancellationToken); return result.ToHttpResult(); } - private static async Task GetTicket(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task GetTicket( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new GetTicketAdminQuery(id), cancellationToken); return result.ToHttpResult(); } private static async Task AddComment( - Guid id, [FromForm] string body, IFormFileCollection? files, ISender sender, CancellationToken cancellationToken) + Guid id, + [FromForm] string body, + IFormFileCollection? files, + ISender sender, + CancellationToken cancellationToken + ) { var command = new AddTicketCommentCommand(id, body, SupportEndpoints.ToUploads(files)); var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task Resolve(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task Resolve( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ResolveTicketCommand(id), cancellationToken); return result.ToHttpResult(); } - private static async Task Close(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task Close( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new CloseTicketCommand(id), cancellationToken); return result.ToHttpResult(); } - private static async Task ApproveRoleRequest(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task ApproveRoleRequest( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ApproveRoleRequestCommand(id), cancellationToken); return result.ToHttpResult(); } private static async Task RejectRoleRequest( - Guid id, RejectRoleRequestBody body, ISender sender, CancellationToken cancellationToken) + Guid id, + RejectRoleRequestBody body, + ISender sender, + CancellationToken cancellationToken + ) { - var result = await sender.Send(new RejectRoleRequestCommand(id, body.Reason), cancellationToken); + var result = await sender.Send( + new RejectRoleRequestCommand(id, body.Reason), + cancellationToken + ); return result.ToHttpResult(); } } diff --git a/backend/src/PnvPanel.Api/Endpoints/AdminUserEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/AdminUserEndpoints.cs index 0f1bd70..85f183d 100644 --- a/backend/src/PnvPanel.Api/Endpoints/AdminUserEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/AdminUserEndpoints.cs @@ -19,64 +19,118 @@ public static class AdminUserEndpoints .RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin)); admin.MapGet("/users", ListUsers).Produces>(); - admin.MapPatch("/users/{id:guid}/block", BlockUser).Produces(StatusCodes.Status204NoContent); - admin.MapPatch("/users/{id:guid}/unblock", UnblockUser).Produces(StatusCodes.Status204NoContent); - admin.MapPost("/users/{id:guid}/reset-password", ResetPassword).Produces(StatusCodes.Status204NoContent); + admin + .MapPatch("/users/{id:guid}/block", BlockUser) + .Produces(StatusCodes.Status204NoContent); + admin + .MapPatch("/users/{id:guid}/unblock", UnblockUser) + .Produces(StatusCodes.Status204NoContent); + admin + .MapPost("/users/{id:guid}/reset-password", ResetPassword) + .Produces(StatusCodes.Status204NoContent); admin.MapDelete("/users/{id:guid}", DeleteUser).Produces(StatusCodes.Status204NoContent); - admin.MapGet("/users/{id:guid}/configs", GetUserConfigs).Produces>(); + admin + .MapGet("/users/{id:guid}/configs", GetUserConfigs) + .Produces>(); admin.MapGet("/configs", ListAllConfigs).Produces>(); - admin.MapDelete("/configs/{id:guid}", ForceRevokeConfig).Produces(StatusCodes.Status204NoContent); + admin + .MapDelete("/configs/{id:guid}", ForceRevokeConfig) + .Produces(StatusCodes.Status204NoContent); return app; } private static async Task ListUsers( - int page, int pageSize, string? search, ISender sender, CancellationToken cancellationToken) + int page, + int pageSize, + string? search, + ISender sender, + CancellationToken cancellationToken + ) { var query = new ListUsersQuery(page <= 0 ? 1 : page, pageSize <= 0 ? 20 : pageSize, search); var result = await sender.Send(query, cancellationToken); return result.ToHttpResult(); } - private static async Task BlockUser(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task BlockUser( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new BlockUserCommand(id), cancellationToken); return result.ToHttpResult(); } - private static async Task UnblockUser(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task UnblockUser( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new UnblockUserCommand(id), cancellationToken); return result.ToHttpResult(); } - private static async Task ResetPassword(Guid id, ResetPasswordBody body, ISender sender, CancellationToken cancellationToken) + private static async Task ResetPassword( + Guid id, + ResetPasswordBody body, + ISender sender, + CancellationToken cancellationToken + ) { - var result = await sender.Send(new ResetUserPasswordCommand(id, body.NewPassword), cancellationToken); + var result = await sender.Send( + new ResetUserPasswordCommand(id, body.NewPassword), + cancellationToken + ); return result.ToHttpResult(); } - private static async Task DeleteUser(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task DeleteUser( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new DeleteUserCommand(id), cancellationToken); return result.ToHttpResult(); } - private static async Task GetUserConfigs(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task GetUserConfigs( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new GetUserConfigsQuery(id), cancellationToken); return result.ToHttpResult(); } private static async Task ListAllConfigs( - int page, int pageSize, string? search, ConfigStatus? status, ISender sender, CancellationToken cancellationToken) + int page, + int pageSize, + string? search, + ConfigStatus? status, + ISender sender, + CancellationToken cancellationToken + ) { - var query = new ListAllConfigsQuery(page <= 0 ? 1 : page, pageSize <= 0 ? 20 : pageSize, search, status); + var query = new ListAllConfigsQuery( + page <= 0 ? 1 : page, + pageSize <= 0 ? 20 : pageSize, + search, + status + ); var result = await sender.Send(query, cancellationToken); return result.ToHttpResult(); } - private static async Task ForceRevokeConfig(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task ForceRevokeConfig( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ForceRevokeConfigCommand(id), cancellationToken); return result.ToHttpResult(); diff --git a/backend/src/PnvPanel.Api/Endpoints/AuthEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/AuthEndpoints.cs index 83db3d9..2a8f053 100644 --- a/backend/src/PnvPanel.Api/Endpoints/AuthEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/AuthEndpoints.cs @@ -25,34 +25,69 @@ public static class AuthEndpoints group.MapPost("/register", Register).Produces(); group.MapPost("/login", Login).Produces(); group.MapPost("/refresh", Refresh).Produces(); - group.MapPost("/logout", Logout).RequireAuthorization().Produces(StatusCodes.Status204NoContent); - group.MapPost("/change-password", ChangePassword).RequireAuthorization().Produces(StatusCodes.Status204NoContent); - group.MapPost("/change-username", ChangeUserName).RequireAuthorization().Produces(StatusCodes.Status204NoContent); + group + .MapPost("/logout", Logout) + .RequireAuthorization() + .Produces(StatusCodes.Status204NoContent); + group + .MapPost("/change-password", ChangePassword) + .RequireAuthorization() + .Produces(StatusCodes.Status204NoContent); + group + .MapPost("/change-username", ChangeUserName) + .RequireAuthorization() + .Produces(StatusCodes.Status204NoContent); group.MapGet("/me", Me).RequireAuthorization().Produces(); - group.MapDelete("/me", DeleteMe).RequireAuthorization().Produces(StatusCodes.Status204NoContent); + group + .MapDelete("/me", DeleteMe) + .RequireAuthorization() + .Produces(StatusCodes.Status204NoContent); return app; } - private static async Task Register(RegisterCommand command, ISender sender, CancellationToken cancellationToken) + private static async Task Register( + RegisterCommand command, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task Login(LoginCommand command, ISender sender, HttpRequest request, HttpResponse response, CancellationToken cancellationToken) + private static async Task Login( + LoginCommand command, + ISender sender, + HttpRequest request, + HttpResponse response, + CancellationToken cancellationToken + ) { var result = await sender.Send(command, cancellationToken); if (!result.IsSuccess) return result.ToHttpResult(); - SetRefreshCookie(request, response, result.Value.RefreshToken, result.Value.RefreshTokenExpiresAt); + SetRefreshCookie( + request, + response, + result.Value.RefreshToken, + result.Value.RefreshTokenExpiresAt + ); return Results.Ok(ToLoginResponse(result.Value)); } - private static async Task Refresh(HttpRequest request, HttpResponse response, ISender sender, CancellationToken cancellationToken) + private static async Task Refresh( + HttpRequest request, + HttpResponse response, + ISender sender, + CancellationToken cancellationToken + ) { - if (!request.Cookies.TryGetValue(RefreshCookieName, out var rawToken) || string.IsNullOrEmpty(rawToken)) + if ( + !request.Cookies.TryGetValue(RefreshCookieName, out var rawToken) + || string.IsNullOrEmpty(rawToken) + ) return Results.Unauthorized(); var result = await sender.Send(new RefreshCommand(rawToken), cancellationToken); @@ -62,29 +97,50 @@ public static class AuthEndpoints return result.ToHttpResult(); } - SetRefreshCookie(request, response, result.Value.RefreshToken, result.Value.RefreshTokenExpiresAt); + SetRefreshCookie( + request, + response, + result.Value.RefreshToken, + result.Value.RefreshTokenExpiresAt + ); return Results.Ok(ToLoginResponse(result.Value)); } internal static AuthResponseDto ToLoginResponse(AuthResult auth) => new(auth.AccessToken, auth.AccessTokenExpiresAt, auth.User); - private static async Task Logout(HttpRequest request, HttpResponse response, ISender sender, CancellationToken cancellationToken) + private static async Task Logout( + HttpRequest request, + HttpResponse response, + ISender sender, + CancellationToken cancellationToken + ) { - if (request.Cookies.TryGetValue(RefreshCookieName, out var rawToken) && !string.IsNullOrEmpty(rawToken)) + if ( + request.Cookies.TryGetValue(RefreshCookieName, out var rawToken) + && !string.IsNullOrEmpty(rawToken) + ) await sender.Send(new LogoutCommand(rawToken), cancellationToken); response.Cookies.Delete(RefreshCookieName, BuildCookieOptions(request)); return Results.NoContent(); } - private static async Task ChangePassword(ChangePasswordCommand command, ISender sender, CancellationToken cancellationToken) + private static async Task ChangePassword( + ChangePasswordCommand command, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task ChangeUserName(ChangeUserNameCommand command, ISender sender, CancellationToken cancellationToken) + private static async Task ChangeUserName( + ChangeUserNameCommand command, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); @@ -96,14 +152,24 @@ public static class AuthEndpoints return result.ToHttpResult(); } - private static async Task DeleteMe(HttpRequest request, HttpResponse response, ISender sender, CancellationToken cancellationToken) + private static async Task DeleteMe( + HttpRequest request, + HttpResponse response, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new DeleteMyAccountCommand(), cancellationToken); response.Cookies.Delete(RefreshCookieName, BuildCookieOptions(request)); return result.ToHttpResult(); } - private static void SetRefreshCookie(HttpRequest request, HttpResponse response, string rawToken, DateTimeOffset expiresAt) + private static void SetRefreshCookie( + HttpRequest request, + HttpResponse response, + string rawToken, + DateTimeOffset expiresAt + ) { var options = BuildCookieOptions(request); options.Expires = expiresAt; @@ -112,13 +178,18 @@ public static class AuthEndpoints // Secure = IsHttps запроса (учитывает ForwardedHeaders за внешним TLS-прокси, см. CLAUDE.md) — // иначе браузер/HttpClient не пришлёт cookie обратно на plain-http (локальный dev, TestServer). - private static CookieOptions BuildCookieOptions(HttpRequest request) => new() - { - HttpOnly = true, - Secure = request.IsHttps, - SameSite = SameSiteMode.Strict, - Path = "/api/auth", - }; + private static CookieOptions BuildCookieOptions(HttpRequest request) => + new() + { + HttpOnly = true, + Secure = request.IsHttps, + SameSite = SameSiteMode.Strict, + Path = "/api/auth", + }; } -public sealed record AuthResponseDto(string AccessToken, DateTimeOffset ExpiresAt, CurrentUserDto User); +public sealed record AuthResponseDto( + string AccessToken, + DateTimeOffset ExpiresAt, + CurrentUserDto User +); diff --git a/backend/src/PnvPanel.Api/Endpoints/ConfigEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/ConfigEndpoints.cs index e6f5384..89f6ebe 100644 --- a/backend/src/PnvPanel.Api/Endpoints/ConfigEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/ConfigEndpoints.cs @@ -18,73 +18,113 @@ public static class ConfigEndpoints { var group = app.MapGroup("/api").WithTags("Configs").RequireAuthorization(); - group.MapGet("/inbounds/available", ListAvailableInbounds).Produces>(); + group + .MapGet("/inbounds/available", ListAvailableInbounds) + .Produces>(); group.MapGet("/configs", GetMyConfigs).Produces(); group.MapPost("/configs", CreateConfig).Produces(); group.MapPatch("/configs/{id:guid}", EditConfig).Produces(); group.MapPost("/configs/{id:guid}/rotate", RotateConfig).Produces(); - group.MapDelete("/configs/{id:guid}", RevokeConfig).Produces(StatusCodes.Status204NoContent); + group + .MapDelete("/configs/{id:guid}", RevokeConfig) + .Produces(StatusCodes.Status204NoContent); group.MapGet("/configs/{id:guid}/link", GetConfigLink).Produces(); group.MapGet("/subscription", GetMySubscription).Produces(); return app; } - private static async Task ListAvailableInbounds(ISender sender, CancellationToken cancellationToken) + private static async Task ListAvailableInbounds( + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ListAvailableInboundsQuery(), cancellationToken); return result.ToHttpResult(); } - private static async Task GetMyConfigs(ISender sender, CancellationToken cancellationToken) + private static async Task GetMyConfigs( + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new GetMyConfigsQuery(), cancellationToken); return result.ToHttpResult(); } - private static async Task CreateConfig(CreateConfigBody body, ISender sender, CancellationToken cancellationToken) + private static async Task CreateConfig( + CreateConfigBody body, + ISender sender, + CancellationToken cancellationToken + ) { var command = new CreateVpnConfigCommand(body.InboundId, body.Label); var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task EditConfig(Guid id, EditConfigBody body, ISender sender, CancellationToken cancellationToken) + private static async Task EditConfig( + Guid id, + EditConfigBody body, + ISender sender, + CancellationToken cancellationToken + ) { var command = new EditVpnConfigCommand(id, body.Label); var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task RotateConfig(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task RotateConfig( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new RotateVpnConfigCommand(id), cancellationToken); return result.ToHttpResult(); } - private static async Task RevokeConfig(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task RevokeConfig( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new RevokeVpnConfigCommand(id), cancellationToken); return result.ToHttpResult(); } - private static async Task GetConfigLink(Guid id, HttpRequest request, ISender sender, CancellationToken cancellationToken) + private static async Task GetConfigLink( + Guid id, + HttpRequest request, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new GetConfigLinkQuery(id), cancellationToken); if (!result.IsSuccess) return result.ToHttpResult(); - var subscriptionUrl = $"{request.Scheme}://{request.Host}/sub/{result.Value.SubscriptionToken}"; - return Results.Ok(new ConfigLinkResponseDto(result.Value.ConnectionString, subscriptionUrl)); + var subscriptionUrl = + $"{request.Scheme}://{request.Host}/sub/{result.Value.SubscriptionToken}"; + return Results.Ok( + new ConfigLinkResponseDto(result.Value.ConnectionString, subscriptionUrl) + ); } - private static async Task GetMySubscription(HttpRequest request, ISender sender, CancellationToken cancellationToken) + private static async Task GetMySubscription( + HttpRequest request, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new GetMySubscriptionQuery(), cancellationToken); if (!result.IsSuccess) return result.ToHttpResult(); - var subscriptionUrl = $"{request.Scheme}://{request.Host}/sub/{result.Value.SubscriptionToken}"; + var subscriptionUrl = + $"{request.Scheme}://{request.Host}/sub/{result.Value.SubscriptionToken}"; return Results.Ok(new MySubscriptionResponseDto(subscriptionUrl)); } } diff --git a/backend/src/PnvPanel.Api/Endpoints/InboundEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/InboundEndpoints.cs index 67114b7..3f7d180 100644 --- a/backend/src/PnvPanel.Api/Endpoints/InboundEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/InboundEndpoints.cs @@ -19,20 +19,38 @@ public static class InboundEndpoints return app; } - private static async Task ListInbounds(Guid? nodeId, ISender sender, CancellationToken cancellationToken) + private static async Task ListInbounds( + Guid? nodeId, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ListInboundsQuery(nodeId), cancellationToken); return result.ToHttpResult(); } private static async Task PublishInbound( - Guid id, PublishInboundBody body, ISender sender, CancellationToken cancellationToken) + Guid id, + PublishInboundBody body, + ISender sender, + CancellationToken cancellationToken + ) { var command = new PublishInboundCommand( - id, body.IsPublished, body.DisplayName, body.AllowedRoleIds ?? [], body.MaxClients); + id, + body.IsPublished, + body.DisplayName, + body.AllowedRoleIds ?? [], + body.MaxClients + ); var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } } -public sealed record PublishInboundBody(bool IsPublished, string? DisplayName, IReadOnlyList? AllowedRoleIds, int? MaxClients); +public sealed record PublishInboundBody( + bool IsPublished, + string? DisplayName, + IReadOnlyList? AllowedRoleIds, + int? MaxClients +); diff --git a/backend/src/PnvPanel.Api/Endpoints/NewsEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/NewsEndpoints.cs index 4dcc9cd..e3eaeab 100644 --- a/backend/src/PnvPanel.Api/Endpoints/NewsEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/NewsEndpoints.cs @@ -16,7 +16,12 @@ public static class NewsEndpoints return app; } - private static async Task ListNews(int page, int pageSize, ISender sender, CancellationToken cancellationToken) + private static async Task ListNews( + int page, + int pageSize, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ListNewsQuery(page, pageSize), cancellationToken); return result.ToHttpResult(); diff --git a/backend/src/PnvPanel.Api/Endpoints/NodeEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/NodeEndpoints.cs index f4c8158..354661d 100644 --- a/backend/src/PnvPanel.Api/Endpoints/NodeEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/NodeEndpoints.cs @@ -23,42 +23,79 @@ public static class NodeEndpoints return app; } - private static async Task ListNodes(ISender sender, CancellationToken cancellationToken) + private static async Task ListNodes( + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ListNodesQuery(), cancellationToken); return result.ToHttpResult(); } - private static async Task RegisterNode(RegisterNodeCommand command, ISender sender, CancellationToken cancellationToken) + private static async Task RegisterNode( + RegisterNodeCommand command, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task UpdateNode(Guid id, UpdateNodeBody body, ISender sender, CancellationToken cancellationToken) + private static async Task UpdateNode( + Guid id, + UpdateNodeBody body, + ISender sender, + CancellationToken cancellationToken + ) { - var command = new UpdateNodeCommand(id, body.Name, body.Location, body.IsEnabled, body.Username, body.Password); + var command = new UpdateNodeCommand( + id, + body.Name, + body.Location, + body.IsEnabled, + body.Username, + body.Password + ); var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task DeleteNode(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task DeleteNode( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new DeleteNodeCommand(id), cancellationToken); return result.ToHttpResult(); } - private static async Task SyncNode(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task SyncNode( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new SyncNodeCommand(id), cancellationToken); return result.ToHttpResult(); } - private static async Task ProbeNode(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task ProbeNode( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ProbeNodeCommand(id), cancellationToken); return result.ToHttpResult(); } } -public sealed record UpdateNodeBody(string Name, string? Location, bool IsEnabled, string? Username, string? Password); +public sealed record UpdateNodeBody( + string Name, + string? Location, + bool IsEnabled, + string? Username, + string? Password +); diff --git a/backend/src/PnvPanel.Api/Endpoints/RoleEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/RoleEndpoints.cs index 6c17643..f36f2c0 100644 --- a/backend/src/PnvPanel.Api/Endpoints/RoleEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/RoleEndpoints.cs @@ -19,38 +19,67 @@ public static class RoleEndpoints admin.MapPost("/roles", CreateRole).Produces(); admin.MapPut("/roles/{id:guid}", UpdateRole).Produces(); admin.MapDelete("/roles/{id:guid}", DeleteRole).Produces(StatusCodes.Status204NoContent); - admin.MapPatch("/users/{id:guid}/role", ChangeUserRole).Produces(StatusCodes.Status204NoContent); + admin + .MapPatch("/users/{id:guid}/role", ChangeUserRole) + .Produces(StatusCodes.Status204NoContent); return app; } - private static async Task ListRoles(ISender sender, CancellationToken cancellationToken) + private static async Task ListRoles( + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ListRolesQuery(), cancellationToken); return result.ToHttpResult(); } - private static async Task CreateRole(CreateRoleCommand command, ISender sender, CancellationToken cancellationToken) + private static async Task CreateRole( + CreateRoleCommand command, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task UpdateRole(Guid id, UpdateRoleBody body, ISender sender, CancellationToken cancellationToken) + private static async Task UpdateRole( + Guid id, + UpdateRoleBody body, + ISender sender, + CancellationToken cancellationToken + ) { - var result = await sender.Send(new UpdateRoleCommand(id, body.MaxConfigs, body.MaxIpLimit), cancellationToken); + var result = await sender.Send( + new UpdateRoleCommand(id, body.MaxConfigs, body.MaxIpLimit), + cancellationToken + ); return result.ToHttpResult(); } - private static async Task DeleteRole(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task DeleteRole( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new DeleteRoleCommand(id), cancellationToken); return result.ToHttpResult(); } - private static async Task ChangeUserRole(Guid id, ChangeUserRoleBody body, ISender sender, CancellationToken cancellationToken) + private static async Task ChangeUserRole( + Guid id, + ChangeUserRoleBody body, + ISender sender, + CancellationToken cancellationToken + ) { - var result = await sender.Send(new ChangeUserRoleCommand(id, body.RoleId), cancellationToken); + var result = await sender.Send( + new ChangeUserRoleCommand(id, body.RoleId), + cancellationToken + ); return result.ToHttpResult(); } } diff --git a/backend/src/PnvPanel.Api/Endpoints/SubscriptionEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/SubscriptionEndpoints.cs index f22b5f6..06c3f68 100644 --- a/backend/src/PnvPanel.Api/Endpoints/SubscriptionEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/SubscriptionEndpoints.cs @@ -19,12 +19,19 @@ public static class SubscriptionEndpoints return app; } - private static async Task GetSubscription(string token, HttpResponse response, ISender sender, CancellationToken cancellationToken) + private static async Task GetSubscription( + string token, + HttpResponse response, + ISender sender, + CancellationToken cancellationToken + ) { // Токен — либо AppUser.SubscriptionToken (агрегированная подписка), либо VpnConfig.SubscriptionToken // (один конфиг). Пробуем пользовательский токен первым. var userResult = await sender.Send(new GetUserSubscriptionQuery(token), cancellationToken); - var result = userResult.IsSuccess ? userResult : await sender.Send(new GetConfigSubscriptionQuery(token), cancellationToken); + var result = userResult.IsSuccess + ? userResult + : await sender.Send(new GetConfigSubscriptionQuery(token), cancellationToken); if (!result.IsSuccess) return Results.NotFound(); @@ -36,7 +43,8 @@ public static class SubscriptionEndpoints var expire = result.Value.ExpiresAt is { } exp ? exp.ToUnixTimeSeconds().ToString() : "0"; response.Headers.Append( "Subscription-Userinfo", - $"upload={result.Value.UsedUpBytes}; download={result.Value.UsedDownBytes}; total={total}; expire={expire}"); + $"upload={result.Value.UsedUpBytes}; download={result.Value.UsedDownBytes}; total={total}; expire={expire}" + ); response.Headers.Append("Profile-Update-Interval", "12"); return Results.Text(base64Body, "text/plain; charset=utf-8"); diff --git a/backend/src/PnvPanel.Api/Endpoints/SupportEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/SupportEndpoints.cs index 1095424..f29a7e9 100644 --- a/backend/src/PnvPanel.Api/Endpoints/SupportEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/SupportEndpoints.cs @@ -23,25 +23,38 @@ public static class SupportEndpoints var group = app.MapGroup("/api/support").WithTags("Support").RequireAuthorization(); group.MapGet("/roles", ListSelectableRoles).Produces>(); - group.MapPost("/tickets/bug-reports", CreateBugReport).DisableAntiforgery().Produces(); + group + .MapPost("/tickets/bug-reports", CreateBugReport) + .DisableAntiforgery() + .Produces(); group.MapPost("/tickets/role-requests", CreateRoleRequest).Produces(); group.MapGet("/tickets", ListMyTickets).Produces>(); group.MapGet("/tickets/{id:guid}", GetTicket).Produces(); - group.MapPost("/tickets/{id:guid}/comments", AddComment).DisableAntiforgery().Produces(); + group + .MapPost("/tickets/{id:guid}/comments", AddComment) + .DisableAntiforgery() + .Produces(); group.MapPost("/tickets/{id:guid}/reopen", Reopen).Produces(StatusCodes.Status204NoContent); group.MapGet("/attachments/{id:guid}", GetAttachment); return app; } - private static async Task ListSelectableRoles(ISender sender, CancellationToken cancellationToken) + private static async Task ListSelectableRoles( + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ListSelectableRolesQuery(), cancellationToken); return result.ToHttpResult(); } private static async Task CreateBugReport( - [FromForm] string message, IFormFileCollection? files, ISender sender, CancellationToken cancellationToken) + [FromForm] string message, + IFormFileCollection? files, + ISender sender, + CancellationToken cancellationToken + ) { var command = new CreateBugReportTicketCommand(message, ToUploads(files)); var result = await sender.Send(command, cancellationToken); @@ -49,43 +62,76 @@ public static class SupportEndpoints } private static async Task CreateRoleRequest( - CreateRoleRequestBody body, ISender sender, CancellationToken cancellationToken) + CreateRoleRequestBody body, + ISender sender, + CancellationToken cancellationToken + ) { var command = new CreateRoleRequestTicketCommand( - body.ExistingRoleId, body.NewRoleName, body.NewRoleMaxConfigs, body.NewRoleMaxIpLimit, body.Justification); + body.ExistingRoleId, + body.NewRoleName, + body.NewRoleMaxConfigs, + body.NewRoleMaxIpLimit, + body.Justification + ); var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } private static async Task ListMyTickets( - [AsParameters] ListTicketsRequest request, ISender sender, CancellationToken cancellationToken) + [AsParameters] ListTicketsRequest request, + ISender sender, + CancellationToken cancellationToken + ) { - var query = new ListMyTicketsQuery(request.Type, request.Status, request.Page, request.PageSize); + var query = new ListMyTicketsQuery( + request.Type, + request.Status, + request.Page, + request.PageSize + ); var result = await sender.Send(query, cancellationToken); return result.ToHttpResult(); } - private static async Task GetTicket(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task GetTicket( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new GetTicketQuery(id), cancellationToken); return result.ToHttpResult(); } private static async Task AddComment( - Guid id, [FromForm] string body, IFormFileCollection? files, ISender sender, CancellationToken cancellationToken) + Guid id, + [FromForm] string body, + IFormFileCollection? files, + ISender sender, + CancellationToken cancellationToken + ) { var command = new AddTicketCommentCommand(id, body, ToUploads(files)); var result = await sender.Send(command, cancellationToken); return result.ToHttpResult(); } - private static async Task Reopen(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task Reopen( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new ReopenTicketCommand(id), cancellationToken); return result.ToHttpResult(); } - private static async Task GetAttachment(Guid id, ISender sender, CancellationToken cancellationToken) + private static async Task GetAttachment( + Guid id, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new GetTicketAttachmentQuery(id), cancellationToken); if (!result.IsSuccess) @@ -100,12 +146,27 @@ public static class SupportEndpoints return []; return files - .Select(f => new TicketAttachmentUpload(f.OpenReadStream(), f.FileName, f.ContentType, f.Length)) + .Select(f => new TicketAttachmentUpload( + f.OpenReadStream(), + f.FileName, + f.ContentType, + f.Length + )) .ToList(); } } public sealed record CreateRoleRequestBody( - Guid? ExistingRoleId, string? NewRoleName, int? NewRoleMaxConfigs, int? NewRoleMaxIpLimit, string Justification); + Guid? ExistingRoleId, + string? NewRoleName, + int? NewRoleMaxConfigs, + int? NewRoleMaxIpLimit, + string Justification +); -public sealed record ListTicketsRequest(TicketType? Type, TicketStatus? Status, int Page = 1, int PageSize = 20); +public sealed record ListTicketsRequest( + TicketType? Type, + TicketStatus? Status, + int Page = 1, + int PageSize = 20 +); diff --git a/backend/src/PnvPanel.Api/Endpoints/TelegramEndpoints.cs b/backend/src/PnvPanel.Api/Endpoints/TelegramEndpoints.cs index 11d7ea9..b40e7b9 100644 --- a/backend/src/PnvPanel.Api/Endpoints/TelegramEndpoints.cs +++ b/backend/src/PnvPanel.Api/Endpoints/TelegramEndpoints.cs @@ -15,23 +15,38 @@ public static class TelegramEndpoints .WithTags("Auth.Telegram") .RequireRateLimiting(RateLimiting.AuthPolicy); - group.MapPost("/link-token", CreateLinkToken).RequireAuthorization().Produces(); - group.MapPost("/unlink", Unlink).RequireAuthorization().Produces(StatusCodes.Status204NoContent); - group.MapPost("/login-request", CreateLoginRequest).Produces(); - group.MapGet("/login-request/{id:guid}", GetLoginRequestStatus).Produces(); + group + .MapPost("/link-token", CreateLinkToken) + .RequireAuthorization() + .Produces(); + group + .MapPost("/unlink", Unlink) + .RequireAuthorization() + .Produces(StatusCodes.Status204NoContent); + group + .MapPost("/login-request", CreateLoginRequest) + .Produces(); + group + .MapGet("/login-request/{id:guid}", GetLoginRequestStatus) + .Produces(); return app; } private static async Task CreateLinkToken( - ISender sender, ITelegramBotInfo botInfo, CancellationToken cancellationToken) + ISender sender, + ITelegramBotInfo botInfo, + CancellationToken cancellationToken + ) { var result = await sender.Send(new CreateLinkTokenCommand(), cancellationToken); if (!result.IsSuccess) return result.ToHttpResult(); var botUsername = await botInfo.GetUsernameAsync(cancellationToken); - var deepLink = botUsername is null ? null : $"https://t.me/{botUsername}?start=link_{result.Value.Token}"; + var deepLink = botUsername is null + ? null + : $"https://t.me/{botUsername}?start=link_{result.Value.Token}"; return Results.Ok(new LinkTokenResponseDto(deepLink, result.Value.ExpiresAt)); } @@ -43,7 +58,11 @@ public static class TelegramEndpoints } private static async Task CreateLoginRequest( - HttpRequest request, ISender sender, ITelegramBotInfo botInfo, CancellationToken cancellationToken) + HttpRequest request, + ISender sender, + ITelegramBotInfo botInfo, + CancellationToken cancellationToken + ) { var context = request.HttpContext.Connection.RemoteIpAddress?.ToString(); var result = await sender.Send(new CreateLoginRequestCommand(context), cancellationToken); @@ -51,13 +70,26 @@ public static class TelegramEndpoints return result.ToHttpResult(); var botUsername = await botInfo.GetUsernameAsync(cancellationToken); - var deepLink = botUsername is null ? null : $"https://t.me/{botUsername}?start=login_{result.Value.RequestId}"; + var deepLink = botUsername is null + ? null + : $"https://t.me/{botUsername}?start=login_{result.Value.RequestId}"; - return Results.Ok(new TelegramLoginRequestResponseDto(result.Value.RequestId, deepLink, result.Value.ExpiresAt)); + return Results.Ok( + new TelegramLoginRequestResponseDto( + result.Value.RequestId, + deepLink, + result.Value.ExpiresAt + ) + ); } private static async Task GetLoginRequestStatus( - Guid id, HttpRequest request, HttpResponse response, ISender sender, CancellationToken cancellationToken) + Guid id, + HttpRequest request, + HttpResponse response, + ISender sender, + CancellationToken cancellationToken + ) { var result = await sender.Send(new GetLoginRequestStatusQuery(id), cancellationToken); if (!result.IsSuccess) @@ -77,17 +109,33 @@ public static class TelegramEndpoints }; response.Cookies.Append("pnv_refresh_token", auth.RefreshToken, cookieOptions); - return Results.Ok(new TelegramLoginStatusResponseDto( - result.Value.Status, auth.AccessToken, auth.AccessTokenExpiresAt, auth.User)); + return Results.Ok( + new TelegramLoginStatusResponseDto( + result.Value.Status, + auth.AccessToken, + auth.AccessTokenExpiresAt, + auth.User + ) + ); } - return Results.Ok(new TelegramLoginStatusResponseDto(result.Value.Status, null, null, null)); + return Results.Ok( + new TelegramLoginStatusResponseDto(result.Value.Status, null, null, null) + ); } } public sealed record LinkTokenResponseDto(string? DeepLink, DateTimeOffset ExpiresAt); -public sealed record TelegramLoginRequestResponseDto(Guid RequestId, string? DeepLink, DateTimeOffset ExpiresAt); +public sealed record TelegramLoginRequestResponseDto( + Guid RequestId, + string? DeepLink, + DateTimeOffset ExpiresAt +); public sealed record TelegramLoginStatusResponseDto( - TelegramLoginStatus Status, string? AccessToken, DateTimeOffset? ExpiresAt, CurrentUserDto? User); + TelegramLoginStatus Status, + string? AccessToken, + DateTimeOffset? ExpiresAt, + CurrentUserDto? User +); diff --git a/backend/src/PnvPanel.Api/Hubs/SignalRRealtimeNotifier.cs b/backend/src/PnvPanel.Api/Hubs/SignalRRealtimeNotifier.cs index bbc2b8d..a15d0da 100644 --- a/backend/src/PnvPanel.Api/Hubs/SignalRRealtimeNotifier.cs +++ b/backend/src/PnvPanel.Api/Hubs/SignalRRealtimeNotifier.cs @@ -9,68 +9,146 @@ namespace PnvPanel.Api.Hubs; internal sealed class SignalRRealtimeNotifier(IHubContext hubContext) : IRealtimeNotifier { public Task NotifyConfigTrafficUpdatedAsync( - Guid userId, Guid configId, long usedUpBytes, long usedDownBytes, CancellationToken cancellationToken) + Guid userId, + Guid configId, + long usedUpBytes, + long usedDownBytes, + CancellationToken cancellationToken + ) { - return hubContext.Clients.Group(GroupNames.User(userId)).SendAsync( - "configTrafficUpdated", - new { configId, usedUpBytes, usedDownBytes }, - cancellationToken); + return hubContext + .Clients.Group(GroupNames.User(userId)) + .SendAsync( + "configTrafficUpdated", + new + { + configId, + usedUpBytes, + usedDownBytes, + }, + cancellationToken + ); } - public Task NotifyConfigStatusChangedAsync(Guid userId, Guid configId, ConfigStatus status, CancellationToken cancellationToken) + public Task NotifyConfigStatusChangedAsync( + Guid userId, + Guid configId, + ConfigStatus status, + CancellationToken cancellationToken + ) { - return hubContext.Clients.Group(GroupNames.User(userId)).SendAsync( - "configStatusChanged", - new { configId, status = status.ToString() }, - cancellationToken); + return hubContext + .Clients.Group(GroupNames.User(userId)) + .SendAsync( + "configStatusChanged", + new { configId, status = status.ToString() }, + cancellationToken + ); } - public Task NotifyNodeStatusChangedAsync(Guid nodeId, NodeStatus status, DateTimeOffset? lastSyncAt, CancellationToken cancellationToken) + public Task NotifyNodeStatusChangedAsync( + Guid nodeId, + NodeStatus status, + DateTimeOffset? lastSyncAt, + CancellationToken cancellationToken + ) { - return hubContext.Clients.Group(GroupNames.Admins).SendAsync( - "nodeStatusChanged", - new { nodeId, status = status.ToString(), lastSyncAt }, - cancellationToken); + return hubContext + .Clients.Group(GroupNames.Admins) + .SendAsync( + "nodeStatusChanged", + new + { + nodeId, + status = status.ToString(), + lastSyncAt, + }, + cancellationToken + ); } public 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 + ) { - return hubContext.Clients.Group(GroupNames.Admins).SendAsync( - "activationRequested", - new { requestId, userId, userName, comment, createdAt }, - cancellationToken); + return hubContext + .Clients.Group(GroupNames.Admins) + .SendAsync( + "activationRequested", + new + { + requestId, + userId, + userName, + comment, + createdAt, + }, + cancellationToken + ); } public Task NotifyUserActivatedAsync(Guid userId, CancellationToken cancellationToken) { - return hubContext.Clients.Group(GroupNames.User(userId)).SendAsync( - "userActivated", - new { userId }, - cancellationToken); + return hubContext + .Clients.Group(GroupNames.User(userId)) + .SendAsync("userActivated", new { userId }, cancellationToken); } - public Task NotifyNewsPublishedAsync(Guid postId, string title, DateTimeOffset createdAt, CancellationToken cancellationToken) + public Task NotifyNewsPublishedAsync( + Guid postId, + string title, + DateTimeOffset createdAt, + CancellationToken cancellationToken + ) { return hubContext.Clients.All.SendAsync( "newsPublished", - new { id = postId, title, createdAt }, - cancellationToken); + new + { + id = postId, + title, + createdAt, + }, + cancellationToken + ); } - public Task NotifyTicketCreatedAsync(Guid ticketId, Guid userId, string userName, TicketType type, CancellationToken cancellationToken) + public Task NotifyTicketCreatedAsync( + Guid ticketId, + Guid userId, + string userName, + TicketType type, + CancellationToken cancellationToken + ) { - return hubContext.Clients.Group(GroupNames.Admins).SendAsync( - "ticketCreated", - new { ticketId, userId, userName, type = type.ToString() }, - cancellationToken); + return hubContext + .Clients.Group(GroupNames.Admins) + .SendAsync( + "ticketCreated", + new + { + ticketId, + userId, + userName, + type = type.ToString(), + }, + cancellationToken + ); } - public Task NotifyTicketUpdatedAsync(Guid ticketId, Guid userId, CancellationToken cancellationToken) + public Task NotifyTicketUpdatedAsync( + Guid ticketId, + Guid userId, + CancellationToken cancellationToken + ) { - return hubContext.Clients.Group(GroupNames.User(userId)).SendAsync( - "ticketUpdated", - new { ticketId }, - cancellationToken); + return hubContext + .Clients.Group(GroupNames.User(userId)) + .SendAsync("ticketUpdated", new { ticketId }, cancellationToken); } } diff --git a/backend/src/PnvPanel.Api/PnvPanel.Api.csproj b/backend/src/PnvPanel.Api/PnvPanel.Api.csproj index c00562a..aa7d769 100644 --- a/backend/src/PnvPanel.Api/PnvPanel.Api.csproj +++ b/backend/src/PnvPanel.Api/PnvPanel.Api.csproj @@ -1,12 +1,15 @@ - - + @@ -27,5 +30,4 @@ enable enable - diff --git a/backend/src/PnvPanel.Api/Program.cs b/backend/src/PnvPanel.Api/Program.cs index 85017d8..7b33030 100644 --- a/backend/src/PnvPanel.Api/Program.cs +++ b/backend/src/PnvPanel.Api/Program.cs @@ -20,10 +20,13 @@ using Telegram.Bot; var builder = WebApplication.CreateBuilder(args); // Структурное логирование (Serilog), конфигурация из appsettings/env. -builder.Services.AddSerilog((services, configuration) => configuration - .ReadFrom.Configuration(builder.Configuration) - .ReadFrom.Services(services) - .Enrich.FromLogContext()); +builder.Services.AddSerilog( + (services, configuration) => + configuration + .ReadFrom.Configuration(builder.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() +); // За внешним прокси доверяем X-Forwarded-* (TLS терминируется вне контейнера), но ТОЛЬКО от явно // перечисленных адресов/сетей прокси — иначе клиент может подделать свой IP/схему напрямую, минуя @@ -34,13 +37,25 @@ builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; - foreach (var proxy in builder.Configuration.GetSection("ForwardedHeaders:KnownProxies").Get() ?? []) + foreach ( + var proxy in builder + .Configuration.GetSection("ForwardedHeaders:KnownProxies") + .Get() + ?? [] + ) options.KnownProxies.Add(IPAddress.Parse(proxy)); - foreach (var network in builder.Configuration.GetSection("ForwardedHeaders:KnownNetworks").Get() ?? []) + foreach ( + var network in builder + .Configuration.GetSection("ForwardedHeaders:KnownNetworks") + .Get() + ?? [] + ) { var parts = network.Split('/'); - options.KnownIPNetworks.Add(new System.Net.IPNetwork(IPAddress.Parse(parts[0]), int.Parse(parts[1]))); + options.KnownIPNetworks.Add( + new System.Net.IPNetwork(IPAddress.Parse(parts[0]), int.Parse(parts[1])) + ); } }); @@ -49,6 +64,7 @@ builder.Services.AddApplication(); builder.Services.AddInfrastructure(builder.Configuration); builder.Services.AddSignalR(); + // В Api, не в Infrastructure — реализации нужен IHubContext, а Hub определён здесь же. builder.Services.AddSingleton(); @@ -74,17 +90,27 @@ builder.Services.AddSingleton(sp => if (!string.IsNullOrEmpty(proxyUri.UserInfo)) { var credentials = proxyUri.UserInfo.Split(':', 2); - proxy.Credentials = new NetworkCredential(credentials[0], credentials.Length > 1 ? credentials[1] : string.Empty); + proxy.Credentials = new NetworkCredential( + credentials[0], + credentials.Length > 1 ? credentials[1] : string.Empty + ); } - sp.GetRequiredService>().LogInformation( - "Telegram bot using proxy {Scheme}://{Host}:{Port}", proxyUri.Scheme, proxyUri.Host, proxyUri.Port); + sp.GetRequiredService>() + .LogInformation( + "Telegram bot using proxy {Scheme}://{Host}:{Port}", + proxyUri.Scheme, + proxyUri.Host, + proxyUri.Port + ); var handler = new SocketsHttpHandler { Proxy = proxy, UseProxy = true }; return new TelegramBotClient(token, new HttpClient(handler)); }); + // Scoped — зависит от IIdentityService (scoped), не Singleton. builder.Services.AddScoped(); + // Singleton — кэширует username бота (getMe) на весь процесс, не из ручного env (см. TelegramBotInfo). builder.Services.AddSingleton(); builder.Services.AddSingleton(); @@ -92,26 +118,32 @@ builder.Services.AddHostedService(); builder.Services.AddRateLimiter(options => { - options.AddFixedWindowLimiter(RateLimiting.AuthPolicy, limiterOptions => - { - // Настраиваемо через конфиг, чтобы интеграционные тесты (общий TestServer/host на весь - // collection, все запросы — от одного "клиента") могли поднять лимит и не ловить 429. - limiterOptions.PermitLimit = builder.Configuration.GetValue("RateLimiting:AuthPermitLimit", 20); - limiterOptions.Window = TimeSpan.FromMinutes(1); - limiterOptions.QueueLimit = 0; - }); + options.AddFixedWindowLimiter( + RateLimiting.AuthPolicy, + limiterOptions => + { + // Настраиваемо через конфиг, чтобы интеграционные тесты (общий TestServer/host на весь + // collection, все запросы — от одного "клиента") могли поднять лимит и не ловить 429. + limiterOptions.PermitLimit = builder.Configuration.GetValue( + "RateLimiting:AuthPermitLimit", + 20 + ); + limiterOptions.Window = TimeSpan.FromMinutes(1); + limiterOptions.QueueLimit = 0; + } + ); options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; }); // Энумы сериализуются строками ("Vless", "Active", ...), не числами — самодокументируемый JSON, // корректные строковые литералы при генерации TS-типов из OpenAPI-схемы (см. docs/frontend.md). builder.Services.ConfigureHttpJsonOptions(options => - options.SerializerOptions.Converters.Add(new JsonStringEnumConverter())); + options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()) +); builder.Services.AddProblemDetails(); builder.Services.AddOpenApi(); -builder.Services.AddHealthChecks() - .AddDbContextCheck(); +builder.Services.AddHealthChecks().AddDbContextCheck(); var app = builder.Build(); @@ -120,8 +152,9 @@ var app = builder.Build(); if (string.IsNullOrWhiteSpace(builder.Configuration["DataProtection:KeyRingPath"])) { app.Logger.LogWarning( - "DataProtection:KeyRingPath is not set — node secret encryption keys are not persistent " + - "and will be lost when the container is recreated. Mount a volume and set the path in production."); + "DataProtection:KeyRingPath is not set — node secret encryption keys are not persistent " + + "and will be lost when the container is recreated. Mount a volume and set the path in production." + ); } // Авто-применение миграций и идемпотентный сидинг (роли + админ из env) на старте. diff --git a/backend/src/PnvPanel.Api/Telegram/PnvBotUpdateHandler.cs b/backend/src/PnvPanel.Api/Telegram/PnvBotUpdateHandler.cs index 338ea7e..e34aab0 100644 --- a/backend/src/PnvPanel.Api/Telegram/PnvBotUpdateHandler.cs +++ b/backend/src/PnvPanel.Api/Telegram/PnvBotUpdateHandler.cs @@ -23,24 +23,41 @@ namespace PnvPanel.Api.Telegram; /// свежие scoped-сервисы (ISender, ICurrentUserSetter, ...). Бот — read-only по конфигам в MVP. /// public sealed class PnvBotUpdateHandler( - IServiceScopeFactory scopeFactory, IOptions options, ILogger logger) - : IUpdateHandler + IServiceScopeFactory scopeFactory, + IOptions options, + ILogger logger +) : IUpdateHandler { // Показывается везде, где боту нужен привязанный аккаунт, а его нет — явно проговариваем оба шага, // иначе новые пользователи не понимают, что сначала нужен обычный аккаунт на сайте. private const string NotLinkedMessage = "Сначала зарегистрируйтесь и войдите на сайте, затем привяжите Telegram: Настройки → «Привязать Telegram»."; - public async Task HandleUpdateAsync(ITelegramBotClient botClient, Update update, CancellationToken cancellationToken) + public async Task HandleUpdateAsync( + ITelegramBotClient botClient, + Update update, + CancellationToken cancellationToken + ) { await using var scope = scopeFactory.CreateAsyncScope(); try { if (update.Message is { Text: { } text } message) - await HandleMessageAsync(botClient, scope.ServiceProvider, message, text, cancellationToken); + await HandleMessageAsync( + botClient, + scope.ServiceProvider, + message, + text, + cancellationToken + ); else if (update.CallbackQuery is { } callback) - await HandleCallbackAsync(botClient, scope.ServiceProvider, callback, cancellationToken); + await HandleCallbackAsync( + botClient, + scope.ServiceProvider, + callback, + cancellationToken + ); } catch (Exception ex) { @@ -48,14 +65,24 @@ public sealed class PnvBotUpdateHandler( } } - public Task HandleErrorAsync(ITelegramBotClient botClient, Exception exception, HandleErrorSource source, CancellationToken cancellationToken) + public Task HandleErrorAsync( + ITelegramBotClient botClient, + Exception exception, + HandleErrorSource source, + CancellationToken cancellationToken + ) { logger.LogError(exception, "Telegram bot error (source {Source})", source); return Task.CompletedTask; } private async Task HandleMessageAsync( - ITelegramBotClient botClient, IServiceProvider services, Message message, string text, CancellationToken cancellationToken) + ITelegramBotClient botClient, + IServiceProvider services, + Message message, + string text, + CancellationToken cancellationToken + ) { var chatId = message.Chat.Id; var fromId = message.From?.Id; @@ -67,11 +94,32 @@ public sealed class PnvBotUpdateHandler( var payload = text.Length > 7 ? text[7..].Trim() : string.Empty; if (payload.StartsWith("link_", StringComparison.Ordinal)) - await HandleLinkAsync(botClient, services, chatId, fromId.Value, message.From?.Username, payload[5..], cancellationToken); + await HandleLinkAsync( + botClient, + services, + chatId, + fromId.Value, + message.From?.Username, + payload[5..], + cancellationToken + ); else if (payload.StartsWith("login_", StringComparison.Ordinal)) - await HandleLoginPromptAsync(botClient, services, chatId, fromId.Value, payload[6..], cancellationToken); + await HandleLoginPromptAsync( + botClient, + services, + chatId, + fromId.Value, + payload[6..], + cancellationToken + ); else - await SendWelcomeAsync(botClient, services, chatId, fromId.Value, cancellationToken); + await SendWelcomeAsync( + botClient, + services, + chatId, + fromId.Value, + cancellationToken + ); return; } @@ -79,25 +127,57 @@ public sealed class PnvBotUpdateHandler( switch (text) { case "/configs": - await HandleConfigsAsync(botClient, services, chatId, fromId.Value, cancellationToken); + await HandleConfigsAsync( + botClient, + services, + chatId, + fromId.Value, + cancellationToken + ); break; case "/unlink": - await HandleUnlinkAsync(botClient, services, chatId, fromId.Value, cancellationToken); + await HandleUnlinkAsync( + botClient, + services, + chatId, + fromId.Value, + cancellationToken + ); break; case "/requests": - await HandleRequestsAsync(botClient, services, chatId, fromId.Value, cancellationToken); + await HandleRequestsAsync( + botClient, + services, + chatId, + fromId.Value, + cancellationToken + ); break; case "/help": - await SendWelcomeAsync(botClient, services, chatId, fromId.Value, cancellationToken); + await SendWelcomeAsync( + botClient, + services, + chatId, + fromId.Value, + cancellationToken + ); break; default: - await botClient.SendMessage(chatId, "Не понимаю эту команду. /help — список команд.", cancellationToken: cancellationToken); + await botClient.SendMessage( + chatId, + "Не понимаю эту команду. /help — список команд.", + cancellationToken: cancellationToken + ); break; } } private async Task HandleCallbackAsync( - ITelegramBotClient botClient, IServiceProvider services, CallbackQuery callback, CancellationToken cancellationToken) + ITelegramBotClient botClient, + IServiceProvider services, + CallbackQuery callback, + CancellationToken cancellationToken + ) { var data = callback.Data; var chatId = callback.Message?.Chat.Id; @@ -107,7 +187,15 @@ public sealed class PnvBotUpdateHandler( if (data == "reg:new") { - await HandleRegisterCallbackAsync(botClient, services, chatId.Value, fromId, callback.From.Username, callback.Id, cancellationToken); + await HandleRegisterCallbackAsync( + botClient, + services, + chatId.Value, + fromId, + callback.From.Username, + callback.Id, + cancellationToken + ); return; } @@ -115,15 +203,28 @@ public sealed class PnvBotUpdateHandler( { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { - await botClient.AnswerCallbackQuery(callback.Id, "Telegram не привязан.", cancellationToken: cancellationToken); + await botClient.AnswerCallbackQuery( + callback.Id, + "Telegram не привязан.", + cancellationToken: cancellationToken + ); return; } await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken); - var (configsText, configsKeyboard) = await BuildConfigsMenuAsync(services, cancellationToken); + var (configsText, configsKeyboard) = await BuildConfigsMenuAsync( + services, + cancellationToken + ); if (callback.Message is not null) - await botClient.EditMessageText(chatId.Value, callback.Message.Id, configsText, replyMarkup: configsKeyboard, cancellationToken: cancellationToken); + await botClient.EditMessageText( + chatId.Value, + callback.Message.Id, + configsText, + replyMarkup: configsKeyboard, + cancellationToken: cancellationToken + ); return; } @@ -135,9 +236,18 @@ public sealed class PnvBotUpdateHandler( return; var identityService = services.GetRequiredService(); - var linkedUserId = await identityService.FindUserIdByTelegramUserIdAsync(fromId, cancellationToken); + var linkedUserId = await identityService.FindUserIdByTelegramUserIdAsync( + fromId, + cancellationToken + ); var (menuText, menuKeyboard) = BuildMainMenu(isLinked: linkedUserId is not null); - await botClient.EditMessageText(chatId.Value, callback.Message.Id, menuText, replyMarkup: menuKeyboard, cancellationToken: cancellationToken); + await botClient.EditMessageText( + chatId.Value, + callback.Message.Id, + menuText, + replyMarkup: menuKeyboard, + cancellationToken: cancellationToken + ); return; } @@ -146,12 +256,19 @@ public sealed class PnvBotUpdateHandler( { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { - await botClient.AnswerCallbackQuery(callback.Id, "Telegram не привязан.", cancellationToken: cancellationToken); + await botClient.AnswerCallbackQuery( + callback.Id, + "Telegram не привязан.", + cancellationToken: cancellationToken + ); return; } var unlinkSender = services.GetRequiredService(); - var unlinkResult = await unlinkSender.Send(new UnlinkTelegramCommand(), cancellationToken); + var unlinkResult = await unlinkSender.Send( + new UnlinkTelegramCommand(), + cancellationToken + ); await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken); if (callback.Message is null) @@ -160,15 +277,23 @@ public sealed class PnvBotUpdateHandler( if (!unlinkResult.IsSuccess) { await botClient.EditMessageText( - chatId.Value, callback.Message.Id, $"❌ Ошибка: {unlinkResult.Error.Message}", - replyMarkup: BackToMenuKeyboard(), cancellationToken: cancellationToken); + chatId.Value, + callback.Message.Id, + $"❌ Ошибка: {unlinkResult.Error.Message}", + replyMarkup: BackToMenuKeyboard(), + cancellationToken: cancellationToken + ); return; } var (unlinkedText, unlinkedKeyboard) = BuildMainMenu(isLinked: false); await botClient.EditMessageText( - chatId.Value, callback.Message.Id, "✅ Telegram отвязан от аккаунта.\n\n" + unlinkedText, - replyMarkup: unlinkedKeyboard, cancellationToken: cancellationToken); + chatId.Value, + callback.Message.Id, + "✅ Telegram отвязан от аккаунта.\n\n" + unlinkedText, + replyMarkup: unlinkedKeyboard, + cancellationToken: cancellationToken + ); return; } @@ -183,11 +308,22 @@ public sealed class PnvBotUpdateHandler( { case "login": { - var result = parts[1] == "approve" - ? await sender.Send(new ApproveTelegramLoginCommand(requestId, fromId), cancellationToken) - : await sender.Send(new RejectTelegramLoginCommand(requestId, fromId), cancellationToken); + var result = + parts[1] == "approve" + ? await sender.Send( + new ApproveTelegramLoginCommand(requestId, fromId), + cancellationToken + ) + : await sender.Send( + new RejectTelegramLoginCommand(requestId, fromId), + cancellationToken + ); - await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken); + await botClient.AnswerCallbackQuery( + callback.Id, + result.IsSuccess ? "Готово" : result.Error.Message, + cancellationToken: cancellationToken + ); if (callback.Message is not null) { var statusText = result.IsSuccess @@ -195,7 +331,12 @@ public sealed class PnvBotUpdateHandler( : $"⚠️ {result.Error.Message}"; var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}"; await botClient.EditMessageText( - chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken); + chatId.Value, + callback.Message.Id, + text, + parseMode: ParseMode.Html, + cancellationToken: cancellationToken + ); } break; @@ -204,26 +345,50 @@ public sealed class PnvBotUpdateHandler( { if (!await TrySetAdminCurrentUserAsync(services, fromId, cancellationToken)) { - await botClient.AnswerCallbackQuery(callback.Id, "Недостаточно прав.", cancellationToken: cancellationToken); + await botClient.AnswerCallbackQuery( + callback.Id, + "Недостаточно прав.", + cancellationToken: cancellationToken + ); return; } - var result = parts[1] == "approve" - ? await sender.Send(new ApproveActivationCommand(requestId), cancellationToken) - : await sender.Send(new RejectActivationCommand(requestId, Reason: null), cancellationToken); + var result = + parts[1] == "approve" + ? await sender.Send( + new ApproveActivationCommand(requestId), + cancellationToken + ) + : await sender.Send( + new RejectActivationCommand(requestId, Reason: null), + cancellationToken + ); - await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken); + await botClient.AnswerCallbackQuery( + callback.Id, + result.IsSuccess ? "Готово" : result.Error.Message, + cancellationToken: cancellationToken + ); if (callback.Message is not null) { // Редактируем исходное сообщение с запросом вместо отдельного — иначе кнопки // «Активировать/Отклонить» остаются висеть под уже обработанным запросом (в т.ч. // если его обработали в другом месте — на сайте или из другого чата). var statusText = result.IsSuccess - ? (parts[1] == "approve" ? "✅ Пользователь активирован." : "❌ Запрос отклонён.") + ? ( + parts[1] == "approve" + ? "✅ Пользователь активирован." + : "❌ Запрос отклонён." + ) : $"⚠️ {result.Error.Message}"; var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}"; await botClient.EditMessageText( - chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken); + chatId.Value, + callback.Message.Id, + text, + parseMode: ParseMode.Html, + cancellationToken: cancellationToken + ); } break; @@ -232,23 +397,47 @@ public sealed class PnvBotUpdateHandler( { if (!await TrySetAdminCurrentUserAsync(services, fromId, cancellationToken)) { - await botClient.AnswerCallbackQuery(callback.Id, "Недостаточно прав.", cancellationToken: cancellationToken); + await botClient.AnswerCallbackQuery( + callback.Id, + "Недостаточно прав.", + cancellationToken: cancellationToken + ); return; } - var result = parts[1] == "approve" - ? await sender.Send(new ApproveRoleRequestCommand(requestId), cancellationToken) - : await sender.Send(new RejectRoleRequestCommand(requestId, Reason: null), cancellationToken); + var result = + parts[1] == "approve" + ? await sender.Send( + new ApproveRoleRequestCommand(requestId), + cancellationToken + ) + : await sender.Send( + new RejectRoleRequestCommand(requestId, Reason: null), + cancellationToken + ); - await botClient.AnswerCallbackQuery(callback.Id, result.IsSuccess ? "Готово" : result.Error.Message, cancellationToken: cancellationToken); + await botClient.AnswerCallbackQuery( + callback.Id, + result.IsSuccess ? "Готово" : result.Error.Message, + cancellationToken: cancellationToken + ); if (callback.Message is not null) { var statusText = result.IsSuccess - ? (parts[1] == "approve" ? "✅ Заявка одобрена, роль выдана." : "❌ Заявка отклонена.") + ? ( + parts[1] == "approve" + ? "✅ Заявка одобрена, роль выдана." + : "❌ Заявка отклонена." + ) : $"⚠️ {result.Error.Message}"; var text = $"{Escape(callback.Message.Text ?? "")}\n\n{statusText}"; await botClient.EditMessageText( - chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken); + chatId.Value, + callback.Message.Id, + text, + parseMode: ParseMode.Html, + cancellationToken: cancellationToken + ); } break; @@ -257,16 +446,30 @@ public sealed class PnvBotUpdateHandler( { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { - await botClient.AnswerCallbackQuery(callback.Id, "Telegram не привязан.", cancellationToken: cancellationToken); + await botClient.AnswerCallbackQuery( + callback.Id, + "Telegram не привязан.", + cancellationToken: cancellationToken + ); return; } - var linkResult = await sender.Send(new GetConfigLinkQuery(requestId), cancellationToken); - await botClient.AnswerCallbackQuery(callback.Id, cancellationToken: cancellationToken); + var linkResult = await sender.Send( + new GetConfigLinkQuery(requestId), + cancellationToken + ); + await botClient.AnswerCallbackQuery( + callback.Id, + cancellationToken: cancellationToken + ); if (!linkResult.IsSuccess) { - await botClient.SendMessage(chatId.Value, $"Не удалось получить ссылку: {linkResult.Error.Message}", cancellationToken: cancellationToken); + await botClient.SendMessage( + chatId.Value, + $"Не удалось получить ссылку: {linkResult.Error.Message}", + cancellationToken: cancellationToken + ); return; } @@ -276,15 +479,22 @@ public sealed class PnvBotUpdateHandler( // Редактируем то же сообщение (не плодим отдельное с сырым URL) — ссылка моноширинным // блоком, по нему в Telegram можно тапнуть и скопировать целиком одним движением. // Убираем именно эту кнопку из клавиатуры — остальные конфиги и «В меню» остаются на месте. - var text = $"{Escape(callback.Message.Text ?? "")}\n\n{Escape(linkResult.Value.ConnectionString)}"; + var text = + $"{Escape(callback.Message.Text ?? "")}\n\n{Escape(linkResult.Value.ConnectionString)}"; var remainingRows = (callback.Message.ReplyMarkup?.InlineKeyboard ?? []) .Where(row => row.All(b => b.CallbackData != data)) .ToArray(); await botClient.EditMessageText( - chatId.Value, callback.Message.Id, text, parseMode: ParseMode.Html, - replyMarkup: remainingRows.Length > 0 ? new InlineKeyboardMarkup(remainingRows) : null, - cancellationToken: cancellationToken); + chatId.Value, + callback.Message.Id, + text, + parseMode: ParseMode.Html, + replyMarkup: remainingRows.Length > 0 + ? new InlineKeyboardMarkup(remainingRows) + : null, + cancellationToken: cancellationToken + ); break; } @@ -292,11 +502,20 @@ public sealed class PnvBotUpdateHandler( } private async Task HandleLinkAsync( - ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, string? username, string token, - CancellationToken cancellationToken) + ITelegramBotClient botClient, + IServiceProvider services, + long chatId, + long fromId, + string? username, + string token, + CancellationToken cancellationToken + ) { var sender = services.GetRequiredService(); - var result = await sender.Send(new LinkTelegramCommand(token, fromId, username), cancellationToken); + var result = await sender.Send( + new LinkTelegramCommand(token, fromId, username), + cancellationToken + ); var text = result.IsSuccess ? "✅ Telegram успешно привязан к вашему аккаунту." @@ -306,56 +525,103 @@ public sealed class PnvBotUpdateHandler( } private async Task HandleLoginPromptAsync( - ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, string requestIdRaw, CancellationToken cancellationToken) + ITelegramBotClient botClient, + IServiceProvider services, + long chatId, + long fromId, + string requestIdRaw, + CancellationToken cancellationToken + ) { if (!Guid.TryParse(requestIdRaw, out var requestId)) { - await botClient.SendMessage(chatId, "Некорректная ссылка входа.", cancellationToken: cancellationToken); + await botClient.SendMessage( + chatId, + "Некорректная ссылка входа.", + cancellationToken: cancellationToken + ); return; } var identityService = services.GetRequiredService(); - var userId = await identityService.FindUserIdByTelegramUserIdAsync(fromId, cancellationToken); + var userId = await identityService.FindUserIdByTelegramUserIdAsync( + fromId, + cancellationToken + ); if (userId is null) { await botClient.SendMessage( - chatId, NotLinkedMessage, - replyMarkup: new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new") }), - cancellationToken: cancellationToken); + chatId, + NotLinkedMessage, + replyMarkup: new InlineKeyboardMarkup( + new[] + { + InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new"), + } + ), + cancellationToken: cancellationToken + ); return; } - var keyboard = new InlineKeyboardMarkup(new[] - { - InlineKeyboardButton.WithCallbackData("✅ Подтвердить вход", $"login:approve:{requestId}"), - InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"login:reject:{requestId}"), - }); + var keyboard = new InlineKeyboardMarkup( + new[] + { + InlineKeyboardButton.WithCallbackData( + "✅ Подтвердить вход", + $"login:approve:{requestId}" + ), + InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"login:reject:{requestId}"), + } + ); await botClient.SendMessage( - chatId, "Кто-то пытается войти в PnvPanel через ваш аккаунт. Подтвердить вход?", - replyMarkup: keyboard, cancellationToken: cancellationToken); + chatId, + "Кто-то пытается войти в PnvPanel через ваш аккаунт. Подтвердить вход?", + replyMarkup: keyboard, + cancellationToken: cancellationToken + ); } private async Task HandleConfigsAsync( - ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken) + ITelegramBotClient botClient, + IServiceProvider services, + long chatId, + long fromId, + CancellationToken cancellationToken + ) { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { await botClient.SendMessage( - chatId, NotLinkedMessage, - replyMarkup: new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new") }), - cancellationToken: cancellationToken); + chatId, + NotLinkedMessage, + replyMarkup: new InlineKeyboardMarkup( + new[] + { + InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new"), + } + ), + cancellationToken: cancellationToken + ); return; } var (text, keyboard) = await BuildConfigsMenuAsync(services, cancellationToken); - await botClient.SendMessage(chatId, text, replyMarkup: keyboard, cancellationToken: cancellationToken); + await botClient.SendMessage( + chatId, + text, + replyMarkup: keyboard, + cancellationToken: cancellationToken + ); } /// Список конфигов одним сообщением: строка на конфиг + кнопка «🔗 {Label}» на каждый /// не отозванный, плюс «🔙 В меню» внизу. private static async Task<(string Text, InlineKeyboardMarkup Keyboard)> BuildConfigsMenuAsync( - IServiceProvider services, CancellationToken cancellationToken) + IServiceProvider services, + CancellationToken cancellationToken + ) { var sender = services.GetRequiredService(); var result = await sender.Send(new GetMyConfigsQuery(), cancellationToken); @@ -363,29 +629,53 @@ public sealed class PnvBotUpdateHandler( if (!result.IsSuccess || result.Value.Configs.Count == 0) return ("У вас пока нет конфигов.", BackToMenuKeyboard()); - var text = "Ваши конфиги:\n" + string.Join('\n', result.Value.Configs.Select(c => - $"• {c.Label ?? c.Location} ({c.Protocol}) — {c.Status}")); + var text = + "Ваши конфиги:\n" + + string.Join( + '\n', + result.Value.Configs.Select(c => + $"• {c.Label ?? c.Location} ({c.Protocol}) — {c.Status}" + ) + ); // Отозванному конфигу нечего показывать — кнопку не даём. - var rows = result.Value.Configs - .Where(c => c.Status != ConfigStatus.Revoked) - .Select(c => new[] { InlineKeyboardButton.WithCallbackData($"🔗 {c.Label ?? c.Location}", $"cfg:link:{c.Id}") }) + var rows = result + .Value.Configs.Where(c => c.Status != ConfigStatus.Revoked) + .Select(c => + new[] + { + InlineKeyboardButton.WithCallbackData( + $"🔗 {c.Label ?? c.Location}", + $"cfg:link:{c.Id}" + ), + } + ) .Append(BackToMenuRow()) .ToArray(); return (text, new InlineKeyboardMarkup(rows)); } - private static InlineKeyboardButton[] BackToMenuRow() => new[] { InlineKeyboardButton.WithCallbackData("🔙 В меню", "menu:back") }; + private static InlineKeyboardButton[] BackToMenuRow() => + new[] { InlineKeyboardButton.WithCallbackData("🔙 В меню", "menu:back") }; private static InlineKeyboardMarkup BackToMenuKeyboard() => new(new[] { BackToMenuRow() }); private async Task HandleUnlinkAsync( - ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken) + ITelegramBotClient botClient, + IServiceProvider services, + long chatId, + long fromId, + CancellationToken cancellationToken + ) { if (!await TrySetCurrentUserAsync(services, fromId, cancellationToken)) { - await botClient.SendMessage(chatId, "Telegram не привязан.", cancellationToken: cancellationToken); + await botClient.SendMessage( + chatId, + "Telegram не привязан.", + cancellationToken: cancellationToken + ); return; } @@ -393,105 +683,191 @@ public sealed class PnvBotUpdateHandler( var result = await sender.Send(new UnlinkTelegramCommand(), cancellationToken); await botClient.SendMessage( - chatId, result.IsSuccess ? "Telegram отвязан от аккаунта." : $"Ошибка: {result.Error.Message}", - cancellationToken: cancellationToken); + chatId, + result.IsSuccess ? "Telegram отвязан от аккаунта." : $"Ошибка: {result.Error.Message}", + cancellationToken: cancellationToken + ); } private async Task HandleRequestsAsync( - ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken) + ITelegramBotClient botClient, + IServiceProvider services, + long chatId, + long fromId, + CancellationToken cancellationToken + ) { if (!await TrySetAdminCurrentUserAsync(services, fromId, cancellationToken)) { - await botClient.SendMessage(chatId, "Недостаточно прав.", cancellationToken: cancellationToken); + await botClient.SendMessage( + chatId, + "Недостаточно прав.", + cancellationToken: cancellationToken + ); return; } var sender = services.GetRequiredService(); - var result = await sender.Send(new ListActivationRequestsQuery(ActivationStatus.Pending, 1, 10), cancellationToken); + var result = await sender.Send( + new ListActivationRequestsQuery(ActivationStatus.Pending, 1, 10), + cancellationToken + ); if (!result.IsSuccess || result.Value.Items.Count == 0) { - await botClient.SendMessage(chatId, "Нет ожидающих запросов на активацию.", cancellationToken: cancellationToken); + await botClient.SendMessage( + chatId, + "Нет ожидающих запросов на активацию.", + cancellationToken: cancellationToken + ); return; } foreach (var item in result.Value.Items) { - var text = $"Запрос от {item.UserName}" + (string.IsNullOrWhiteSpace(item.Comment) ? "" : $"\n{item.Comment}"); - var keyboard = new InlineKeyboardMarkup(new[] - { - InlineKeyboardButton.WithCallbackData("✅ Активировать", $"act:approve:{item.Id}"), - InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"act:reject:{item.Id}"), - }); - await botClient.SendMessage(chatId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken); + var text = + $"Запрос от {item.UserName}" + + (string.IsNullOrWhiteSpace(item.Comment) ? "" : $"\n{item.Comment}"); + var keyboard = new InlineKeyboardMarkup( + new[] + { + InlineKeyboardButton.WithCallbackData( + "✅ Активировать", + $"act:approve:{item.Id}" + ), + InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"act:reject:{item.Id}"), + } + ); + await botClient.SendMessage( + chatId, + text, + parseMode: ParseMode.Html, + replyMarkup: keyboard, + cancellationToken: cancellationToken + ); } } private async Task SendWelcomeAsync( - ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, CancellationToken cancellationToken) + ITelegramBotClient botClient, + IServiceProvider services, + long chatId, + long fromId, + CancellationToken cancellationToken + ) { var identityService = services.GetRequiredService(); - var userId = await identityService.FindUserIdByTelegramUserIdAsync(fromId, cancellationToken); + var userId = await identityService.FindUserIdByTelegramUserIdAsync( + fromId, + cancellationToken + ); var (text, keyboard) = BuildMainMenu(isLinked: userId is not null); - await botClient.SendMessage(chatId, text, replyMarkup: keyboard, cancellationToken: cancellationToken); + await botClient.SendMessage( + chatId, + text, + replyMarkup: keyboard, + cancellationToken: cancellationToken + ); } /// Привязанному аккаунту — кнопки-действия вместо текстовых команд; непривязанному — /// только регистрация (остальное ему всё равно недоступно). Кнопка на сайт — если задан PublicSiteUrl. private (string Text, InlineKeyboardMarkup Keyboard) BuildMainMenu(bool isLinked) { - const string text = "Привет! Это бот PnvPanel.\n\n" + const string text = + "Привет! Это бот PnvPanel.\n\n" + "Вход без пароля запускается кнопкой «Войти через Telegram» на сайте — бот пришлёт запрос на подтверждение."; var rows = new List(); if (isLinked) { - rows.Add(new[] { InlineKeyboardButton.WithCallbackData("📋 Мои конфиги", "menu:configs") }); - rows.Add(new[] { InlineKeyboardButton.WithCallbackData("🔓 Отвязать Telegram", "menu:unlink") }); + rows.Add( + new[] { InlineKeyboardButton.WithCallbackData("📋 Мои конфиги", "menu:configs") } + ); + rows.Add( + new[] + { + InlineKeyboardButton.WithCallbackData("🔓 Отвязать Telegram", "menu:unlink"), + } + ); } else { - rows.Add(new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new") }); + rows.Add( + new[] { InlineKeyboardButton.WithCallbackData("📝 Зарегистрироваться", "reg:new") } + ); } if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) - rows.Add(new[] { InlineKeyboardButton.WithUrl("🌐 Сайт панели", options.Value.PublicSiteUrl) }); + rows.Add( + new[] + { + InlineKeyboardButton.WithUrl("🌐 Сайт панели", options.Value.PublicSiteUrl), + } + ); return (text, new InlineKeyboardMarkup(rows)); } private static async Task HandleRegisterCallbackAsync( - ITelegramBotClient botClient, IServiceProvider services, long chatId, long fromId, string? username, string callbackId, - CancellationToken cancellationToken) + ITelegramBotClient botClient, + IServiceProvider services, + long chatId, + long fromId, + string? username, + string callbackId, + CancellationToken cancellationToken + ) { var sender = services.GetRequiredService(); - var result = await sender.Send(new RegisterViaTelegramCommand(fromId, username), cancellationToken); + var result = await sender.Send( + new RegisterViaTelegramCommand(fromId, username), + cancellationToken + ); await botClient.AnswerCallbackQuery(callbackId, cancellationToken: cancellationToken); if (!result.IsSuccess) { - await botClient.SendMessage(chatId, $"Не удалось зарегистрироваться: {result.Error.Message}", cancellationToken: cancellationToken); + await botClient.SendMessage( + chatId, + $"Не удалось зарегистрироваться: {result.Error.Message}", + cancellationToken: cancellationToken + ); return; } - var text = "✅ Аккаунт создан.\n\n" + var text = + "✅ Аккаунт создан.\n\n" + $"Логин: {result.Value.UserName}\n" + $"Пароль: {result.Value.Password}\n\n" + "Сохраните пароль — он присылается только один раз. Логин можно сменить в Настройках на сайте.\n\n" + "Дальше нужно дождаться активации администратором — после неё будут доступны конфиги. " + "Входить можно как по паролю, так и кнопкой «Войти через Telegram»."; - await botClient.SendMessage(chatId, text, parseMode: ParseMode.Html, cancellationToken: cancellationToken); + await botClient.SendMessage( + chatId, + text, + parseMode: ParseMode.Html, + cancellationToken: cancellationToken + ); } - private static string Escape(string text) => text.Replace("&", "&").Replace("<", "<").Replace(">", ">"); + private static string Escape(string text) => + text.Replace("&", "&").Replace("<", "<").Replace(">", ">"); - private static async Task TrySetCurrentUserAsync(IServiceProvider services, long telegramUserId, CancellationToken cancellationToken) + private static async Task TrySetCurrentUserAsync( + IServiceProvider services, + long telegramUserId, + CancellationToken cancellationToken + ) { var identityService = services.GetRequiredService(); - var userId = await identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, cancellationToken); + var userId = await identityService.FindUserIdByTelegramUserIdAsync( + telegramUserId, + cancellationToken + ); if (userId is null) return false; @@ -503,7 +879,11 @@ public sealed class PnvBotUpdateHandler( return true; } - private async Task TrySetAdminCurrentUserAsync(IServiceProvider services, long telegramUserId, CancellationToken cancellationToken) + private async Task TrySetAdminCurrentUserAsync( + IServiceProvider services, + long telegramUserId, + CancellationToken cancellationToken + ) { if (!options.Value.ParseAdminTelegramUserIds().Contains(telegramUserId)) return false; diff --git a/backend/src/PnvPanel.Api/Telegram/TelegramBotHostedService.cs b/backend/src/PnvPanel.Api/Telegram/TelegramBotHostedService.cs index 6189042..106818d 100644 --- a/backend/src/PnvPanel.Api/Telegram/TelegramBotHostedService.cs +++ b/backend/src/PnvPanel.Api/Telegram/TelegramBotHostedService.cs @@ -13,9 +13,11 @@ namespace PnvPanel.Api.Telegram; /// вызывает те же CQRS-команды, что и веб, через собственный ISender. /// public sealed class TelegramBotHostedService( - ITelegramBotClient botClient, PnvBotUpdateHandler updateHandler, IOptions options, - ILogger logger) - : BackgroundService + ITelegramBotClient botClient, + PnvBotUpdateHandler updateHandler, + IOptions options, + ILogger logger +) : BackgroundService { private static readonly TimeSpan RetryDelay = TimeSpan.FromSeconds(30); diff --git a/backend/src/PnvPanel.Api/Telegram/TelegramBotInfo.cs b/backend/src/PnvPanel.Api/Telegram/TelegramBotInfo.cs index f155f12..541685d 100644 --- a/backend/src/PnvPanel.Api/Telegram/TelegramBotInfo.cs +++ b/backend/src/PnvPanel.Api/Telegram/TelegramBotInfo.cs @@ -6,7 +6,10 @@ using Telegram.Bot; namespace PnvPanel.Api.Telegram; /// Кэширует username бота на время жизни процесса (getMe не меняется, повторный запрос не нужен). -internal sealed class TelegramBotInfo(ITelegramBotClient botClient, IOptions options) : ITelegramBotInfo +internal sealed class TelegramBotInfo( + ITelegramBotClient botClient, + IOptions options +) : ITelegramBotInfo { private readonly SemaphoreSlim _lock = new(1, 1); private string? _cachedUsername; diff --git a/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs b/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs index 90f23e4..f8caa6e 100644 --- a/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs +++ b/backend/src/PnvPanel.Api/Telegram/TelegramNotifier.cs @@ -8,30 +8,49 @@ using Telegram.Bot.Types.ReplyMarkups; namespace PnvPanel.Api.Telegram; -internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentityService identityService, IOptions options) - : ITelegramNotifier +internal sealed class TelegramNotifier( + ITelegramBotClient botClient, + IIdentityService identityService, + IOptions options +) : ITelegramNotifier { public async Task NotifyAdminsActivationRequestedAsync( - Guid requestId, string userName, string? comment, CancellationToken cancellationToken) + Guid requestId, + string userName, + string? comment, + CancellationToken cancellationToken + ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; - var text = $"🆕 Запрос на активацию от {Escape(userName)}" - + (string.IsNullOrWhiteSpace(comment) ? string.Empty : $"\nКомментарий: {Escape(comment)}"); + var text = + $"🆕 Запрос на активацию от {Escape(userName)}" + + ( + string.IsNullOrWhiteSpace(comment) + ? string.Empty + : $"\nКомментарий: {Escape(comment)}" + ); - var keyboard = new InlineKeyboardMarkup(new[] - { - InlineKeyboardButton.WithCallbackData("✅ Активировать", $"act:approve:{requestId}"), - InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"act:reject:{requestId}"), - }); + var keyboard = new InlineKeyboardMarkup( + new[] + { + InlineKeyboardButton.WithCallbackData("✅ Активировать", $"act:approve:{requestId}"), + InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"act:reject:{requestId}"), + } + ); foreach (var adminId in options.Value.ParseAdminTelegramUserIds()) { try { await botClient.SendMessage( - adminId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken); + adminId, + text, + parseMode: ParseMode.Html, + replyMarkup: keyboard, + cancellationToken: cancellationToken + ); } catch { @@ -40,20 +59,28 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe } } - public async Task NotifyAdminsBugReportCreatedAsync(Guid ticketId, string userName, string message, CancellationToken cancellationToken) + public async Task NotifyAdminsBugReportCreatedAsync( + Guid ticketId, + string userName, + string message, + CancellationToken cancellationToken + ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; var preview = message.Length > 300 ? message[..300] + "…" : message; - var text = $"🐞 Новый тикет (баг/предложение) от {Escape(userName)}\n{Escape(preview)}"; + var text = + $"🐞 Новый тикет (баг/предложение) от {Escape(userName)}\n{Escape(preview)}"; // Только ссылка на сайт — переписка и картинки удобнее там, инлайн-действий для баг-тикетов нет. InlineKeyboardMarkup? keyboard = null; if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) { var url = $"{options.Value.PublicSiteUrl.TrimEnd('/')}/admin/support?ticket={ticketId}"; - keyboard = new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) }); + keyboard = new InlineKeyboardMarkup( + new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) } + ); } foreach (var adminId in options.Value.ParseAdminTelegramUserIds()) @@ -61,7 +88,12 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe try { await botClient.SendMessage( - adminId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken); + adminId, + text, + parseMode: ParseMode.Html, + replyMarkup: keyboard, + cancellationToken: cancellationToken + ); } catch { @@ -71,25 +103,38 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe } public async Task NotifyAdminsRoleRequestCreatedAsync( - Guid ticketId, string userName, string roleDescription, string justification, CancellationToken cancellationToken) + Guid ticketId, + string userName, + string roleDescription, + string justification, + CancellationToken cancellationToken + ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; - var text = $"🆕 Заявка на роль от {Escape(userName)}\n{Escape(roleDescription)}\nОбоснование: {Escape(justification)}"; + var text = + $"🆕 Заявка на роль от {Escape(userName)}\n{Escape(roleDescription)}\nОбоснование: {Escape(justification)}"; - var keyboard = new InlineKeyboardMarkup(new[] - { - InlineKeyboardButton.WithCallbackData("✅ Одобрить", $"rrq:approve:{ticketId}"), - InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"rrq:reject:{ticketId}"), - }); + var keyboard = new InlineKeyboardMarkup( + new[] + { + InlineKeyboardButton.WithCallbackData("✅ Одобрить", $"rrq:approve:{ticketId}"), + InlineKeyboardButton.WithCallbackData("❌ Отклонить", $"rrq:reject:{ticketId}"), + } + ); foreach (var adminId in options.Value.ParseAdminTelegramUserIds()) { try { await botClient.SendMessage( - adminId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken); + adminId, + text, + parseMode: ParseMode.Html, + replyMarkup: keyboard, + cancellationToken: cancellationToken + ); } catch { @@ -98,7 +143,12 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe } } - public async Task NotifyAdminsTicketReopenedAsync(Guid ticketId, string userName, TicketType type, CancellationToken cancellationToken) + public async Task NotifyAdminsTicketReopenedAsync( + Guid ticketId, + string userName, + TicketType type, + CancellationToken cancellationToken + ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; @@ -110,7 +160,9 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) { var url = $"{options.Value.PublicSiteUrl.TrimEnd('/')}/admin/support?ticket={ticketId}"; - keyboard = new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) }); + keyboard = new InlineKeyboardMarkup( + new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) } + ); } foreach (var adminId in options.Value.ParseAdminTelegramUserIds()) @@ -118,7 +170,12 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe try { await botClient.SendMessage( - adminId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken); + adminId, + text, + parseMode: ParseMode.Html, + replyMarkup: keyboard, + cancellationToken: cancellationToken + ); } catch { @@ -127,7 +184,10 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe } } - public async Task NotifyUsersNewsPublishedAsync(string title, CancellationToken cancellationToken) + public async Task NotifyUsersNewsPublishedAsync( + string title, + CancellationToken cancellationToken + ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; @@ -138,16 +198,25 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) { var url = $"{options.Value.PublicSiteUrl.TrimEnd('/')}/news"; - keyboard = new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) }); + keyboard = new InlineKeyboardMarkup( + new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", url) } + ); } - var telegramUserIds = await identityService.GetActivatedLinkedTelegramUserIdsAsync(cancellationToken); + var telegramUserIds = await identityService.GetActivatedLinkedTelegramUserIdsAsync( + cancellationToken + ); foreach (var telegramUserId in telegramUserIds) { try { await botClient.SendMessage( - telegramUserId, text, parseMode: ParseMode.Html, replyMarkup: keyboard, cancellationToken: cancellationToken); + telegramUserId, + text, + parseMode: ParseMode.Html, + replyMarkup: keyboard, + cancellationToken: cancellationToken + ); } catch { @@ -156,7 +225,11 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe } } - public async Task NotifyUserAsync(Guid userId, string message, CancellationToken cancellationToken) + public async Task NotifyUserAsync( + Guid userId, + string message, + CancellationToken cancellationToken + ) { if (string.IsNullOrWhiteSpace(options.Value.BotToken)) return; @@ -167,11 +240,24 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe InlineKeyboardMarkup? keyboard = null; if (!string.IsNullOrWhiteSpace(options.Value.PublicSiteUrl)) - keyboard = new InlineKeyboardMarkup(new[] { InlineKeyboardButton.WithUrl("🌐 Открыть на сайте", options.Value.PublicSiteUrl) }); + keyboard = new InlineKeyboardMarkup( + new[] + { + InlineKeyboardButton.WithUrl( + "🌐 Открыть на сайте", + options.Value.PublicSiteUrl + ), + } + ); try { - await botClient.SendMessage(telegramUserId, message, replyMarkup: keyboard, cancellationToken: cancellationToken); + await botClient.SendMessage( + telegramUserId, + message, + replyMarkup: keyboard, + cancellationToken: cancellationToken + ); } catch { @@ -179,5 +265,6 @@ internal sealed class TelegramNotifier(ITelegramBotClient botClient, IIdentitySe } } - private static string Escape(string text) => text.Replace("&", "&").Replace("<", "<").Replace(">", ">"); + private static string Escape(string text) => + text.Replace("&", "&").Replace("<", "<").Replace(">", ">"); } diff --git a/backend/src/PnvPanel.Application/Activation/ActivationErrors.cs b/backend/src/PnvPanel.Application/Activation/ActivationErrors.cs index eed1d2b..6359b6b 100644 --- a/backend/src/PnvPanel.Application/Activation/ActivationErrors.cs +++ b/backend/src/PnvPanel.Application/Activation/ActivationErrors.cs @@ -4,12 +4,18 @@ namespace PnvPanel.Application.Activation; public static class ActivationErrors { - public static readonly Error AlreadyPending = - Error.Conflict("Activation.AlreadyPending", "У вас уже есть необработанный запрос на активацию."); + public static readonly Error AlreadyPending = Error.Conflict( + "Activation.AlreadyPending", + "У вас уже есть необработанный запрос на активацию." + ); - public static readonly Error NotFound = - Error.NotFound("Activation.NotFound", "Запрос на активацию не найден."); + public static readonly Error NotFound = Error.NotFound( + "Activation.NotFound", + "Запрос на активацию не найден." + ); - public static readonly Error AlreadyDecided = - Error.Conflict("Activation.AlreadyDecided", "Запрос на активацию уже обработан."); + public static readonly Error AlreadyDecided = Error.Conflict( + "Activation.AlreadyDecided", + "Запрос на активацию уже обработан." + ); } diff --git a/backend/src/PnvPanel.Application/Activation/GetActivationStatusQueryHandler.cs b/backend/src/PnvPanel.Application/Activation/GetActivationStatusQueryHandler.cs index 54d2e05..172eef6 100644 --- a/backend/src/PnvPanel.Application/Activation/GetActivationStatusQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Activation/GetActivationStatusQueryHandler.cs @@ -7,10 +7,16 @@ using PnvPanel.Domain.Activation; namespace PnvPanel.Application.Activation; -public sealed class GetActivationStatusQueryHandler(IIdentityService identityService, IAppDbContext dbContext, ICurrentUser currentUser) - : IQueryHandler> +public sealed class GetActivationStatusQueryHandler( + IIdentityService identityService, + IAppDbContext dbContext, + ICurrentUser currentUser +) : IQueryHandler> { - public async Task> Handle(GetActivationStatusQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetActivationStatusQuery query, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); @@ -19,8 +25,10 @@ public sealed class GetActivationStatusQueryHandler(IIdentityService identitySer if (profile is null) return Result.Failure(AuthErrors.Unauthorized); - var pending = await dbContext.ActivationRequests - .Where(r => r.UserId == userId && r.Status == ActivationStatus.Pending) + var pending = await dbContext + .ActivationRequests.Where(r => + r.UserId == userId && r.Status == ActivationStatus.Pending + ) .Select(r => new ActivationRequestDto(r.Id, r.Comment, r.CreatedAt)) .FirstOrDefaultAsync(cancellationToken); diff --git a/backend/src/PnvPanel.Application/Activation/RequestActivationCommand.cs b/backend/src/PnvPanel.Application/Activation/RequestActivationCommand.cs index a601190..c4915e2 100644 --- a/backend/src/PnvPanel.Application/Activation/RequestActivationCommand.cs +++ b/backend/src/PnvPanel.Application/Activation/RequestActivationCommand.cs @@ -3,4 +3,5 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Activation; -public sealed record RequestActivationCommand(string? Comment) : ICommand>; +public sealed record RequestActivationCommand(string? Comment) + : ICommand>; diff --git a/backend/src/PnvPanel.Application/Activation/RequestActivationCommandHandler.cs b/backend/src/PnvPanel.Application/Activation/RequestActivationCommandHandler.cs index 200ec3d..0103375 100644 --- a/backend/src/PnvPanel.Application/Activation/RequestActivationCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Activation/RequestActivationCommandHandler.cs @@ -8,16 +8,24 @@ using PnvPanel.Domain.Activation; namespace PnvPanel.Application.Activation; public sealed class RequestActivationCommandHandler( - IAppDbContext dbContext, IRealtimeNotifier notifier, ITelegramNotifier telegramNotifier, ICurrentUser currentUser) - : ICommandHandler> + IAppDbContext dbContext, + IRealtimeNotifier notifier, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser +) : ICommandHandler> { - public async Task> Handle(RequestActivationCommand command, CancellationToken cancellationToken) + public async Task> Handle( + RequestActivationCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); - var hasPending = await dbContext.ActivationRequests - .AnyAsync(r => r.UserId == userId && r.Status == ActivationStatus.Pending, cancellationToken); + var hasPending = await dbContext.ActivationRequests.AnyAsync( + r => r.UserId == userId && r.Status == ActivationStatus.Pending, + cancellationToken + ); if (hasPending) return Result.Failure(ActivationErrors.AlreadyPending); @@ -27,9 +35,23 @@ public sealed class RequestActivationCommandHandler( var userName = currentUser.UserName ?? userId.ToString(); - await notifier.NotifyActivationRequestedAsync(request.Id, userId, userName, request.Comment, request.CreatedAt, cancellationToken); - await telegramNotifier.NotifyAdminsActivationRequestedAsync(request.Id, userName, request.Comment, cancellationToken); + await notifier.NotifyActivationRequestedAsync( + request.Id, + userId, + userName, + request.Comment, + request.CreatedAt, + cancellationToken + ); + await telegramNotifier.NotifyAdminsActivationRequestedAsync( + request.Id, + userName, + request.Comment, + cancellationToken + ); - return Result.Success(new ActivationRequestDto(request.Id, request.Comment, request.CreatedAt)); + return Result.Success( + new ActivationRequestDto(request.Id, request.Comment, request.CreatedAt) + ); } } diff --git a/backend/src/PnvPanel.Application/Admin/Activation/ActivationRequestAdminDto.cs b/backend/src/PnvPanel.Application/Admin/Activation/ActivationRequestAdminDto.cs index 21864f9..e67e9f0 100644 --- a/backend/src/PnvPanel.Application/Admin/Activation/ActivationRequestAdminDto.cs +++ b/backend/src/PnvPanel.Application/Admin/Activation/ActivationRequestAdminDto.cs @@ -8,4 +8,5 @@ public sealed record ActivationRequestAdminDto( string UserName, string? Comment, ActivationStatus Status, - DateTimeOffset CreatedAt); + DateTimeOffset CreatedAt +); diff --git a/backend/src/PnvPanel.Application/Admin/Activation/ApproveActivationCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Activation/ApproveActivationCommandHandler.cs index 074d479..fd60041 100644 --- a/backend/src/PnvPanel.Application/Admin/Activation/ApproveActivationCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Activation/ApproveActivationCommandHandler.cs @@ -10,17 +10,25 @@ using PnvPanel.Domain.Audit; namespace PnvPanel.Application.Admin.Activation; public sealed class ApproveActivationCommandHandler( - IAppDbContext dbContext, IIdentityService identityService, IRealtimeNotifier notifier, - ITelegramNotifier telegramNotifier, ICurrentUser currentUser) - : ICommandHandler + IAppDbContext dbContext, + IIdentityService identityService, + IRealtimeNotifier notifier, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(ApproveActivationCommand command, CancellationToken cancellationToken) + public async Task Handle( + ApproveActivationCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } adminId) return Result.Failure(AuthErrors.Unauthorized); - var request = await dbContext.ActivationRequests - .FirstOrDefaultAsync(r => r.Id == command.RequestId, cancellationToken); + var request = await dbContext.ActivationRequests.FirstOrDefaultAsync( + r => r.Id == command.RequestId, + cancellationToken + ); if (request is null) return Result.Failure(ActivationErrors.NotFound); @@ -30,15 +38,31 @@ public sealed class ApproveActivationCommandHandler( request.Approve(adminId); - var activateResult = await identityService.ActivateUserAsync(request.UserId, adminId, cancellationToken); + var activateResult = await identityService.ActivateUserAsync( + request.UserId, + adminId, + cancellationToken + ); if (!activateResult.IsSuccess) return activateResult; - dbContext.AuditLogs.Add(AuditLog.Create( - adminId, "ActivationApproved", "User", request.UserId.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + adminId, + "ActivationApproved", + "User", + request.UserId.ToString(), + metadata: null, + AuditSource.Web + ) + ); await notifier.NotifyUserActivatedAsync(request.UserId, cancellationToken); - await telegramNotifier.NotifyUserAsync(request.UserId, "✅ Ваш аккаунт активирован администратором.", cancellationToken); + await telegramNotifier.NotifyUserAsync( + request.UserId, + "✅ Ваш аккаунт активирован администратором.", + cancellationToken + ); return Result.Success(); } } diff --git a/backend/src/PnvPanel.Application/Admin/Activation/ListActivationRequestsQuery.cs b/backend/src/PnvPanel.Application/Admin/Activation/ListActivationRequestsQuery.cs index b9f3b67..b92a255 100644 --- a/backend/src/PnvPanel.Application/Admin/Activation/ListActivationRequestsQuery.cs +++ b/backend/src/PnvPanel.Application/Admin/Activation/ListActivationRequestsQuery.cs @@ -4,5 +4,8 @@ using PnvPanel.Domain.Activation; namespace PnvPanel.Application.Admin.Activation; -public sealed record ListActivationRequestsQuery(ActivationStatus? StatusFilter, int Page, int PageSize) - : IQuery>>; +public sealed record ListActivationRequestsQuery( + ActivationStatus? StatusFilter, + int Page, + int PageSize +) : IQuery>>; diff --git a/backend/src/PnvPanel.Application/Admin/Activation/ListActivationRequestsQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Activation/ListActivationRequestsQueryHandler.cs index b72d243..25a21c8 100644 --- a/backend/src/PnvPanel.Application/Admin/Activation/ListActivationRequestsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Activation/ListActivationRequestsQueryHandler.cs @@ -5,10 +5,15 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Activation; -public sealed class ListActivationRequestsQueryHandler(IAppDbContext dbContext, IIdentityService identityService) - : IQueryHandler>> +public sealed class ListActivationRequestsQueryHandler( + IAppDbContext dbContext, + IIdentityService identityService +) : IQueryHandler>> { - public async Task>> Handle(ListActivationRequestsQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListActivationRequestsQuery query, + CancellationToken cancellationToken + ) { var page = query.Page <= 0 ? 1 : query.Page; var pageSize = query.PageSize is <= 0 or > 100 ? 20 : query.PageSize; @@ -23,13 +28,22 @@ public sealed class ListActivationRequestsQueryHandler(IAppDbContext dbContext, var userNames = await identityService.GetUserNamesAsync( page1.Items.Select(r => r.UserId).Distinct().ToList(), - cancellationToken); + cancellationToken + ); - var items = page1.Items - .Select(r => new ActivationRequestAdminDto( - r.Id, r.UserId, userNames.GetValueOrDefault(r.UserId, "?"), r.Comment, r.Status, r.CreatedAt)) + var items = page1 + .Items.Select(r => new ActivationRequestAdminDto( + r.Id, + r.UserId, + userNames.GetValueOrDefault(r.UserId, "?"), + r.Comment, + r.Status, + r.CreatedAt + )) .ToList(); - return Result.Success(new PagedList(items, page1.Total, page1.Page, page1.PageSize)); + return Result.Success( + new PagedList(items, page1.Total, page1.Page, page1.PageSize) + ); } } diff --git a/backend/src/PnvPanel.Application/Admin/Activation/RejectActivationCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Activation/RejectActivationCommandHandler.cs index d5e643f..4b567c5 100644 --- a/backend/src/PnvPanel.Application/Admin/Activation/RejectActivationCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Activation/RejectActivationCommandHandler.cs @@ -9,16 +9,23 @@ using PnvPanel.Domain.Audit; namespace PnvPanel.Application.Admin.Activation; -public sealed class RejectActivationCommandHandler(IAppDbContext dbContext, ICurrentUser currentUser) - : ICommandHandler +public sealed class RejectActivationCommandHandler( + IAppDbContext dbContext, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(RejectActivationCommand command, CancellationToken cancellationToken) + public async Task Handle( + RejectActivationCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } adminId) return Result.Failure(AuthErrors.Unauthorized); - var request = await dbContext.ActivationRequests - .FirstOrDefaultAsync(r => r.Id == command.RequestId, cancellationToken); + var request = await dbContext.ActivationRequests.FirstOrDefaultAsync( + r => r.Id == command.RequestId, + cancellationToken + ); if (request is null) return Result.Failure(ActivationErrors.NotFound); @@ -28,8 +35,16 @@ public sealed class RejectActivationCommandHandler(IAppDbContext dbContext, ICur request.Reject(adminId, command.Reason); - dbContext.AuditLogs.Add(AuditLog.Create( - adminId, "ActivationRejected", "User", request.UserId.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + adminId, + "ActivationRejected", + "User", + request.UserId.ToString(), + metadata: null, + AuditSource.Web + ) + ); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Admin/Apps/AdminAppDto.cs b/backend/src/PnvPanel.Application/Admin/Apps/AdminAppDto.cs index e430043..06da96e 100644 --- a/backend/src/PnvPanel.Application/Admin/Apps/AdminAppDto.cs +++ b/backend/src/PnvPanel.Application/Admin/Apps/AdminAppDto.cs @@ -3,10 +3,25 @@ using PnvPanel.Domain.Apps; namespace PnvPanel.Application.Admin.Apps; public sealed record AdminAppDto( - Guid Id, string Name, string DownloadUrl, OsPlatform OperatingSystem, string? Description, - string? IconUrl, int SortOrder, bool IsEnabled) + Guid Id, + string Name, + string DownloadUrl, + OsPlatform OperatingSystem, + string? Description, + string? IconUrl, + int SortOrder, + bool IsEnabled +) { - public static AdminAppDto FromDomain(ClientApp app) => new( - app.Id, app.Name, app.DownloadUrl.ToString(), app.OperatingSystem, app.Description, - app.IconUrl, app.SortOrder, app.IsEnabled); + public static AdminAppDto FromDomain(ClientApp app) => + new( + app.Id, + app.Name, + app.DownloadUrl.ToString(), + app.OperatingSystem, + app.Description, + app.IconUrl, + app.SortOrder, + app.IsEnabled + ); } diff --git a/backend/src/PnvPanel.Application/Admin/Apps/AppErrors.cs b/backend/src/PnvPanel.Application/Admin/Apps/AppErrors.cs index 7e16574..267be8e 100644 --- a/backend/src/PnvPanel.Application/Admin/Apps/AppErrors.cs +++ b/backend/src/PnvPanel.Application/Admin/Apps/AppErrors.cs @@ -4,5 +4,8 @@ namespace PnvPanel.Application.Admin.Apps; public static class AppErrors { - public static readonly Error NotFound = Error.NotFound("Apps.NotFound", "Приложение не найдено."); + public static readonly Error NotFound = Error.NotFound( + "Apps.NotFound", + "Приложение не найдено." + ); } diff --git a/backend/src/PnvPanel.Application/Admin/Apps/CreateAppCommand.cs b/backend/src/PnvPanel.Application/Admin/Apps/CreateAppCommand.cs index bb55ff9..ee0215f 100644 --- a/backend/src/PnvPanel.Application/Admin/Apps/CreateAppCommand.cs +++ b/backend/src/PnvPanel.Application/Admin/Apps/CreateAppCommand.cs @@ -5,5 +5,10 @@ using PnvPanel.Domain.Apps; namespace PnvPanel.Application.Admin.Apps; public sealed record CreateAppCommand( - string Name, string DownloadUrl, OsPlatform OperatingSystem, string? Description, string? IconUrl, int SortOrder) - : ICommand>; + string Name, + string DownloadUrl, + OsPlatform OperatingSystem, + string? Description, + string? IconUrl, + int SortOrder +) : ICommand>; diff --git a/backend/src/PnvPanel.Application/Admin/Apps/CreateAppCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Apps/CreateAppCommandHandler.cs index 434fbae..dc18865 100644 --- a/backend/src/PnvPanel.Application/Admin/Apps/CreateAppCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Apps/CreateAppCommandHandler.cs @@ -5,13 +5,22 @@ using PnvPanel.Domain.Apps; namespace PnvPanel.Application.Admin.Apps; -public sealed class CreateAppCommandHandler(IAppDbContext dbContext) : ICommandHandler> +public sealed class CreateAppCommandHandler(IAppDbContext dbContext) + : ICommandHandler> { - public Task> Handle(CreateAppCommand command, CancellationToken cancellationToken) + public Task> Handle( + CreateAppCommand command, + CancellationToken cancellationToken + ) { var app = ClientApp.Create( - command.Name, new Uri(command.DownloadUrl, UriKind.Absolute), command.OperatingSystem, - command.Description, command.IconUrl, command.SortOrder); + command.Name, + new Uri(command.DownloadUrl, UriKind.Absolute), + command.OperatingSystem, + command.Description, + command.IconUrl, + command.SortOrder + ); dbContext.ClientApps.Add(app); diff --git a/backend/src/PnvPanel.Application/Admin/Apps/DeleteAppCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Apps/DeleteAppCommandHandler.cs index 2d6d1a1..6d3de73 100644 --- a/backend/src/PnvPanel.Application/Admin/Apps/DeleteAppCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Apps/DeleteAppCommandHandler.cs @@ -5,11 +5,15 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Apps; -public sealed class DeleteAppCommandHandler(IAppDbContext dbContext) : ICommandHandler +public sealed class DeleteAppCommandHandler(IAppDbContext dbContext) + : ICommandHandler { public async Task Handle(DeleteAppCommand command, CancellationToken cancellationToken) { - var app = await dbContext.ClientApps.FirstOrDefaultAsync(a => a.Id == command.AppId, cancellationToken); + var app = await dbContext.ClientApps.FirstOrDefaultAsync( + a => a.Id == command.AppId, + cancellationToken + ); if (app is null) return Result.Failure(AppErrors.NotFound); diff --git a/backend/src/PnvPanel.Application/Admin/Apps/ListAdminAppsQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Apps/ListAdminAppsQueryHandler.cs index 95b390f..164decc 100644 --- a/backend/src/PnvPanel.Application/Admin/Apps/ListAdminAppsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Apps/ListAdminAppsQueryHandler.cs @@ -5,14 +5,22 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Apps; -public sealed class ListAdminAppsQueryHandler(IAppDbContext dbContext) : IQueryHandler>> +public sealed class ListAdminAppsQueryHandler(IAppDbContext dbContext) + : IQueryHandler>> { - public async Task>> Handle(ListAdminAppsQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListAdminAppsQuery query, + CancellationToken cancellationToken + ) { - var apps = await dbContext.ClientApps.AsNoTracking() - .OrderBy(a => a.OperatingSystem).ThenBy(a => a.SortOrder) + var apps = await dbContext + .ClientApps.AsNoTracking() + .OrderBy(a => a.OperatingSystem) + .ThenBy(a => a.SortOrder) .ToListAsync(cancellationToken); - return Result.Success>(apps.Select(AdminAppDto.FromDomain).ToList()); + return Result.Success>( + apps.Select(AdminAppDto.FromDomain).ToList() + ); } } diff --git a/backend/src/PnvPanel.Application/Admin/Apps/UpdateAppCommand.cs b/backend/src/PnvPanel.Application/Admin/Apps/UpdateAppCommand.cs index cf5d089..e01a8fa 100644 --- a/backend/src/PnvPanel.Application/Admin/Apps/UpdateAppCommand.cs +++ b/backend/src/PnvPanel.Application/Admin/Apps/UpdateAppCommand.cs @@ -5,6 +5,12 @@ using PnvPanel.Domain.Apps; namespace PnvPanel.Application.Admin.Apps; public sealed record UpdateAppCommand( - Guid AppId, string Name, string DownloadUrl, OsPlatform OperatingSystem, string? Description, - string? IconUrl, int SortOrder, bool IsEnabled) - : ICommand>; + Guid AppId, + string Name, + string DownloadUrl, + OsPlatform OperatingSystem, + string? Description, + string? IconUrl, + int SortOrder, + bool IsEnabled +) : ICommand>; diff --git a/backend/src/PnvPanel.Application/Admin/Apps/UpdateAppCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Apps/UpdateAppCommandHandler.cs index 2ad1f2b..0069f85 100644 --- a/backend/src/PnvPanel.Application/Admin/Apps/UpdateAppCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Apps/UpdateAppCommandHandler.cs @@ -5,17 +5,30 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Apps; -public sealed class UpdateAppCommandHandler(IAppDbContext dbContext) : ICommandHandler> +public sealed class UpdateAppCommandHandler(IAppDbContext dbContext) + : ICommandHandler> { - public async Task> Handle(UpdateAppCommand command, CancellationToken cancellationToken) + public async Task> Handle( + UpdateAppCommand command, + CancellationToken cancellationToken + ) { - var app = await dbContext.ClientApps.FirstOrDefaultAsync(a => a.Id == command.AppId, cancellationToken); + var app = await dbContext.ClientApps.FirstOrDefaultAsync( + a => a.Id == command.AppId, + cancellationToken + ); if (app is null) return Result.Failure(AppErrors.NotFound); app.Update( - command.Name, new Uri(command.DownloadUrl, UriKind.Absolute), command.OperatingSystem, - command.Description, command.IconUrl, command.SortOrder, command.IsEnabled); + command.Name, + new Uri(command.DownloadUrl, UriKind.Absolute), + command.OperatingSystem, + command.Description, + command.IconUrl, + command.SortOrder, + command.IsEnabled + ); return Result.Success(AdminAppDto.FromDomain(app)); } diff --git a/backend/src/PnvPanel.Application/Admin/Audit/ListAuditLogsQuery.cs b/backend/src/PnvPanel.Application/Admin/Audit/ListAuditLogsQuery.cs index e33a781..cee2f38 100644 --- a/backend/src/PnvPanel.Application/Admin/Audit/ListAuditLogsQuery.cs +++ b/backend/src/PnvPanel.Application/Admin/Audit/ListAuditLogsQuery.cs @@ -4,8 +4,16 @@ using PnvPanel.Domain.Audit; namespace PnvPanel.Application.Admin.Audit; -public sealed record ListAuditLogsQuery(int Page, int PageSize) : IQuery>>; +public sealed record ListAuditLogsQuery(int Page, int PageSize) + : IQuery>>; public sealed record AuditLogDto( - long Id, Guid? ActorId, string Action, string TargetType, string TargetId, string? Metadata, - AuditSource Source, DateTimeOffset CreatedAt); + long Id, + Guid? ActorId, + string Action, + string TargetType, + string TargetId, + string? Metadata, + AuditSource Source, + DateTimeOffset CreatedAt +); diff --git a/backend/src/PnvPanel.Application/Admin/Audit/ListAuditLogsQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Audit/ListAuditLogsQueryHandler.cs index 27aca6e..1897c4f 100644 --- a/backend/src/PnvPanel.Application/Admin/Audit/ListAuditLogsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Audit/ListAuditLogsQueryHandler.cs @@ -5,16 +5,30 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Audit; -public sealed class ListAuditLogsQueryHandler(IAppDbContext dbContext) : IQueryHandler>> +public sealed class ListAuditLogsQueryHandler(IAppDbContext dbContext) + : IQueryHandler>> { - public async Task>> Handle(ListAuditLogsQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListAuditLogsQuery query, + CancellationToken cancellationToken + ) { var page = query.Page <= 0 ? 1 : query.Page; var pageSize = query.PageSize is <= 0 or > 200 ? 50 : query.PageSize; - var result = await dbContext.AuditLogs.AsNoTracking() + var result = await dbContext + .AuditLogs.AsNoTracking() .OrderByDescending(a => a.CreatedAt) - .Select(a => new AuditLogDto(a.Id, a.ActorId, a.Action, a.TargetType, a.TargetId, a.Metadata, a.Source, a.CreatedAt)) + .Select(a => new AuditLogDto( + a.Id, + a.ActorId, + a.Action, + a.TargetType, + a.TargetId, + a.Metadata, + a.Source, + a.CreatedAt + )) .ToPagedListAsync(page, pageSize, cancellationToken); return Result.Success(result); diff --git a/backend/src/PnvPanel.Application/Admin/Configs/AdminVpnConfigDto.cs b/backend/src/PnvPanel.Application/Admin/Configs/AdminVpnConfigDto.cs index 33b4ffc..49bb562 100644 --- a/backend/src/PnvPanel.Application/Admin/Configs/AdminVpnConfigDto.cs +++ b/backend/src/PnvPanel.Application/Admin/Configs/AdminVpnConfigDto.cs @@ -6,6 +6,17 @@ namespace PnvPanel.Application.Admin.Configs; /// Строка глобального списка конфигов для админа — в отличие от VpnConfigDto (self-service) /// содержит владельца и ноду, т.к. список не скоупится одним пользователем. public sealed record AdminVpnConfigDto( - Guid Id, Guid UserId, string UserName, string? Label, string ClientEmail, VpnProtocol Protocol, - string Location, string NodeName, long UsedUpBytes, long UsedDownBytes, DateTimeOffset? ExpiresAt, - ConfigStatus Status, DateTimeOffset CreatedAt); + Guid Id, + Guid UserId, + string UserName, + string? Label, + string ClientEmail, + VpnProtocol Protocol, + string Location, + string NodeName, + long UsedUpBytes, + long UsedDownBytes, + DateTimeOffset? ExpiresAt, + ConfigStatus Status, + DateTimeOffset CreatedAt +); diff --git a/backend/src/PnvPanel.Application/Admin/Configs/ListAllConfigsQuery.cs b/backend/src/PnvPanel.Application/Admin/Configs/ListAllConfigsQuery.cs index 723f904..a61fc0e 100644 --- a/backend/src/PnvPanel.Application/Admin/Configs/ListAllConfigsQuery.cs +++ b/backend/src/PnvPanel.Application/Admin/Configs/ListAllConfigsQuery.cs @@ -7,5 +7,9 @@ namespace PnvPanel.Application.Admin.Configs; /// матчится по ClientEmail/Label — это то, по чему админ сверяет /// конфиг с записью в 3x-ui, а не по владельцу (для поиска по пользователю есть /admin/users). -public sealed record ListAllConfigsQuery(int Page, int PageSize, string? Search, ConfigStatus? Status) - : IQuery>>; +public sealed record ListAllConfigsQuery( + int Page, + int PageSize, + string? Search, + ConfigStatus? Status +) : IQuery>>; diff --git a/backend/src/PnvPanel.Application/Admin/Configs/ListAllConfigsQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Configs/ListAllConfigsQueryHandler.cs index 9318447..b3c7ad0 100644 --- a/backend/src/PnvPanel.Application/Admin/Configs/ListAllConfigsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Configs/ListAllConfigsQueryHandler.cs @@ -5,10 +5,15 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Configs; -public sealed class ListAllConfigsQueryHandler(IAppDbContext dbContext, IIdentityService identityService) - : IQueryHandler>> +public sealed class ListAllConfigsQueryHandler( + IAppDbContext dbContext, + IIdentityService identityService +) : IQueryHandler>> { - public async Task>> Handle(ListAllConfigsQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListAllConfigsQuery query, + CancellationToken cancellationToken + ) { var page = query.Page <= 0 ? 1 : query.Page; var pageSize = query.PageSize is <= 0 or > 100 ? 20 : query.PageSize; @@ -21,7 +26,9 @@ public sealed class ListAllConfigsQueryHandler(IAppDbContext dbContext, IIdentit if (!string.IsNullOrWhiteSpace(query.Search)) { var search = query.Search.Trim(); - configsQuery = configsQuery.Where(c => c.ClientEmail.Contains(search) || (c.Label != null && c.Label.Contains(search))); + configsQuery = configsQuery.Where(c => + c.ClientEmail.Contains(search) || (c.Label != null && c.Label.Contains(search)) + ); } var pageResult = await configsQuery @@ -29,30 +36,56 @@ public sealed class ListAllConfigsQueryHandler(IAppDbContext dbContext, IIdentit .ToPagedListAsync(page, pageSize, cancellationToken); var inboundIds = pageResult.Items.Select(c => c.InboundId).Distinct().ToList(); - var inbounds = (await dbContext.Inbounds.AsNoTracking() + var inbounds = ( + await dbContext + .Inbounds.AsNoTracking() .Where(i => inboundIds.Contains(i.Id)) - .ToListAsync(cancellationToken)) - .ToDictionary(i => i.Id); + .ToListAsync(cancellationToken) + ).ToDictionary(i => i.Id); var nodeIds = inbounds.Values.Select(i => i.NodeId).Distinct().ToList(); - var nodes = (await dbContext.Nodes.AsNoTracking() + var nodes = ( + await dbContext + .Nodes.AsNoTracking() .Where(n => nodeIds.Contains(n.Id)) - .ToListAsync(cancellationToken)) - .ToDictionary(n => n.Id); + .ToListAsync(cancellationToken) + ).ToDictionary(n => n.Id); var userNames = await identityService.GetUserNamesAsync( - pageResult.Items.Select(c => c.UserId).Distinct().ToList(), cancellationToken); + pageResult.Items.Select(c => c.UserId).Distinct().ToList(), + cancellationToken + ); - var items = pageResult.Items.Select(c => - { - var inbound = inbounds.GetValueOrDefault(c.InboundId); - var node = inbound is null ? null : nodes.GetValueOrDefault(inbound.NodeId); - return new AdminVpnConfigDto( - c.Id, c.UserId, userNames.GetValueOrDefault(c.UserId, "?"), c.Label, c.ClientEmail, c.Protocol, - inbound?.DisplayName ?? inbound?.Remark ?? "?", node?.Name ?? "?", - c.UsedUpBytes, c.UsedDownBytes, c.ExpiresAt, c.Status, c.CreatedAt); - }).ToList(); + var items = pageResult + .Items.Select(c => + { + var inbound = inbounds.GetValueOrDefault(c.InboundId); + var node = inbound is null ? null : nodes.GetValueOrDefault(inbound.NodeId); + return new AdminVpnConfigDto( + c.Id, + c.UserId, + userNames.GetValueOrDefault(c.UserId, "?"), + c.Label, + c.ClientEmail, + c.Protocol, + inbound?.DisplayName ?? inbound?.Remark ?? "?", + node?.Name ?? "?", + c.UsedUpBytes, + c.UsedDownBytes, + c.ExpiresAt, + c.Status, + c.CreatedAt + ); + }) + .ToList(); - return Result.Success(new PagedList(items, pageResult.Total, pageResult.Page, pageResult.PageSize)); + return Result.Success( + new PagedList( + items, + pageResult.Total, + pageResult.Page, + pageResult.PageSize + ) + ); } } diff --git a/backend/src/PnvPanel.Application/Admin/Inbounds/InboundDto.cs b/backend/src/PnvPanel.Application/Admin/Inbounds/InboundDto.cs index d8b260a..3624843 100644 --- a/backend/src/PnvPanel.Application/Admin/Inbounds/InboundDto.cs +++ b/backend/src/PnvPanel.Application/Admin/Inbounds/InboundDto.cs @@ -3,11 +3,31 @@ using PnvPanel.Domain.Inbounds; namespace PnvPanel.Application.Admin.Inbounds; public sealed record InboundDto( - Guid Id, Guid NodeId, string RemoteInboundId, VpnProtocol Protocol, string Remark, int Port, - bool IsPublished, string? DisplayName, int? MaxClients, IReadOnlyList AllowedRoleIds, - DateTimeOffset? LastSyncAt) + Guid Id, + Guid NodeId, + string RemoteInboundId, + VpnProtocol Protocol, + string Remark, + int Port, + bool IsPublished, + string? DisplayName, + int? MaxClients, + IReadOnlyList AllowedRoleIds, + DateTimeOffset? LastSyncAt +) { - public static InboundDto FromDomain(Inbound inbound) => new( - inbound.Id, inbound.NodeId, inbound.RemoteInboundId, inbound.Protocol, inbound.Remark, inbound.Port, - inbound.IsPublished, inbound.DisplayName, inbound.MaxClients, inbound.AllowedRoleIds, inbound.LastSyncAt); + public static InboundDto FromDomain(Inbound inbound) => + new( + inbound.Id, + inbound.NodeId, + inbound.RemoteInboundId, + inbound.Protocol, + inbound.Remark, + inbound.Port, + inbound.IsPublished, + inbound.DisplayName, + inbound.MaxClients, + inbound.AllowedRoleIds, + inbound.LastSyncAt + ); } diff --git a/backend/src/PnvPanel.Application/Admin/Inbounds/InboundErrors.cs b/backend/src/PnvPanel.Application/Admin/Inbounds/InboundErrors.cs index ceac891..4d7a472 100644 --- a/backend/src/PnvPanel.Application/Admin/Inbounds/InboundErrors.cs +++ b/backend/src/PnvPanel.Application/Admin/Inbounds/InboundErrors.cs @@ -4,5 +4,8 @@ namespace PnvPanel.Application.Admin.Inbounds; public static class InboundErrors { - public static readonly Error NotFound = Error.NotFound("Inbounds.NotFound", "Inbound не найден."); + public static readonly Error NotFound = Error.NotFound( + "Inbounds.NotFound", + "Inbound не найден." + ); } diff --git a/backend/src/PnvPanel.Application/Admin/Inbounds/ListInboundsQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Inbounds/ListInboundsQueryHandler.cs index d71e5be..5d61839 100644 --- a/backend/src/PnvPanel.Application/Admin/Inbounds/ListInboundsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Inbounds/ListInboundsQueryHandler.cs @@ -5,15 +5,21 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Inbounds; -public sealed class ListInboundsQueryHandler(IAppDbContext dbContext) : IQueryHandler>> +public sealed class ListInboundsQueryHandler(IAppDbContext dbContext) + : IQueryHandler>> { - public async Task>> Handle(ListInboundsQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListInboundsQuery query, + CancellationToken cancellationToken + ) { var inboundsQuery = dbContext.Inbounds.AsNoTracking(); if (query.NodeId is { } nodeId) inboundsQuery = inboundsQuery.Where(i => i.NodeId == nodeId); var inbounds = await inboundsQuery.OrderBy(i => i.Remark).ToListAsync(cancellationToken); - return Result.Success>(inbounds.Select(InboundDto.FromDomain).ToList()); + return Result.Success>( + inbounds.Select(InboundDto.FromDomain).ToList() + ); } } diff --git a/backend/src/PnvPanel.Application/Admin/Inbounds/PublishInboundCommand.cs b/backend/src/PnvPanel.Application/Admin/Inbounds/PublishInboundCommand.cs index e84b41c..b8b8529 100644 --- a/backend/src/PnvPanel.Application/Admin/Inbounds/PublishInboundCommand.cs +++ b/backend/src/PnvPanel.Application/Admin/Inbounds/PublishInboundCommand.cs @@ -4,5 +4,9 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Inbounds; public sealed record PublishInboundCommand( - Guid InboundId, bool IsPublished, string? DisplayName, IReadOnlyList AllowedRoleIds, int? MaxClients) - : ICommand>; + Guid InboundId, + bool IsPublished, + string? DisplayName, + IReadOnlyList AllowedRoleIds, + int? MaxClients +) : ICommand>; diff --git a/backend/src/PnvPanel.Application/Admin/Inbounds/PublishInboundCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Inbounds/PublishInboundCommandHandler.cs index 77e90dd..ab341d3 100644 --- a/backend/src/PnvPanel.Application/Admin/Inbounds/PublishInboundCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Inbounds/PublishInboundCommandHandler.cs @@ -9,9 +9,15 @@ namespace PnvPanel.Application.Admin.Inbounds; public sealed class PublishInboundCommandHandler(IAppDbContext dbContext, ICurrentUser currentUser) : ICommandHandler> { - public async Task> Handle(PublishInboundCommand command, CancellationToken cancellationToken) + public async Task> Handle( + PublishInboundCommand command, + CancellationToken cancellationToken + ) { - var inbound = await dbContext.Inbounds.FirstOrDefaultAsync(i => i.Id == command.InboundId, cancellationToken); + var inbound = await dbContext.Inbounds.FirstOrDefaultAsync( + i => i.Id == command.InboundId, + cancellationToken + ); if (inbound is null) return Result.Failure(InboundErrors.NotFound); @@ -20,9 +26,16 @@ public sealed class PublishInboundCommandHandler(IAppDbContext dbContext, ICurre else inbound.Unpublish(); - dbContext.AuditLogs.Add(AuditLog.Create( - currentUser.UserId, command.IsPublished ? "InboundPublished" : "InboundUnpublished", - "Inbound", inbound.Id.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + currentUser.UserId, + command.IsPublished ? "InboundPublished" : "InboundUnpublished", + "Inbound", + inbound.Id.ToString(), + metadata: null, + AuditSource.Web + ) + ); return Result.Success(InboundDto.FromDomain(inbound)); } diff --git a/backend/src/PnvPanel.Application/Admin/News/CreatePostCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/News/CreatePostCommandHandler.cs index d6dee7d..a487e03 100644 --- a/backend/src/PnvPanel.Application/Admin/News/CreatePostCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/News/CreatePostCommandHandler.cs @@ -6,15 +6,26 @@ using PnvPanel.Domain.News; namespace PnvPanel.Application.Admin.News; -public sealed class CreatePostCommandHandler(IAppDbContext dbContext, IRealtimeNotifier notifier, ITelegramNotifier telegramNotifier) - : ICommandHandler> +public sealed class CreatePostCommandHandler( + IAppDbContext dbContext, + IRealtimeNotifier notifier, + ITelegramNotifier telegramNotifier +) : ICommandHandler> { - public async Task> Handle(CreatePostCommand command, CancellationToken cancellationToken) + public async Task> Handle( + CreatePostCommand command, + CancellationToken cancellationToken + ) { var post = NewsPost.Create(command.Title, command.Body); dbContext.NewsPosts.Add(post); - await notifier.NotifyNewsPublishedAsync(post.Id, post.Title, post.CreatedAt, cancellationToken); + await notifier.NotifyNewsPublishedAsync( + post.Id, + post.Title, + post.CreatedAt, + cancellationToken + ); await telegramNotifier.NotifyUsersNewsPublishedAsync(post.Title, cancellationToken); return Result.Success(NewsPostDto.FromDomain(post)); diff --git a/backend/src/PnvPanel.Application/Admin/News/DeletePostCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/News/DeletePostCommandHandler.cs index 4217794..bde163e 100644 --- a/backend/src/PnvPanel.Application/Admin/News/DeletePostCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/News/DeletePostCommandHandler.cs @@ -5,11 +5,15 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.News; -public sealed class DeletePostCommandHandler(IAppDbContext dbContext) : ICommandHandler +public sealed class DeletePostCommandHandler(IAppDbContext dbContext) + : ICommandHandler { public async Task Handle(DeletePostCommand command, CancellationToken cancellationToken) { - var post = await dbContext.NewsPosts.FirstOrDefaultAsync(p => p.Id == command.PostId, cancellationToken); + var post = await dbContext.NewsPosts.FirstOrDefaultAsync( + p => p.Id == command.PostId, + cancellationToken + ); if (post is null) return Result.Failure(NewsErrors.NotFound); diff --git a/backend/src/PnvPanel.Application/Admin/News/ListAdminNewsQuery.cs b/backend/src/PnvPanel.Application/Admin/News/ListAdminNewsQuery.cs index db07567..b06b3a7 100644 --- a/backend/src/PnvPanel.Application/Admin/News/ListAdminNewsQuery.cs +++ b/backend/src/PnvPanel.Application/Admin/News/ListAdminNewsQuery.cs @@ -4,4 +4,5 @@ using PnvPanel.Application.News; namespace PnvPanel.Application.Admin.News; -public sealed record ListAdminNewsQuery(int Page, int PageSize) : IQuery>>; +public sealed record ListAdminNewsQuery(int Page, int PageSize) + : IQuery>>; diff --git a/backend/src/PnvPanel.Application/Admin/News/ListAdminNewsQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/News/ListAdminNewsQueryHandler.cs index 7bf3d8f..8048ab4 100644 --- a/backend/src/PnvPanel.Application/Admin/News/ListAdminNewsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/News/ListAdminNewsQueryHandler.cs @@ -6,14 +6,19 @@ using PnvPanel.Application.News; namespace PnvPanel.Application.Admin.News; -public sealed class ListAdminNewsQueryHandler(IAppDbContext dbContext) : IQueryHandler>> +public sealed class ListAdminNewsQueryHandler(IAppDbContext dbContext) + : IQueryHandler>> { - public async Task>> Handle(ListAdminNewsQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListAdminNewsQuery query, + CancellationToken cancellationToken + ) { var page = query.Page <= 0 ? 1 : query.Page; var pageSize = query.PageSize is <= 0 or > 100 ? 20 : query.PageSize; - var result = await dbContext.NewsPosts.AsNoTracking() + var result = await dbContext + .NewsPosts.AsNoTracking() .OrderByDescending(p => p.CreatedAt) .Select(p => new NewsPostDto(p.Id, p.Title, p.Body, p.CreatedAt, p.UpdatedAt)) .ToPagedListAsync(page, pageSize, cancellationToken); diff --git a/backend/src/PnvPanel.Application/Admin/News/UpdatePostCommand.cs b/backend/src/PnvPanel.Application/Admin/News/UpdatePostCommand.cs index db67b58..5084d5c 100644 --- a/backend/src/PnvPanel.Application/Admin/News/UpdatePostCommand.cs +++ b/backend/src/PnvPanel.Application/Admin/News/UpdatePostCommand.cs @@ -4,4 +4,5 @@ using PnvPanel.Application.News; namespace PnvPanel.Application.Admin.News; -public sealed record UpdatePostCommand(Guid PostId, string Title, string Body) : ICommand>; +public sealed record UpdatePostCommand(Guid PostId, string Title, string Body) + : ICommand>; diff --git a/backend/src/PnvPanel.Application/Admin/News/UpdatePostCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/News/UpdatePostCommandHandler.cs index 65db338..0e9db5b 100644 --- a/backend/src/PnvPanel.Application/Admin/News/UpdatePostCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/News/UpdatePostCommandHandler.cs @@ -6,11 +6,18 @@ using PnvPanel.Application.News; namespace PnvPanel.Application.Admin.News; -public sealed class UpdatePostCommandHandler(IAppDbContext dbContext) : ICommandHandler> +public sealed class UpdatePostCommandHandler(IAppDbContext dbContext) + : ICommandHandler> { - public async Task> Handle(UpdatePostCommand command, CancellationToken cancellationToken) + public async Task> Handle( + UpdatePostCommand command, + CancellationToken cancellationToken + ) { - var post = await dbContext.NewsPosts.FirstOrDefaultAsync(p => p.Id == command.PostId, cancellationToken); + var post = await dbContext.NewsPosts.FirstOrDefaultAsync( + p => p.Id == command.PostId, + cancellationToken + ); if (post is null) return Result.Failure(NewsErrors.NotFound); diff --git a/backend/src/PnvPanel.Application/Admin/Nodes/DeleteNodeCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Nodes/DeleteNodeCommandHandler.cs index 7a3a8a8..c6747dd 100644 --- a/backend/src/PnvPanel.Application/Admin/Nodes/DeleteNodeCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Nodes/DeleteNodeCommandHandler.cs @@ -6,22 +6,38 @@ using PnvPanel.Domain.Audit; namespace PnvPanel.Application.Admin.Nodes; -public sealed class DeleteNodeCommandHandler(IAppDbContext dbContext, IXuiPanelGateway gateway, ICurrentUser currentUser) - : ICommandHandler +public sealed class DeleteNodeCommandHandler( + IAppDbContext dbContext, + IXuiPanelGateway gateway, + ICurrentUser currentUser +) : ICommandHandler { public async Task Handle(DeleteNodeCommand command, CancellationToken cancellationToken) { - var node = await dbContext.Nodes.FirstOrDefaultAsync(n => n.Id == command.NodeId, cancellationToken); + var node = await dbContext.Nodes.FirstOrDefaultAsync( + n => n.Id == command.NodeId, + cancellationToken + ); if (node is null) return Result.Failure(NodeErrors.NotFound); - var inbounds = await dbContext.Inbounds.Where(i => i.NodeId == node.Id).ToListAsync(cancellationToken); + var inbounds = await dbContext + .Inbounds.Where(i => i.NodeId == node.Id) + .ToListAsync(cancellationToken); dbContext.Inbounds.RemoveRange(inbounds); dbContext.Nodes.Remove(node); gateway.InvalidateClient(node.Id); - dbContext.AuditLogs.Add(AuditLog.Create( - currentUser.UserId, "NodeDeleted", "Node", node.Id.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + currentUser.UserId, + "NodeDeleted", + "Node", + node.Id.ToString(), + metadata: null, + AuditSource.Web + ) + ); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Admin/Nodes/ListNodesQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Nodes/ListNodesQueryHandler.cs index 5491a4a..7b866ff 100644 --- a/backend/src/PnvPanel.Application/Admin/Nodes/ListNodesQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Nodes/ListNodesQueryHandler.cs @@ -5,11 +5,18 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Nodes; -public sealed class ListNodesQueryHandler(IAppDbContext dbContext) : IQueryHandler>> +public sealed class ListNodesQueryHandler(IAppDbContext dbContext) + : IQueryHandler>> { - public async Task>> Handle(ListNodesQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListNodesQuery query, + CancellationToken cancellationToken + ) { - var nodes = await dbContext.Nodes.AsNoTracking().OrderBy(n => n.Name).ToListAsync(cancellationToken); + var nodes = await dbContext + .Nodes.AsNoTracking() + .OrderBy(n => n.Name) + .ToListAsync(cancellationToken); return Result.Success>(nodes.Select(NodeDto.FromDomain).ToList()); } } diff --git a/backend/src/PnvPanel.Application/Admin/Nodes/NodeDto.cs b/backend/src/PnvPanel.Application/Admin/Nodes/NodeDto.cs index 923964b..e4aa487 100644 --- a/backend/src/PnvPanel.Application/Admin/Nodes/NodeDto.cs +++ b/backend/src/PnvPanel.Application/Admin/Nodes/NodeDto.cs @@ -4,10 +4,25 @@ namespace PnvPanel.Application.Admin.Nodes; /// Админский DTO ноды. Пароль никогда не попадает в ответ API. public sealed record NodeDto( - Guid Id, string Name, string BaseAddress, string Username, string? Location, - NodeStatus Status, bool IsEnabled, DateTimeOffset? LastSyncAt) + Guid Id, + string Name, + string BaseAddress, + string Username, + string? Location, + NodeStatus Status, + bool IsEnabled, + DateTimeOffset? LastSyncAt +) { - public static NodeDto FromDomain(Node node) => new( - node.Id, node.Name, node.BaseAddress.ToString(), node.Credentials.Username, node.Location, - node.Status, node.IsEnabled, node.LastSyncAt); + public static NodeDto FromDomain(Node node) => + new( + node.Id, + node.Name, + node.BaseAddress.ToString(), + node.Credentials.Username, + node.Location, + node.Status, + node.IsEnabled, + node.LastSyncAt + ); } diff --git a/backend/src/PnvPanel.Application/Admin/Nodes/NodeErrors.cs b/backend/src/PnvPanel.Application/Admin/Nodes/NodeErrors.cs index d76fe9c..f0ee50e 100644 --- a/backend/src/PnvPanel.Application/Admin/Nodes/NodeErrors.cs +++ b/backend/src/PnvPanel.Application/Admin/Nodes/NodeErrors.cs @@ -5,5 +5,8 @@ namespace PnvPanel.Application.Admin.Nodes; public static class NodeErrors { public static readonly Error NotFound = Error.NotFound("Nodes.NotFound", "Нода не найдена."); - public static readonly Error InvalidBaseAddress = Error.Validation("Nodes.InvalidBaseAddress", "Некорректный адрес панели."); + public static readonly Error InvalidBaseAddress = Error.Validation( + "Nodes.InvalidBaseAddress", + "Некорректный адрес панели." + ); } diff --git a/backend/src/PnvPanel.Application/Admin/Nodes/ProbeNodeCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Nodes/ProbeNodeCommandHandler.cs index a2ebb69..cc17598 100644 --- a/backend/src/PnvPanel.Application/Admin/Nodes/ProbeNodeCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Nodes/ProbeNodeCommandHandler.cs @@ -9,15 +9,23 @@ namespace PnvPanel.Application.Admin.Nodes; public sealed class ProbeNodeCommandHandler(IAppDbContext dbContext, IXuiPanelGateway gateway) : ICommandHandler> { - public async Task> Handle(ProbeNodeCommand command, CancellationToken cancellationToken) + public async Task> Handle( + ProbeNodeCommand command, + CancellationToken cancellationToken + ) { - var node = await dbContext.Nodes.FirstOrDefaultAsync(n => n.Id == command.NodeId, cancellationToken); + var node = await dbContext.Nodes.FirstOrDefaultAsync( + n => n.Id == command.NodeId, + cancellationToken + ); if (node is null) return Result.Failure(NodeErrors.NotFound); var probe = await gateway.ProbeAsync(node, cancellationToken); node.UpdateStatus(probe.IsReachable ? NodeStatus.Online : NodeStatus.Offline); - return Result.Success(new NodeProbeResultDto(probe.IsReachable, probe.ErrorMessage, node.Status)); + return Result.Success( + new NodeProbeResultDto(probe.IsReachable, probe.ErrorMessage, node.Status) + ); } } diff --git a/backend/src/PnvPanel.Application/Admin/Nodes/RegisterNodeCommand.cs b/backend/src/PnvPanel.Application/Admin/Nodes/RegisterNodeCommand.cs index 900e957..a00b532 100644 --- a/backend/src/PnvPanel.Application/Admin/Nodes/RegisterNodeCommand.cs +++ b/backend/src/PnvPanel.Application/Admin/Nodes/RegisterNodeCommand.cs @@ -3,5 +3,10 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Nodes; -public sealed record RegisterNodeCommand(string Name, string BaseAddress, string Username, string Password, string? Location) - : ICommand>; +public sealed record RegisterNodeCommand( + string Name, + string BaseAddress, + string Username, + string Password, + string? Location +) : ICommand>; diff --git a/backend/src/PnvPanel.Application/Admin/Nodes/RegisterNodeCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Nodes/RegisterNodeCommandHandler.cs index 20fff70..fd163a8 100644 --- a/backend/src/PnvPanel.Application/Admin/Nodes/RegisterNodeCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Nodes/RegisterNodeCommandHandler.cs @@ -7,10 +7,16 @@ using PnvPanel.Domain.Nodes; namespace PnvPanel.Application.Admin.Nodes; public sealed class RegisterNodeCommandHandler( - IAppDbContext dbContext, IXuiPanelGateway gateway, ISecretProtector secretProtector, ICurrentUser currentUser) - : ICommandHandler> + IAppDbContext dbContext, + IXuiPanelGateway gateway, + ISecretProtector secretProtector, + ICurrentUser currentUser +) : ICommandHandler> { - public Task> Handle(RegisterNodeCommand command, CancellationToken cancellationToken) + public Task> Handle( + RegisterNodeCommand command, + CancellationToken cancellationToken + ) { if (!Uri.TryCreate(command.BaseAddress, UriKind.Absolute, out var baseAddress)) return Task.FromResult(Result.Failure(NodeErrors.InvalidBaseAddress)); @@ -19,12 +25,23 @@ public sealed class RegisterNodeCommandHandler( if (!validation.IsSuccess) return Task.FromResult(Result.Failure(validation.Error)); - var credentials = new NodeCredentials(command.Username, secretProtector.Protect(command.Password)); + var credentials = new NodeCredentials( + command.Username, + secretProtector.Protect(command.Password) + ); var node = Node.Register(command.Name, baseAddress, credentials, command.Location); dbContext.Nodes.Add(node); - dbContext.AuditLogs.Add(AuditLog.Create( - currentUser.UserId, "NodeRegistered", "Node", node.Id.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + currentUser.UserId, + "NodeRegistered", + "Node", + node.Id.ToString(), + metadata: null, + AuditSource.Web + ) + ); return Task.FromResult(Result.Success(NodeDto.FromDomain(node))); } diff --git a/backend/src/PnvPanel.Application/Admin/Nodes/SyncNodeCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Nodes/SyncNodeCommandHandler.cs index 32f0897..52b62e4 100644 --- a/backend/src/PnvPanel.Application/Admin/Nodes/SyncNodeCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Nodes/SyncNodeCommandHandler.cs @@ -10,9 +10,15 @@ namespace PnvPanel.Application.Admin.Nodes; public sealed class SyncNodeCommandHandler(IAppDbContext dbContext, IXuiPanelGateway gateway) : ICommandHandler> { - public async Task> Handle(SyncNodeCommand command, CancellationToken cancellationToken) + public async Task> Handle( + SyncNodeCommand command, + CancellationToken cancellationToken + ) { - var node = await dbContext.Nodes.FirstOrDefaultAsync(n => n.Id == command.NodeId, cancellationToken); + var node = await dbContext.Nodes.FirstOrDefaultAsync( + n => n.Id == command.NodeId, + cancellationToken + ); if (node is null) return Result.Failure(NodeErrors.NotFound); @@ -23,7 +29,9 @@ public sealed class SyncNodeCommandHandler(IAppDbContext dbContext, IXuiPanelGat return Result.Failure(remoteResult.Error); } - var existing = await dbContext.Inbounds.Where(i => i.NodeId == node.Id).ToListAsync(cancellationToken); + var existing = await dbContext + .Inbounds.Where(i => i.NodeId == node.Id) + .ToListAsync(cancellationToken); var existingByRemoteId = existing.ToDictionary(i => i.RemoteInboundId); foreach (var remote in remoteResult.Value) @@ -31,13 +39,25 @@ public sealed class SyncNodeCommandHandler(IAppDbContext dbContext, IXuiPanelGat if (existingByRemoteId.TryGetValue(remote.RemoteInboundId, out var inbound)) inbound.UpdateFromRemote(remote.Protocol, remote.Remark, remote.Port); else - dbContext.Inbounds.Add(Inbound.FromRemote(node.Id, remote.RemoteInboundId, remote.Protocol, remote.Remark, remote.Port)); + dbContext.Inbounds.Add( + Inbound.FromRemote( + node.Id, + remote.RemoteInboundId, + remote.Protocol, + remote.Remark, + remote.Port + ) + ); } // Inbound, пропавший на панели, снимаем с публикации (не удаляем — реконсиляция дрейфа, // см. architecture.md); новые конфиги на нём создать будет нельзя, старые не трогаем. var remoteIds = remoteResult.Value.Select(r => r.RemoteInboundId).ToHashSet(); - foreach (var stale in existing.Where(i => i.IsPublished && !remoteIds.Contains(i.RemoteInboundId))) + foreach ( + var stale in existing.Where(i => + i.IsPublished && !remoteIds.Contains(i.RemoteInboundId) + ) + ) stale.Unpublish(); node.UpdateStatus(NodeStatus.Online); diff --git a/backend/src/PnvPanel.Application/Admin/Nodes/UpdateNodeCommand.cs b/backend/src/PnvPanel.Application/Admin/Nodes/UpdateNodeCommand.cs index f67c93f..e86404c 100644 --- a/backend/src/PnvPanel.Application/Admin/Nodes/UpdateNodeCommand.cs +++ b/backend/src/PnvPanel.Application/Admin/Nodes/UpdateNodeCommand.cs @@ -4,5 +4,10 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Nodes; public sealed record UpdateNodeCommand( - Guid NodeId, string Name, string? Location, bool IsEnabled, string? Username, string? Password) - : ICommand>; + Guid NodeId, + string Name, + string? Location, + bool IsEnabled, + string? Username, + string? Password +) : ICommand>; diff --git a/backend/src/PnvPanel.Application/Admin/Nodes/UpdateNodeCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Nodes/UpdateNodeCommandHandler.cs index 638de8e..8562431 100644 --- a/backend/src/PnvPanel.Application/Admin/Nodes/UpdateNodeCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Nodes/UpdateNodeCommandHandler.cs @@ -8,12 +8,21 @@ using PnvPanel.Domain.Nodes; namespace PnvPanel.Application.Admin.Nodes; public sealed class UpdateNodeCommandHandler( - IAppDbContext dbContext, IXuiPanelGateway gateway, ISecretProtector secretProtector, ICurrentUser currentUser) - : ICommandHandler> + IAppDbContext dbContext, + IXuiPanelGateway gateway, + ISecretProtector secretProtector, + ICurrentUser currentUser +) : ICommandHandler> { - public async Task> Handle(UpdateNodeCommand command, CancellationToken cancellationToken) + public async Task> Handle( + UpdateNodeCommand command, + CancellationToken cancellationToken + ) { - var node = await dbContext.Nodes.FirstOrDefaultAsync(n => n.Id == command.NodeId, cancellationToken); + var node = await dbContext.Nodes.FirstOrDefaultAsync( + n => n.Id == command.NodeId, + cancellationToken + ); if (node is null) return Result.Failure(NodeErrors.NotFound); @@ -24,14 +33,27 @@ public sealed class UpdateNodeCommandHandler( else node.Disable(); - if (!string.IsNullOrWhiteSpace(command.Username) && !string.IsNullOrWhiteSpace(command.Password)) + if ( + !string.IsNullOrWhiteSpace(command.Username) + && !string.IsNullOrWhiteSpace(command.Password) + ) { - node.UpdateCredentials(new NodeCredentials(command.Username, secretProtector.Protect(command.Password))); + node.UpdateCredentials( + new NodeCredentials(command.Username, secretProtector.Protect(command.Password)) + ); gateway.InvalidateClient(node.Id); } - dbContext.AuditLogs.Add(AuditLog.Create( - currentUser.UserId, "NodeUpdated", "Node", node.Id.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + currentUser.UserId, + "NodeUpdated", + "Node", + node.Id.ToString(), + metadata: null, + AuditSource.Web + ) + ); return Result.Success(NodeDto.FromDomain(node)); } diff --git a/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommand.cs b/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommand.cs index 4e2030f..3fec0d8 100644 --- a/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommand.cs +++ b/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommand.cs @@ -4,4 +4,5 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Roles; -public sealed record CreateRoleCommand(string Name, int MaxConfigs, int MaxIpLimit) : ICommand>; +public sealed record CreateRoleCommand(string Name, int MaxConfigs, int MaxIpLimit) + : ICommand>; diff --git a/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommandHandler.cs index 2972f00..8d6efff 100644 --- a/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommandHandler.cs @@ -4,8 +4,17 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Roles; -public sealed class CreateRoleCommandHandler(IRoleService roleService) : ICommandHandler> +public sealed class CreateRoleCommandHandler(IRoleService roleService) + : ICommandHandler> { - public Task> Handle(CreateRoleCommand command, CancellationToken cancellationToken) - => roleService.CreateRoleAsync(command.Name, command.MaxConfigs, command.MaxIpLimit, cancellationToken); + public Task> Handle( + CreateRoleCommand command, + CancellationToken cancellationToken + ) => + roleService.CreateRoleAsync( + command.Name, + command.MaxConfigs, + command.MaxIpLimit, + cancellationToken + ); } diff --git a/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommandValidator.cs b/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommandValidator.cs index 4e3e4cf..5c20099 100644 --- a/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommandValidator.cs +++ b/backend/src/PnvPanel.Application/Admin/Roles/CreateRoleCommandValidator.cs @@ -6,10 +6,7 @@ public sealed class CreateRoleCommandValidator : AbstractValidator x.Name) - .NotEmpty() - .Length(2, 32) - .Matches("^[a-zA-Z0-9_-]+$"); + RuleFor(x => x.Name).NotEmpty().Length(2, 32).Matches("^[a-zA-Z0-9_-]+$"); RuleFor(x => x.MaxConfigs).GreaterThanOrEqualTo(-1); RuleFor(x => x.MaxIpLimit).GreaterThanOrEqualTo(-1); diff --git a/backend/src/PnvPanel.Application/Admin/Roles/DeleteRoleCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Roles/DeleteRoleCommandHandler.cs index bb76045..9b999ed 100644 --- a/backend/src/PnvPanel.Application/Admin/Roles/DeleteRoleCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Roles/DeleteRoleCommandHandler.cs @@ -4,8 +4,9 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Roles; -public sealed class DeleteRoleCommandHandler(IRoleService roleService) : ICommandHandler +public sealed class DeleteRoleCommandHandler(IRoleService roleService) + : ICommandHandler { - public Task Handle(DeleteRoleCommand command, CancellationToken cancellationToken) - => roleService.DeleteRoleAsync(command.RoleId, cancellationToken); + public Task Handle(DeleteRoleCommand command, CancellationToken cancellationToken) => + roleService.DeleteRoleAsync(command.RoleId, cancellationToken); } diff --git a/backend/src/PnvPanel.Application/Admin/Roles/ListRolesQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Roles/ListRolesQueryHandler.cs index 6634db8..b33d3c7 100644 --- a/backend/src/PnvPanel.Application/Admin/Roles/ListRolesQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Roles/ListRolesQueryHandler.cs @@ -7,6 +7,8 @@ namespace PnvPanel.Application.Admin.Roles; public sealed class ListRolesQueryHandler(IRoleService roleService) : IQueryHandler>> { - public async Task>> Handle(ListRolesQuery query, CancellationToken cancellationToken) - => Result.Success(await roleService.ListRolesAsync(cancellationToken)); + public async Task>> Handle( + ListRolesQuery query, + CancellationToken cancellationToken + ) => Result.Success(await roleService.ListRolesAsync(cancellationToken)); } diff --git a/backend/src/PnvPanel.Application/Admin/Roles/RoleErrors.cs b/backend/src/PnvPanel.Application/Admin/Roles/RoleErrors.cs index 55d12f6..1e2db51 100644 --- a/backend/src/PnvPanel.Application/Admin/Roles/RoleErrors.cs +++ b/backend/src/PnvPanel.Application/Admin/Roles/RoleErrors.cs @@ -5,7 +5,16 @@ namespace PnvPanel.Application.Admin.Roles; public static class RoleErrors { public static readonly Error NotFound = Error.NotFound("Roles.NotFound", "Роль не найдена."); - public static readonly Error DuplicateName = Error.Conflict("Roles.DuplicateName", "Роль с таким именем уже существует."); - public static readonly Error CannotModifySystemRole = Error.Forbidden("Roles.CannotModifySystemRole", "Системную роль нельзя удалить."); - public static readonly Error RoleInUse = Error.Conflict("Roles.RoleInUse", "Роль назначена пользователям — сначала переназначьте их."); + public static readonly Error DuplicateName = Error.Conflict( + "Roles.DuplicateName", + "Роль с таким именем уже существует." + ); + public static readonly Error CannotModifySystemRole = Error.Forbidden( + "Roles.CannotModifySystemRole", + "Системную роль нельзя удалить." + ); + public static readonly Error RoleInUse = Error.Conflict( + "Roles.RoleInUse", + "Роль назначена пользователям — сначала переназначьте их." + ); } diff --git a/backend/src/PnvPanel.Application/Admin/Roles/UpdateRoleCommand.cs b/backend/src/PnvPanel.Application/Admin/Roles/UpdateRoleCommand.cs index ffdfe80..de07ab2 100644 --- a/backend/src/PnvPanel.Application/Admin/Roles/UpdateRoleCommand.cs +++ b/backend/src/PnvPanel.Application/Admin/Roles/UpdateRoleCommand.cs @@ -4,4 +4,5 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Roles; -public sealed record UpdateRoleCommand(Guid RoleId, int MaxConfigs, int MaxIpLimit) : ICommand>; +public sealed record UpdateRoleCommand(Guid RoleId, int MaxConfigs, int MaxIpLimit) + : ICommand>; diff --git a/backend/src/PnvPanel.Application/Admin/Roles/UpdateRoleCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Roles/UpdateRoleCommandHandler.cs index 7c0ae82..a2dc00b 100644 --- a/backend/src/PnvPanel.Application/Admin/Roles/UpdateRoleCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Roles/UpdateRoleCommandHandler.cs @@ -4,8 +4,17 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Roles; -public sealed class UpdateRoleCommandHandler(IRoleService roleService) : ICommandHandler> +public sealed class UpdateRoleCommandHandler(IRoleService roleService) + : ICommandHandler> { - public Task> Handle(UpdateRoleCommand command, CancellationToken cancellationToken) - => roleService.UpdateRoleAsync(command.RoleId, command.MaxConfigs, command.MaxIpLimit, cancellationToken); + public Task> Handle( + UpdateRoleCommand command, + CancellationToken cancellationToken + ) => + roleService.UpdateRoleAsync( + command.RoleId, + command.MaxConfigs, + command.MaxIpLimit, + cancellationToken + ); } diff --git a/backend/src/PnvPanel.Application/Admin/Stats/GetStatsQuery.cs b/backend/src/PnvPanel.Application/Admin/Stats/GetStatsQuery.cs index 3cd6b5e..a20e4f8 100644 --- a/backend/src/PnvPanel.Application/Admin/Stats/GetStatsQuery.cs +++ b/backend/src/PnvPanel.Application/Admin/Stats/GetStatsQuery.cs @@ -6,6 +6,13 @@ namespace PnvPanel.Application.Admin.Stats; public sealed record GetStatsQuery : IQuery>; public sealed record StatsDto( - int TotalUsers, int ActivatedUsers, int PendingActivationRequests, - int TotalNodes, int OnlineNodes, int TotalConfigs, int ActiveConfigs, - long TotalUsedUpBytes, long TotalUsedDownBytes); + int TotalUsers, + int ActivatedUsers, + int PendingActivationRequests, + int TotalNodes, + int OnlineNodes, + int TotalConfigs, + int ActiveConfigs, + long TotalUsedUpBytes, + long TotalUsedDownBytes +); diff --git a/backend/src/PnvPanel.Application/Admin/Stats/GetStatsQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Stats/GetStatsQueryHandler.cs index e881f30..758bb85 100644 --- a/backend/src/PnvPanel.Application/Admin/Stats/GetStatsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Stats/GetStatsQueryHandler.cs @@ -11,27 +11,47 @@ namespace PnvPanel.Application.Admin.Stats; public sealed class GetStatsQueryHandler(IAppDbContext dbContext, IIdentityService identityService) : IQueryHandler> { - public async Task> Handle(GetStatsQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetStatsQuery query, + CancellationToken cancellationToken + ) { var userStats = await identityService.GetUserStatsAsync(cancellationToken); - var pendingActivations = await dbContext.ActivationRequests - .CountAsync(r => r.Status == ActivationStatus.Pending, cancellationToken); + var pendingActivations = await dbContext.ActivationRequests.CountAsync( + r => r.Status == ActivationStatus.Pending, + cancellationToken + ); var totalNodes = await dbContext.Nodes.CountAsync(cancellationToken); - var onlineNodes = await dbContext.Nodes.CountAsync(n => n.Status == NodeStatus.Online, cancellationToken); + var onlineNodes = await dbContext.Nodes.CountAsync( + n => n.Status == NodeStatus.Online, + cancellationToken + ); var totalConfigs = await dbContext.VpnConfigs.CountAsync(cancellationToken); - var activeConfigs = await dbContext.VpnConfigs.CountAsync(c => c.Status == ConfigStatus.Active, cancellationToken); + var activeConfigs = await dbContext.VpnConfigs.CountAsync( + c => c.Status == ConfigStatus.Active, + cancellationToken + ); - var trafficTotals = await dbContext.VpnConfigs - .GroupBy(_ => 1) + var trafficTotals = await dbContext + .VpnConfigs.GroupBy(_ => 1) .Select(g => new { Up = g.Sum(c => c.UsedUpBytes), Down = g.Sum(c => c.UsedDownBytes) }) .FirstOrDefaultAsync(cancellationToken); - return Result.Success(new StatsDto( - userStats.Total, userStats.Activated, pendingActivations, - totalNodes, onlineNodes, totalConfigs, activeConfigs, - trafficTotals?.Up ?? 0, trafficTotals?.Down ?? 0)); + return Result.Success( + new StatsDto( + userStats.Total, + userStats.Activated, + pendingActivations, + totalNodes, + onlineNodes, + totalConfigs, + activeConfigs, + trafficTotals?.Up ?? 0, + trafficTotals?.Down ?? 0 + ) + ); } } diff --git a/backend/src/PnvPanel.Application/Admin/Support/ApproveRoleRequestCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Support/ApproveRoleRequestCommandHandler.cs index e410156..c5873e8 100644 --- a/backend/src/PnvPanel.Application/Admin/Support/ApproveRoleRequestCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Support/ApproveRoleRequestCommandHandler.cs @@ -10,16 +10,25 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Admin.Support; public sealed class ApproveRoleRequestCommandHandler( - IAppDbContext dbContext, IRoleService roleService, IRealtimeNotifier notifier, - ITelegramNotifier telegramNotifier, ICurrentUser currentUser) - : ICommandHandler + IAppDbContext dbContext, + IRoleService roleService, + IRealtimeNotifier notifier, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(ApproveRoleRequestCommand command, CancellationToken cancellationToken) + public async Task Handle( + ApproveRoleRequestCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } adminId) return Result.Failure(AuthErrors.Unauthorized); - var ticket = await dbContext.SupportTickets.FirstOrDefaultAsync(t => t.Id == command.TicketId, cancellationToken); + var ticket = await dbContext.SupportTickets.FirstOrDefaultAsync( + t => t.Id == command.TicketId, + cancellationToken + ); if (ticket is null) return Result.Failure(SupportErrors.NotFound); @@ -37,24 +46,44 @@ public sealed class ApproveRoleRequestCommandHandler( else { var createResult = await roleService.CreateRoleAsync( - ticket.ProposedRoleName!, ticket.ProposedMaxConfigs!.Value, ticket.ProposedMaxIpLimit!.Value, cancellationToken); + ticket.ProposedRoleName!, + ticket.ProposedMaxConfigs!.Value, + ticket.ProposedMaxIpLimit!.Value, + cancellationToken + ); if (!createResult.IsSuccess) return Result.Failure(createResult.Error); roleId = createResult.Value.Id; } - var assignResult = await roleService.ChangeUserRoleAsync(ticket.UserId, roleId, cancellationToken); + var assignResult = await roleService.ChangeUserRoleAsync( + ticket.UserId, + roleId, + cancellationToken + ); if (!assignResult.IsSuccess) return assignResult; ticket.Resolve(); - dbContext.AuditLogs.Add(AuditLog.Create( - adminId, "RoleRequestApproved", "SupportTicket", ticket.Id.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + adminId, + "RoleRequestApproved", + "SupportTicket", + ticket.Id.ToString(), + metadata: null, + AuditSource.Web + ) + ); await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken); - await telegramNotifier.NotifyUserAsync(ticket.UserId, "✅ Ваша заявка на роль одобрена.", cancellationToken); + await telegramNotifier.NotifyUserAsync( + ticket.UserId, + "✅ Ваша заявка на роль одобрена.", + cancellationToken + ); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Admin/Support/CloseTicketCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Support/CloseTicketCommandHandler.cs index 1cede90..2b34af0 100644 --- a/backend/src/PnvPanel.Application/Admin/Support/CloseTicketCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Support/CloseTicketCommandHandler.cs @@ -10,15 +10,24 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Admin.Support; public sealed class CloseTicketCommandHandler( - IAppDbContext dbContext, IRealtimeNotifier notifier, ITelegramNotifier telegramNotifier, ICurrentUser currentUser) - : ICommandHandler + IAppDbContext dbContext, + IRealtimeNotifier notifier, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(CloseTicketCommand command, CancellationToken cancellationToken) + public async Task Handle( + CloseTicketCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } adminId) return Result.Failure(AuthErrors.Unauthorized); - var ticket = await dbContext.SupportTickets.FirstOrDefaultAsync(t => t.Id == command.TicketId, cancellationToken); + var ticket = await dbContext.SupportTickets.FirstOrDefaultAsync( + t => t.Id == command.TicketId, + cancellationToken + ); if (ticket is null) return Result.Failure(SupportErrors.NotFound); @@ -27,11 +36,23 @@ public sealed class CloseTicketCommandHandler( ticket.Close(); - dbContext.AuditLogs.Add(AuditLog.Create( - adminId, "TicketClosed", "SupportTicket", ticket.Id.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + adminId, + "TicketClosed", + "SupportTicket", + ticket.Id.ToString(), + metadata: null, + AuditSource.Web + ) + ); await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken); - await telegramNotifier.NotifyUserAsync(ticket.UserId, "🔒 Ваше обращение закрыто.", cancellationToken); + await telegramNotifier.NotifyUserAsync( + ticket.UserId, + "🔒 Ваше обращение закрыто.", + cancellationToken + ); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Admin/Support/GetTicketAdminQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Support/GetTicketAdminQueryHandler.cs index 6877212..0340d26 100644 --- a/backend/src/PnvPanel.Application/Admin/Support/GetTicketAdminQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Support/GetTicketAdminQueryHandler.cs @@ -6,17 +6,30 @@ using PnvPanel.Application.Support; namespace PnvPanel.Application.Admin.Support; -public sealed class GetTicketAdminQueryHandler(IAppDbContext dbContext, IIdentityService identityService, IRoleService roleService) - : IQueryHandler> +public sealed class GetTicketAdminQueryHandler( + IAppDbContext dbContext, + IIdentityService identityService, + IRoleService roleService +) : IQueryHandler> { - public async Task> Handle(GetTicketAdminQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetTicketAdminQuery query, + CancellationToken cancellationToken + ) { - var ticket = await dbContext.SupportTickets.AsNoTracking() + var ticket = await dbContext + .SupportTickets.AsNoTracking() .FirstOrDefaultAsync(t => t.Id == query.TicketId, cancellationToken); if (ticket is null) return Result.Failure(SupportErrors.NotFound); - var dto = await TicketMapping.ToDetailDtoAsync(dbContext, identityService, roleService, ticket, cancellationToken); + var dto = await TicketMapping.ToDetailDtoAsync( + dbContext, + identityService, + roleService, + ticket, + cancellationToken + ); return Result.Success(dto); } } diff --git a/backend/src/PnvPanel.Application/Admin/Support/ListAllTicketsQuery.cs b/backend/src/PnvPanel.Application/Admin/Support/ListAllTicketsQuery.cs index b0cd9be..f673bdf 100644 --- a/backend/src/PnvPanel.Application/Admin/Support/ListAllTicketsQuery.cs +++ b/backend/src/PnvPanel.Application/Admin/Support/ListAllTicketsQuery.cs @@ -5,5 +5,9 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Admin.Support; -public sealed record ListAllTicketsQuery(TicketType? TypeFilter, TicketStatus? StatusFilter, int Page, int PageSize) - : IQuery>>; +public sealed record ListAllTicketsQuery( + TicketType? TypeFilter, + TicketStatus? StatusFilter, + int Page, + int PageSize +) : IQuery>>; diff --git a/backend/src/PnvPanel.Application/Admin/Support/ListAllTicketsQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Support/ListAllTicketsQueryHandler.cs index b0a9a38..b47e29c 100644 --- a/backend/src/PnvPanel.Application/Admin/Support/ListAllTicketsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Support/ListAllTicketsQueryHandler.cs @@ -6,10 +6,15 @@ using PnvPanel.Application.Support; namespace PnvPanel.Application.Admin.Support; -public sealed class ListAllTicketsQueryHandler(IAppDbContext dbContext, IIdentityService identityService) - : IQueryHandler>> +public sealed class ListAllTicketsQueryHandler( + IAppDbContext dbContext, + IIdentityService identityService +) : IQueryHandler>> { - public async Task>> Handle(ListAllTicketsQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListAllTicketsQuery query, + CancellationToken cancellationToken + ) { var page = query.Page <= 0 ? 1 : query.Page; var pageSize = query.PageSize is <= 0 or > 100 ? 20 : query.PageSize; @@ -20,9 +25,18 @@ public sealed class ListAllTicketsQueryHandler(IAppDbContext dbContext, IIdentit if (query.StatusFilter is { } status) ticketsQuery = ticketsQuery.Where(t => t.Status == status); - var page1 = await ticketsQuery.OrderByDescending(t => t.CreatedAt).ToPagedListAsync(page, pageSize, cancellationToken); - var items = await TicketMapping.ToSummaryDtosAsync(dbContext, identityService, page1.Items, cancellationToken); + var page1 = await ticketsQuery + .OrderByDescending(t => t.CreatedAt) + .ToPagedListAsync(page, pageSize, cancellationToken); + var items = await TicketMapping.ToSummaryDtosAsync( + dbContext, + identityService, + page1.Items, + cancellationToken + ); - return Result.Success(new PagedList(items, page1.Total, page1.Page, page1.PageSize)); + return Result.Success( + new PagedList(items, page1.Total, page1.Page, page1.PageSize) + ); } } diff --git a/backend/src/PnvPanel.Application/Admin/Support/RejectRoleRequestCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Support/RejectRoleRequestCommandHandler.cs index 984acaa..173a09b 100644 --- a/backend/src/PnvPanel.Application/Admin/Support/RejectRoleRequestCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Support/RejectRoleRequestCommandHandler.cs @@ -10,15 +10,24 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Admin.Support; public sealed class RejectRoleRequestCommandHandler( - IAppDbContext dbContext, IRealtimeNotifier notifier, ITelegramNotifier telegramNotifier, ICurrentUser currentUser) - : ICommandHandler + IAppDbContext dbContext, + IRealtimeNotifier notifier, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(RejectRoleRequestCommand command, CancellationToken cancellationToken) + public async Task Handle( + RejectRoleRequestCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } adminId) return Result.Failure(AuthErrors.Unauthorized); - var ticket = await dbContext.SupportTickets.FirstOrDefaultAsync(t => t.Id == command.TicketId, cancellationToken); + var ticket = await dbContext.SupportTickets.FirstOrDefaultAsync( + t => t.Id == command.TicketId, + cancellationToken + ); if (ticket is null) return Result.Failure(SupportErrors.NotFound); @@ -33,11 +42,23 @@ public sealed class RejectRoleRequestCommandHandler( ticket.Close(); - dbContext.AuditLogs.Add(AuditLog.Create( - adminId, "RoleRequestRejected", "SupportTicket", ticket.Id.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + adminId, + "RoleRequestRejected", + "SupportTicket", + ticket.Id.ToString(), + metadata: null, + AuditSource.Web + ) + ); await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken); - await telegramNotifier.NotifyUserAsync(ticket.UserId, "❌ Ваша заявка на роль отклонена.", cancellationToken); + await telegramNotifier.NotifyUserAsync( + ticket.UserId, + "❌ Ваша заявка на роль отклонена.", + cancellationToken + ); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Admin/Support/ResolveTicketCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Support/ResolveTicketCommandHandler.cs index e06f765..be899c0 100644 --- a/backend/src/PnvPanel.Application/Admin/Support/ResolveTicketCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Support/ResolveTicketCommandHandler.cs @@ -10,15 +10,24 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Admin.Support; public sealed class ResolveTicketCommandHandler( - IAppDbContext dbContext, IRealtimeNotifier notifier, ITelegramNotifier telegramNotifier, ICurrentUser currentUser) - : ICommandHandler + IAppDbContext dbContext, + IRealtimeNotifier notifier, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(ResolveTicketCommand command, CancellationToken cancellationToken) + public async Task Handle( + ResolveTicketCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } adminId) return Result.Failure(AuthErrors.Unauthorized); - var ticket = await dbContext.SupportTickets.FirstOrDefaultAsync(t => t.Id == command.TicketId, cancellationToken); + var ticket = await dbContext.SupportTickets.FirstOrDefaultAsync( + t => t.Id == command.TicketId, + cancellationToken + ); if (ticket is null) return Result.Failure(SupportErrors.NotFound); @@ -27,11 +36,23 @@ public sealed class ResolveTicketCommandHandler( ticket.Resolve(); - dbContext.AuditLogs.Add(AuditLog.Create( - adminId, "TicketResolved", "SupportTicket", ticket.Id.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + adminId, + "TicketResolved", + "SupportTicket", + ticket.Id.ToString(), + metadata: null, + AuditSource.Web + ) + ); await notifier.NotifyTicketUpdatedAsync(ticket.Id, ticket.UserId, cancellationToken); - await telegramNotifier.NotifyUserAsync(ticket.UserId, "✅ Ваше обращение решено.", cancellationToken); + await telegramNotifier.NotifyUserAsync( + ticket.UserId, + "✅ Ваше обращение решено.", + cancellationToken + ); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Admin/Users/BlockUserCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Users/BlockUserCommandHandler.cs index 18223ca..893cc25 100644 --- a/backend/src/PnvPanel.Application/Admin/Users/BlockUserCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Users/BlockUserCommandHandler.cs @@ -10,10 +10,14 @@ namespace PnvPanel.Application.Admin.Users; /// Блокировка гасит все активные конфиги в 3x-ui (см. architecture.md). public sealed class BlockUserCommandHandler( - IAppDbContext dbContext, IIdentityService identityService, IXuiPanelGateway gateway, - IRealtimeNotifier notifier, ITelegramNotifier telegramNotifier, ICurrentUser currentUser, - ILogger logger) - : ICommandHandler + IAppDbContext dbContext, + IIdentityService identityService, + IXuiPanelGateway gateway, + IRealtimeNotifier notifier, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser, + ILogger logger +) : ICommandHandler { public async Task Handle(BlockUserCommand command, CancellationToken cancellationToken) { @@ -21,22 +25,32 @@ public sealed class BlockUserCommandHandler( if (!blockResult.IsSuccess) return blockResult; - var configs = await dbContext.VpnConfigs - .Where(c => c.UserId == command.UserId && c.Status == ConfigStatus.Active) + var configs = await dbContext + .VpnConfigs.Where(c => c.UserId == command.UserId && c.Status == ConfigStatus.Active) .ToListAsync(cancellationToken); foreach (var config in configs) { - 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) { var updateResult = await gateway.UpdateClientAsync( - node, inbound.RemoteInboundId, config.ClientExternalId, config.Protocol, - config.Label ?? config.ClientEmail, enable: false, cancellationToken); + node, + inbound.RemoteInboundId, + config.ClientExternalId, + config.Protocol, + config.Label ?? config.ClientEmail, + enable: false, + cancellationToken + ); if (!updateResult.IsSuccess) { @@ -45,19 +59,40 @@ public sealed class BlockUserCommandHandler( // Конфиг останется Active и будет подхвачен повторным BlockUserCommand (идемпотентен). logger.LogWarning( "Failed to disable client for config {ConfigId} on node {NodeId} while blocking user {UserId}: {Error}", - config.Id, node.Id, command.UserId, updateResult.Error); + config.Id, + node.Id, + command.UserId, + updateResult.Error + ); continue; } } config.Disable(); - await notifier.NotifyConfigStatusChangedAsync(config.UserId, config.Id, config.Status, cancellationToken); + await notifier.NotifyConfigStatusChangedAsync( + config.UserId, + config.Id, + config.Status, + cancellationToken + ); } - dbContext.AuditLogs.Add(AuditLog.Create( - currentUser.UserId, "UserBlocked", "User", command.UserId.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + currentUser.UserId, + "UserBlocked", + "User", + command.UserId.ToString(), + metadata: null, + AuditSource.Web + ) + ); - await telegramNotifier.NotifyUserAsync(command.UserId, "⛔ Ваш аккаунт заблокирован администратором.", cancellationToken); + await telegramNotifier.NotifyUserAsync( + command.UserId, + "⛔ Ваш аккаунт заблокирован администратором.", + cancellationToken + ); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Admin/Users/ChangeUserRoleCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Users/ChangeUserRoleCommandHandler.cs index f1014ad..6d7dfdd 100644 --- a/backend/src/PnvPanel.Application/Admin/Users/ChangeUserRoleCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Users/ChangeUserRoleCommandHandler.cs @@ -5,18 +5,35 @@ using PnvPanel.Domain.Audit; namespace PnvPanel.Application.Admin.Users; -public sealed class ChangeUserRoleCommandHandler(IRoleService roleService, IAppDbContext dbContext, ICurrentUser currentUser) - : ICommandHandler +public sealed class ChangeUserRoleCommandHandler( + IRoleService roleService, + IAppDbContext dbContext, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(ChangeUserRoleCommand command, CancellationToken cancellationToken) + public async Task Handle( + ChangeUserRoleCommand command, + CancellationToken cancellationToken + ) { - var result = await roleService.ChangeUserRoleAsync(command.UserId, command.RoleId, cancellationToken); + var result = await roleService.ChangeUserRoleAsync( + command.UserId, + command.RoleId, + cancellationToken + ); if (!result.IsSuccess) return result; - dbContext.AuditLogs.Add(AuditLog.Create( - currentUser.UserId, "UserRoleChanged", "User", command.UserId.ToString(), - metadata: $"{{\"roleId\":\"{command.RoleId}\"}}", AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + currentUser.UserId, + "UserRoleChanged", + "User", + command.UserId.ToString(), + metadata: $"{{\"roleId\":\"{command.RoleId}\"}}", + AuditSource.Web + ) + ); return result; } diff --git a/backend/src/PnvPanel.Application/Admin/Users/DeleteUserCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Users/DeleteUserCommandHandler.cs index 36d0243..df137ca8 100644 --- a/backend/src/PnvPanel.Application/Admin/Users/DeleteUserCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Users/DeleteUserCommandHandler.cs @@ -9,40 +9,65 @@ namespace PnvPanel.Application.Admin.Users; /// Удаление пользователя админом: отзывает все его конфиги в 3x-ui, затем удаляет учётку. public sealed class DeleteUserCommandHandler( - IAppDbContext dbContext, IIdentityService identityService, IXuiPanelGateway gateway, - ITelegramNotifier telegramNotifier, ICurrentUser currentUser) - : ICommandHandler + IAppDbContext dbContext, + IIdentityService identityService, + IXuiPanelGateway gateway, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser +) : ICommandHandler { public async Task Handle(DeleteUserCommand command, CancellationToken cancellationToken) { if (currentUser.UserId == command.UserId) return Result.Failure(UserErrors.CannotDeleteSelf); - var configs = await dbContext.VpnConfigs - .Where(c => c.UserId == command.UserId && c.Status != ConfigStatus.Revoked) + var configs = await dbContext + .VpnConfigs.Where(c => c.UserId == command.UserId && c.Status != ConfigStatus.Revoked) .ToListAsync(cancellationToken); foreach (var config in configs) { - 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(); } - dbContext.AuditLogs.Add(AuditLog.Create( - currentUser.UserId, "UserDeleted", "User", command.UserId.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + currentUser.UserId, + "UserDeleted", + "User", + command.UserId.ToString(), + metadata: null, + AuditSource.Web + ) + ); // Коммитим отзыв конфигов + аудит ДО удаления учётки: UserManager.DeleteAsync ниже удаляет // AppUser отдельным путём (Identity store), после чего NotifyUserAsync уже не найдёт Telegram-привязку. await dbContext.SaveChangesAsync(cancellationToken); - await telegramNotifier.NotifyUserAsync(command.UserId, "🗑 Ваш аккаунт удалён администратором.", cancellationToken); + await telegramNotifier.NotifyUserAsync( + command.UserId, + "🗑 Ваш аккаунт удалён администратором.", + cancellationToken + ); return await identityService.DeleteUserAsync(command.UserId, cancellationToken); } diff --git a/backend/src/PnvPanel.Application/Admin/Users/ForceRevokeConfigCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Users/ForceRevokeConfigCommandHandler.cs index 0099c9d..113ab3d 100644 --- a/backend/src/PnvPanel.Application/Admin/Users/ForceRevokeConfigCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Users/ForceRevokeConfigCommandHandler.cs @@ -9,35 +9,70 @@ using PnvPanel.Domain.Configs; namespace PnvPanel.Application.Admin.Users; public sealed class ForceRevokeConfigCommandHandler( - IAppDbContext dbContext, IXuiPanelGateway gateway, IRealtimeNotifier notifier, - ITelegramNotifier telegramNotifier, ICurrentUser currentUser) - : ICommandHandler + IAppDbContext dbContext, + IXuiPanelGateway gateway, + IRealtimeNotifier notifier, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(ForceRevokeConfigCommand command, CancellationToken cancellationToken) + public async Task Handle( + ForceRevokeConfigCommand command, + CancellationToken cancellationToken + ) { - var config = await dbContext.VpnConfigs.FirstOrDefaultAsync(c => c.Id == command.ConfigId, cancellationToken); + var config = await dbContext.VpnConfigs.FirstOrDefaultAsync( + c => c.Id == command.ConfigId, + 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(config.UserId, config.Id, config.Status, cancellationToken); + await notifier.NotifyConfigStatusChangedAsync( + config.UserId, + config.Id, + config.Status, + cancellationToken + ); - dbContext.AuditLogs.Add(AuditLog.Create( - currentUser.UserId, "ConfigForceRevoked", "VpnConfig", config.Id.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + currentUser.UserId, + "ConfigForceRevoked", + "VpnConfig", + config.Id.ToString(), + metadata: null, + AuditSource.Web + ) + ); await telegramNotifier.NotifyUserAsync( - config.UserId, $"⚠️ Администратор отозвал ваш конфиг «{config.Label ?? config.ClientEmail}».", cancellationToken); + config.UserId, + $"⚠️ Администратор отозвал ваш конфиг «{config.Label ?? config.ClientEmail}».", + cancellationToken + ); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Admin/Users/GetUserConfigsQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Users/GetUserConfigsQueryHandler.cs index 853dbaa..8650051 100644 --- a/backend/src/PnvPanel.Application/Admin/Users/GetUserConfigsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Users/GetUserConfigsQueryHandler.cs @@ -10,11 +10,20 @@ namespace PnvPanel.Application.Admin.Users; public sealed class GetUserConfigsQueryHandler(IAppDbContext dbContext) : IQueryHandler>> { - public async Task>> Handle(GetUserConfigsQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + GetUserConfigsQuery query, + CancellationToken cancellationToken + ) { - var rows = await dbContext.VpnConfigs.AsNoTracking() + var rows = await dbContext + .VpnConfigs.AsNoTracking() .Where(c => c.UserId == query.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); diff --git a/backend/src/PnvPanel.Application/Admin/Users/ListUsersQuery.cs b/backend/src/PnvPanel.Application/Admin/Users/ListUsersQuery.cs index a2d88ef..b8120be 100644 --- a/backend/src/PnvPanel.Application/Admin/Users/ListUsersQuery.cs +++ b/backend/src/PnvPanel.Application/Admin/Users/ListUsersQuery.cs @@ -4,4 +4,5 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Admin.Users; -public sealed record ListUsersQuery(int Page, int PageSize, string? Search) : IQuery>>; +public sealed record ListUsersQuery(int Page, int PageSize, string? Search) + : IQuery>>; diff --git a/backend/src/PnvPanel.Application/Admin/Users/ListUsersQueryHandler.cs b/backend/src/PnvPanel.Application/Admin/Users/ListUsersQueryHandler.cs index 16b0588..037501d 100644 --- a/backend/src/PnvPanel.Application/Admin/Users/ListUsersQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Users/ListUsersQueryHandler.cs @@ -7,12 +7,20 @@ namespace PnvPanel.Application.Admin.Users; public sealed class ListUsersQueryHandler(IIdentityService identityService) : IQueryHandler>> { - public async Task>> Handle(ListUsersQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListUsersQuery query, + CancellationToken cancellationToken + ) { var page = query.Page <= 0 ? 1 : query.Page; var pageSize = query.PageSize is <= 0 or > 100 ? 20 : query.PageSize; - var result = await identityService.ListUsersAsync(page, pageSize, query.Search, cancellationToken); + var result = await identityService.ListUsersAsync( + page, + pageSize, + query.Search, + cancellationToken + ); return Result.Success(result); } } diff --git a/backend/src/PnvPanel.Application/Admin/Users/ResetUserPasswordCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Users/ResetUserPasswordCommandHandler.cs index 7403333..5aba190 100644 --- a/backend/src/PnvPanel.Application/Admin/Users/ResetUserPasswordCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Users/ResetUserPasswordCommandHandler.cs @@ -5,17 +5,35 @@ using PnvPanel.Domain.Audit; namespace PnvPanel.Application.Admin.Users; -public sealed class ResetUserPasswordCommandHandler(IAppDbContext dbContext, IIdentityService identityService, ICurrentUser currentUser) - : ICommandHandler +public sealed class ResetUserPasswordCommandHandler( + IAppDbContext dbContext, + IIdentityService identityService, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(ResetUserPasswordCommand command, CancellationToken cancellationToken) + public async Task Handle( + ResetUserPasswordCommand command, + CancellationToken cancellationToken + ) { - var result = await identityService.ResetPasswordAsync(command.UserId, command.NewPassword, cancellationToken); + var result = await identityService.ResetPasswordAsync( + command.UserId, + command.NewPassword, + cancellationToken + ); if (!result.IsSuccess) return result; - dbContext.AuditLogs.Add(AuditLog.Create( - currentUser.UserId, "UserPasswordReset", "User", command.UserId.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + currentUser.UserId, + "UserPasswordReset", + "User", + command.UserId.ToString(), + metadata: null, + AuditSource.Web + ) + ); await dbContext.SaveChangesAsync(cancellationToken); return Result.Success(); diff --git a/backend/src/PnvPanel.Application/Admin/Users/UnblockUserCommandHandler.cs b/backend/src/PnvPanel.Application/Admin/Users/UnblockUserCommandHandler.cs index 1e359c7..065f169 100644 --- a/backend/src/PnvPanel.Application/Admin/Users/UnblockUserCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Admin/Users/UnblockUserCommandHandler.cs @@ -10,32 +10,52 @@ namespace PnvPanel.Application.Admin.Users; /// Разблокировка возвращает в 3x-ui только конфиги, погашенные блокировкой (Disabled). public sealed class UnblockUserCommandHandler( - IAppDbContext dbContext, IIdentityService identityService, IXuiPanelGateway gateway, - IRealtimeNotifier notifier, ICurrentUser currentUser, ILogger logger) - : ICommandHandler + IAppDbContext dbContext, + IIdentityService identityService, + IXuiPanelGateway gateway, + IRealtimeNotifier notifier, + ICurrentUser currentUser, + ILogger logger +) : ICommandHandler { - public async Task Handle(UnblockUserCommand command, CancellationToken cancellationToken) + public async Task Handle( + UnblockUserCommand command, + CancellationToken cancellationToken + ) { - var unblockResult = await identityService.UnblockUserAsync(command.UserId, cancellationToken); + var unblockResult = await identityService.UnblockUserAsync( + command.UserId, + cancellationToken + ); if (!unblockResult.IsSuccess) return unblockResult; - var configs = await dbContext.VpnConfigs - .Where(c => c.UserId == command.UserId && c.Status == ConfigStatus.Disabled) + var configs = await dbContext + .VpnConfigs.Where(c => c.UserId == command.UserId && c.Status == ConfigStatus.Disabled) .ToListAsync(cancellationToken); foreach (var config in configs) { - 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) { var updateResult = 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 + ); if (!updateResult.IsSuccess) { @@ -44,17 +64,34 @@ public sealed class UnblockUserCommandHandler( // повторным UnblockUserCommand (идемпотентен). logger.LogWarning( "Failed to enable client for config {ConfigId} on node {NodeId} while unblocking user {UserId}: {Error}", - config.Id, node.Id, command.UserId, updateResult.Error); + config.Id, + node.Id, + command.UserId, + updateResult.Error + ); continue; } } config.Enable(); - await notifier.NotifyConfigStatusChangedAsync(config.UserId, config.Id, config.Status, cancellationToken); + await notifier.NotifyConfigStatusChangedAsync( + config.UserId, + config.Id, + config.Status, + cancellationToken + ); } - dbContext.AuditLogs.Add(AuditLog.Create( - currentUser.UserId, "UserUnblocked", "User", command.UserId.ToString(), metadata: null, AuditSource.Web)); + dbContext.AuditLogs.Add( + AuditLog.Create( + currentUser.UserId, + "UserUnblocked", + "User", + command.UserId.ToString(), + metadata: null, + AuditSource.Web + ) + ); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Admin/Users/UserErrors.cs b/backend/src/PnvPanel.Application/Admin/Users/UserErrors.cs index 8baf48d..70f706d 100644 --- a/backend/src/PnvPanel.Application/Admin/Users/UserErrors.cs +++ b/backend/src/PnvPanel.Application/Admin/Users/UserErrors.cs @@ -4,8 +4,13 @@ namespace PnvPanel.Application.Admin.Users; public static class UserErrors { - public static readonly Error NotFound = Error.NotFound("Users.NotFound", "Пользователь не найден."); + public static readonly Error NotFound = Error.NotFound( + "Users.NotFound", + "Пользователь не найден." + ); public static readonly Error CannotDeleteSelf = Error.Validation( - "Users.CannotDeleteSelf", "Нельзя удалить свою учётную запись здесь — используйте удаление аккаунта в Настройках."); + "Users.CannotDeleteSelf", + "Нельзя удалить свою учётную запись здесь — используйте удаление аккаунта в Настройках." + ); } diff --git a/backend/src/PnvPanel.Application/Apps/ClientAppDto.cs b/backend/src/PnvPanel.Application/Apps/ClientAppDto.cs index 5d66310..26ad78f 100644 --- a/backend/src/PnvPanel.Application/Apps/ClientAppDto.cs +++ b/backend/src/PnvPanel.Application/Apps/ClientAppDto.cs @@ -2,7 +2,13 @@ using PnvPanel.Domain.Apps; namespace PnvPanel.Application.Apps; -public sealed record ClientAppDto(Guid Id, string Name, string DownloadUrl, string? Description, string? IconUrl) +public sealed record ClientAppDto( + Guid Id, + string Name, + string DownloadUrl, + string? Description, + string? IconUrl +) { public static ClientAppDto FromDomain(ClientApp app) => new(app.Id, app.Name, app.DownloadUrl.ToString(), app.Description, app.IconUrl); diff --git a/backend/src/PnvPanel.Application/Apps/ListAppsQuery.cs b/backend/src/PnvPanel.Application/Apps/ListAppsQuery.cs index c0a6904..6ab079c 100644 --- a/backend/src/PnvPanel.Application/Apps/ListAppsQuery.cs +++ b/backend/src/PnvPanel.Application/Apps/ListAppsQuery.cs @@ -4,4 +4,6 @@ using PnvPanel.Domain.Apps; namespace PnvPanel.Application.Apps; -public sealed record ListAppsQuery : IQuery>>>, IRequiresActivation; +public sealed record ListAppsQuery + : IQuery>>>, + IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/Apps/ListAppsQueryHandler.cs b/backend/src/PnvPanel.Application/Apps/ListAppsQueryHandler.cs index df66f69..9ebacdf 100644 --- a/backend/src/PnvPanel.Application/Apps/ListAppsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Apps/ListAppsQueryHandler.cs @@ -7,22 +7,30 @@ using PnvPanel.Domain.Apps; namespace PnvPanel.Application.Apps; public sealed class ListAppsQueryHandler(IAppDbContext dbContext) - : IQueryHandler>>> + : IQueryHandler< + ListAppsQuery, + Result>> + > { public async Task>>> Handle( - ListAppsQuery query, CancellationToken cancellationToken) + ListAppsQuery query, + CancellationToken cancellationToken + ) { - var apps = await dbContext.ClientApps.AsNoTracking() + var apps = await dbContext + .ClientApps.AsNoTracking() .Where(a => a.IsEnabled) .OrderBy(a => a.SortOrder) .ToListAsync(cancellationToken); - var grouped = apps - .GroupBy(a => a.OperatingSystem) + var grouped = apps.GroupBy(a => a.OperatingSystem) .ToDictionary( g => g.Key, - g => (IReadOnlyList)g.Select(ClientAppDto.FromDomain).ToList()); + g => (IReadOnlyList)g.Select(ClientAppDto.FromDomain).ToList() + ); - return Result.Success>>(grouped); + return Result.Success>>( + grouped + ); } } diff --git a/backend/src/PnvPanel.Application/Auth/AuthErrors.cs b/backend/src/PnvPanel.Application/Auth/AuthErrors.cs index 4e5bf6d..38a524a 100644 --- a/backend/src/PnvPanel.Application/Auth/AuthErrors.cs +++ b/backend/src/PnvPanel.Application/Auth/AuthErrors.cs @@ -4,24 +4,38 @@ namespace PnvPanel.Application.Auth; public static class AuthErrors { - public static readonly Error DuplicateUserName = - Error.Conflict("Auth.DuplicateUserName", "Пользователь с таким именем уже существует."); + public static readonly Error DuplicateUserName = Error.Conflict( + "Auth.DuplicateUserName", + "Пользователь с таким именем уже существует." + ); - public static readonly Error InvalidCredentials = - Error.Unauthorized("Auth.InvalidCredentials", "Неверное имя пользователя или пароль."); + public static readonly Error InvalidCredentials = Error.Unauthorized( + "Auth.InvalidCredentials", + "Неверное имя пользователя или пароль." + ); - public static readonly Error LockedOut = - Error.Unauthorized("Auth.LockedOut", "Слишком много неудачных попыток входа. Попробуйте позже."); + public static readonly Error LockedOut = Error.Unauthorized( + "Auth.LockedOut", + "Слишком много неудачных попыток входа. Попробуйте позже." + ); - public static readonly Error InvalidRefreshToken = - Error.Unauthorized("Auth.InvalidRefreshToken", "Недействительный refresh-токен."); + public static readonly Error InvalidRefreshToken = Error.Unauthorized( + "Auth.InvalidRefreshToken", + "Недействительный refresh-токен." + ); - public static readonly Error Unauthorized = - Error.Unauthorized("Auth.Unauthorized", "Требуется аутентификация."); + public static readonly Error Unauthorized = Error.Unauthorized( + "Auth.Unauthorized", + "Требуется аутентификация." + ); - public static readonly Error UserBlocked = - Error.Forbidden("Auth.UserBlocked", "Аккаунт заблокирован администратором."); + public static readonly Error UserBlocked = Error.Forbidden( + "Auth.UserBlocked", + "Аккаунт заблокирован администратором." + ); - public static readonly Error NotActivated = - Error.Forbidden("Auth.NotActivated", "Аккаунт не активирован — обратитесь к администратору."); + public static readonly Error NotActivated = Error.Forbidden( + "Auth.NotActivated", + "Аккаунт не активирован — обратитесь к администратору." + ); } diff --git a/backend/src/PnvPanel.Application/Auth/AuthResult.cs b/backend/src/PnvPanel.Application/Auth/AuthResult.cs index fe246c0..e45872a 100644 --- a/backend/src/PnvPanel.Application/Auth/AuthResult.cs +++ b/backend/src/PnvPanel.Application/Auth/AuthResult.cs @@ -5,4 +5,5 @@ public sealed record AuthResult( DateTimeOffset AccessTokenExpiresAt, string RefreshToken, DateTimeOffset RefreshTokenExpiresAt, - CurrentUserDto User); + CurrentUserDto User +); diff --git a/backend/src/PnvPanel.Application/Auth/ChangePassword/ChangePasswordCommand.cs b/backend/src/PnvPanel.Application/Auth/ChangePassword/ChangePasswordCommand.cs index 122e399..f243757 100644 --- a/backend/src/PnvPanel.Application/Auth/ChangePassword/ChangePasswordCommand.cs +++ b/backend/src/PnvPanel.Application/Auth/ChangePassword/ChangePasswordCommand.cs @@ -3,4 +3,5 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Auth.ChangePassword; -public sealed record ChangePasswordCommand(string CurrentPassword, string NewPassword) : ICommand; +public sealed record ChangePasswordCommand(string CurrentPassword, string NewPassword) + : ICommand; diff --git a/backend/src/PnvPanel.Application/Auth/ChangePassword/ChangePasswordCommandHandler.cs b/backend/src/PnvPanel.Application/Auth/ChangePassword/ChangePasswordCommandHandler.cs index 87b97a1..e58df70 100644 --- a/backend/src/PnvPanel.Application/Auth/ChangePassword/ChangePasswordCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Auth/ChangePassword/ChangePasswordCommandHandler.cs @@ -4,14 +4,21 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Auth.ChangePassword; -public sealed class ChangePasswordCommandHandler(IIdentityService identityService, ICurrentUser currentUser) - : ICommandHandler +public sealed class ChangePasswordCommandHandler( + IIdentityService identityService, + ICurrentUser currentUser +) : ICommandHandler { public Task Handle(ChangePasswordCommand command, CancellationToken cancellationToken) { if (currentUser.UserId is not { } userId) return Task.FromResult(Result.Failure(AuthErrors.Unauthorized)); - return identityService.ChangePasswordAsync(userId, command.CurrentPassword, command.NewPassword, cancellationToken); + return identityService.ChangePasswordAsync( + userId, + command.CurrentPassword, + command.NewPassword, + cancellationToken + ); } } diff --git a/backend/src/PnvPanel.Application/Auth/ChangeUserName/ChangeUserNameCommandHandler.cs b/backend/src/PnvPanel.Application/Auth/ChangeUserName/ChangeUserNameCommandHandler.cs index b6eebaa..6ac81a4 100644 --- a/backend/src/PnvPanel.Application/Auth/ChangeUserName/ChangeUserNameCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Auth/ChangeUserName/ChangeUserNameCommandHandler.cs @@ -4,8 +4,10 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Auth.ChangeUserName; -public sealed class ChangeUserNameCommandHandler(IIdentityService identityService, ICurrentUser currentUser) - : ICommandHandler +public sealed class ChangeUserNameCommandHandler( + IIdentityService identityService, + ICurrentUser currentUser +) : ICommandHandler { public Task Handle(ChangeUserNameCommand command, CancellationToken cancellationToken) { diff --git a/backend/src/PnvPanel.Application/Auth/CurrentUserDto.cs b/backend/src/PnvPanel.Application/Auth/CurrentUserDto.cs index 4a91132..925ab35 100644 --- a/backend/src/PnvPanel.Application/Auth/CurrentUserDto.cs +++ b/backend/src/PnvPanel.Application/Auth/CurrentUserDto.cs @@ -1,3 +1,9 @@ namespace PnvPanel.Application.Auth; -public sealed record CurrentUserDto(Guid Id, string UserName, string Role, bool IsActivated, bool TelegramLinked); +public sealed record CurrentUserDto( + Guid Id, + string UserName, + string Role, + bool IsActivated, + bool TelegramLinked +); diff --git a/backend/src/PnvPanel.Application/Auth/DeleteMyAccount/DeleteMyAccountCommandHandler.cs b/backend/src/PnvPanel.Application/Auth/DeleteMyAccount/DeleteMyAccountCommandHandler.cs index c8d3c9e..9b2f27b 100644 --- a/backend/src/PnvPanel.Application/Auth/DeleteMyAccount/DeleteMyAccountCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Auth/DeleteMyAccount/DeleteMyAccountCommandHandler.cs @@ -6,27 +6,44 @@ using PnvPanel.Domain.Configs; namespace PnvPanel.Application.Auth.DeleteMyAccount; -public sealed class DeleteMyAccountCommandHandler(IAppDbContext dbContext, IIdentityService identityService, IXuiPanelGateway gateway, ICurrentUser currentUser) - : ICommandHandler +public sealed class DeleteMyAccountCommandHandler( + IAppDbContext dbContext, + IIdentityService identityService, + IXuiPanelGateway gateway, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(DeleteMyAccountCommand command, CancellationToken cancellationToken) + public async Task Handle( + DeleteMyAccountCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); - var configs = await dbContext.VpnConfigs - .Where(c => c.UserId == userId && c.Status != ConfigStatus.Revoked) + var configs = await dbContext + .VpnConfigs.Where(c => c.UserId == userId && c.Status != ConfigStatus.Revoked) .ToListAsync(cancellationToken); foreach (var config in configs) { - 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(); } diff --git a/backend/src/PnvPanel.Application/Auth/Login/LoginCommandHandler.cs b/backend/src/PnvPanel.Application/Auth/Login/LoginCommandHandler.cs index 748577b..90bec2a 100644 --- a/backend/src/PnvPanel.Application/Auth/Login/LoginCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Auth/Login/LoginCommandHandler.cs @@ -7,11 +7,19 @@ namespace PnvPanel.Application.Auth.Login; public sealed class LoginCommandHandler( IIdentityService identityService, IJwtTokenService jwtTokenService, - IRefreshTokenService refreshTokenService) : ICommandHandler> + IRefreshTokenService refreshTokenService +) : ICommandHandler> { - public async Task> Handle(LoginCommand command, CancellationToken cancellationToken) + public async Task> Handle( + LoginCommand command, + CancellationToken cancellationToken + ) { - var credentialsResult = await identityService.ValidateCredentialsAsync(command.UserName, command.Password, cancellationToken); + var credentialsResult = await identityService.ValidateCredentialsAsync( + command.UserName, + command.Password, + cancellationToken + ); if (!credentialsResult.IsSuccess) return Result.Failure(credentialsResult.Error); @@ -23,14 +31,26 @@ public sealed class LoginCommandHandler( var (accessToken, accessExpiresAt) = jwtTokenService.GenerateAccessToken(user); var refreshToken = await refreshTokenService.IssueAsync(user.Id, cancellationToken); - var telegramInfo = await identityService.GetTelegramLinkInfoAsync(profile.Id, cancellationToken); - var dto = new CurrentUserDto(profile.Id, profile.UserName, profile.Role, profile.IsActivated, telegramInfo.IsLinked); + var telegramInfo = await identityService.GetTelegramLinkInfoAsync( + profile.Id, + cancellationToken + ); + var dto = new CurrentUserDto( + profile.Id, + profile.UserName, + profile.Role, + profile.IsActivated, + telegramInfo.IsLinked + ); - return Result.Success(new AuthResult( - accessToken, - accessExpiresAt, - refreshToken.RawToken, - refreshToken.ExpiresAt, - dto)); + return Result.Success( + new AuthResult( + accessToken, + accessExpiresAt, + refreshToken.RawToken, + refreshToken.ExpiresAt, + dto + ) + ); } } diff --git a/backend/src/PnvPanel.Application/Auth/Me/GetCurrentUserQueryHandler.cs b/backend/src/PnvPanel.Application/Auth/Me/GetCurrentUserQueryHandler.cs index b3c02e7..57573a7 100644 --- a/backend/src/PnvPanel.Application/Auth/Me/GetCurrentUserQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Auth/Me/GetCurrentUserQueryHandler.cs @@ -4,10 +4,15 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Auth.Me; -public sealed class GetCurrentUserQueryHandler(IIdentityService identityService, ICurrentUser currentUser) - : IQueryHandler> +public sealed class GetCurrentUserQueryHandler( + IIdentityService identityService, + ICurrentUser currentUser +) : IQueryHandler> { - public async Task> Handle(GetCurrentUserQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetCurrentUserQuery query, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); @@ -16,8 +21,19 @@ public sealed class GetCurrentUserQueryHandler(IIdentityService identityService, if (profile is null) return Result.Failure(AuthErrors.Unauthorized); - var telegramInfo = await identityService.GetTelegramLinkInfoAsync(userId, cancellationToken); + var telegramInfo = await identityService.GetTelegramLinkInfoAsync( + userId, + cancellationToken + ); - return Result.Success(new CurrentUserDto(profile.Id, profile.UserName, profile.Role, profile.IsActivated, telegramInfo.IsLinked)); + return Result.Success( + new CurrentUserDto( + profile.Id, + profile.UserName, + profile.Role, + profile.IsActivated, + telegramInfo.IsLinked + ) + ); } } diff --git a/backend/src/PnvPanel.Application/Auth/Refresh/RefreshCommandHandler.cs b/backend/src/PnvPanel.Application/Auth/Refresh/RefreshCommandHandler.cs index 3501728..4f39d78 100644 --- a/backend/src/PnvPanel.Application/Auth/Refresh/RefreshCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Auth/Refresh/RefreshCommandHandler.cs @@ -7,29 +7,51 @@ namespace PnvPanel.Application.Auth.Refresh; public sealed class RefreshCommandHandler( IIdentityService identityService, IJwtTokenService jwtTokenService, - IRefreshTokenService refreshTokenService) : ICommandHandler> + IRefreshTokenService refreshTokenService +) : ICommandHandler> { - public async Task> Handle(RefreshCommand command, CancellationToken cancellationToken) + public async Task> Handle( + RefreshCommand command, + CancellationToken cancellationToken + ) { - var rotated = await refreshTokenService.RotateAsync(command.RawRefreshToken, cancellationToken); + var rotated = await refreshTokenService.RotateAsync( + command.RawRefreshToken, + cancellationToken + ); if (!rotated.IsSuccess) return Result.Failure(rotated.Error); - var profile = await identityService.GetProfileAsync(rotated.Value.UserId, cancellationToken); + var profile = await identityService.GetProfileAsync( + rotated.Value.UserId, + cancellationToken + ); if (profile is null) return Result.Failure(AuthErrors.InvalidRefreshToken); var authUser = new AuthenticatedUser(profile.Id, profile.UserName, profile.Role); var (accessToken, accessExpiresAt) = jwtTokenService.GenerateAccessToken(authUser); - var telegramInfo = await identityService.GetTelegramLinkInfoAsync(profile.Id, cancellationToken); - var dto = new CurrentUserDto(profile.Id, profile.UserName, profile.Role, profile.IsActivated, telegramInfo.IsLinked); + var telegramInfo = await identityService.GetTelegramLinkInfoAsync( + profile.Id, + cancellationToken + ); + var dto = new CurrentUserDto( + profile.Id, + profile.UserName, + profile.Role, + profile.IsActivated, + telegramInfo.IsLinked + ); - return Result.Success(new AuthResult( - accessToken, - accessExpiresAt, - rotated.Value.RawToken, - rotated.Value.ExpiresAt, - dto)); + return Result.Success( + new AuthResult( + accessToken, + accessExpiresAt, + rotated.Value.RawToken, + rotated.Value.ExpiresAt, + dto + ) + ); } } diff --git a/backend/src/PnvPanel.Application/Auth/Register/RegisterCommand.cs b/backend/src/PnvPanel.Application/Auth/Register/RegisterCommand.cs index a0481ea..8033ecf 100644 --- a/backend/src/PnvPanel.Application/Auth/Register/RegisterCommand.cs +++ b/backend/src/PnvPanel.Application/Auth/Register/RegisterCommand.cs @@ -3,6 +3,7 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Auth.Register; -public sealed record RegisterCommand(string UserName, string Password) : ICommand>; +public sealed record RegisterCommand(string UserName, string Password) + : ICommand>; public sealed record RegisterResult(Guid Id, string UserName); diff --git a/backend/src/PnvPanel.Application/Auth/Register/RegisterCommandHandler.cs b/backend/src/PnvPanel.Application/Auth/Register/RegisterCommandHandler.cs index 54409f8..8b64bae 100644 --- a/backend/src/PnvPanel.Application/Auth/Register/RegisterCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Auth/Register/RegisterCommandHandler.cs @@ -7,9 +7,16 @@ namespace PnvPanel.Application.Auth.Register; public sealed class RegisterCommandHandler(IIdentityService identityService) : ICommandHandler> { - public async Task> Handle(RegisterCommand command, CancellationToken cancellationToken) + public async Task> Handle( + RegisterCommand command, + CancellationToken cancellationToken + ) { - var result = await identityService.CreateUserAsync(command.UserName, command.Password, cancellationToken); + var result = await identityService.CreateUserAsync( + command.UserName, + command.Password, + cancellationToken + ); return result.IsSuccess ? Result.Success(new RegisterResult(result.Value, command.UserName)) diff --git a/backend/src/PnvPanel.Application/Common/Behaviors/LoggingBehavior.cs b/backend/src/PnvPanel.Application/Common/Behaviors/LoggingBehavior.cs index 344662f..5858deb 100644 --- a/backend/src/PnvPanel.Application/Common/Behaviors/LoggingBehavior.cs +++ b/backend/src/PnvPanel.Application/Common/Behaviors/LoggingBehavior.cs @@ -3,11 +3,16 @@ using PnvPanel.Application.Common.Messaging; namespace PnvPanel.Application.Common.Behaviors; -public sealed class LoggingBehavior(ILogger> logger) - : IPipelineBehavior +public sealed class LoggingBehavior( + ILogger> logger +) : IPipelineBehavior where TRequest : notnull { - public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken + ) { var requestName = typeof(TRequest).Name; logger.LogInformation("Handling {RequestName}", requestName); diff --git a/backend/src/PnvPanel.Application/Common/Behaviors/RequireActivationBehavior.cs b/backend/src/PnvPanel.Application/Common/Behaviors/RequireActivationBehavior.cs index de0bac1..d9c5c9b 100644 --- a/backend/src/PnvPanel.Application/Common/Behaviors/RequireActivationBehavior.cs +++ b/backend/src/PnvPanel.Application/Common/Behaviors/RequireActivationBehavior.cs @@ -10,12 +10,18 @@ namespace PnvPanel.Application.Common.Behaviors; /// вместо разбросанных if(!profile.IsActivated) по хендлерам. Применяется только к запросам с этим /// маркером (generic-ограничение), остальные проходят мимо. /// -public sealed class RequireActivationBehavior(ICurrentUser currentUser, IIdentityService identityService) - : IPipelineBehavior +public sealed class RequireActivationBehavior( + ICurrentUser currentUser, + IIdentityService identityService +) : IPipelineBehavior where TRequest : IRequiresActivation where TResponse : Result { - public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return ResultFailureFactory.Create(AuthErrors.Unauthorized); diff --git a/backend/src/PnvPanel.Application/Common/Behaviors/ResultFailureFactory.cs b/backend/src/PnvPanel.Application/Common/Behaviors/ResultFailureFactory.cs index af04d32..a3ffcc3 100644 --- a/backend/src/PnvPanel.Application/Common/Behaviors/ResultFailureFactory.cs +++ b/backend/src/PnvPanel.Application/Common/Behaviors/ResultFailureFactory.cs @@ -5,7 +5,8 @@ namespace PnvPanel.Application.Common.Behaviors; /// Строит Result/Result<T> failure-ответ через reflection — общий хелпер для generic pipeline behaviors. internal static class ResultFailureFactory { - public static TResponse Create(Error error) where TResponse : Result + public static TResponse Create(Error error) + where TResponse : Result { if (typeof(TResponse) == typeof(Result)) return (TResponse)(object)Result.Failure(error); diff --git a/backend/src/PnvPanel.Application/Common/Behaviors/UnitOfWorkBehavior.cs b/backend/src/PnvPanel.Application/Common/Behaviors/UnitOfWorkBehavior.cs index e4953e0..ae9a662 100644 --- a/backend/src/PnvPanel.Application/Common/Behaviors/UnitOfWorkBehavior.cs +++ b/backend/src/PnvPanel.Application/Common/Behaviors/UnitOfWorkBehavior.cs @@ -12,7 +12,11 @@ public sealed class UnitOfWorkBehavior(IAppDbContext dbCont : IPipelineBehavior where TRequest : ICommand { - public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken + ) { var response = await next(); await dbContext.SaveChangesAsync(cancellationToken); diff --git a/backend/src/PnvPanel.Application/Common/Behaviors/ValidationBehavior.cs b/backend/src/PnvPanel.Application/Common/Behaviors/ValidationBehavior.cs index 0a806e4..4dd6d04 100644 --- a/backend/src/PnvPanel.Application/Common/Behaviors/ValidationBehavior.cs +++ b/backend/src/PnvPanel.Application/Common/Behaviors/ValidationBehavior.cs @@ -4,12 +4,17 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Common.Behaviors; -public sealed class ValidationBehavior(IEnumerable> validators) - : IPipelineBehavior +public sealed class ValidationBehavior( + IEnumerable> validators +) : IPipelineBehavior where TRequest : notnull where TResponse : Result { - public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken) + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken + ) { if (!validators.Any()) return await next(); @@ -25,7 +30,8 @@ public sealed class ValidationBehavior(IEnumerable f.ErrorMessage))); + string.Join("; ", failures.Select(f => f.ErrorMessage)) + ); return ResultFailureFactory.Create(error); } diff --git a/backend/src/PnvPanel.Application/Common/Interfaces/IIdentityService.cs b/backend/src/PnvPanel.Application/Common/Interfaces/IIdentityService.cs index 14ad92e..1069978 100644 --- a/backend/src/PnvPanel.Application/Common/Interfaces/IIdentityService.cs +++ b/backend/src/PnvPanel.Application/Common/Interfaces/IIdentityService.cs @@ -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> CreateUserAsync(string userName, string password, CancellationToken cancellationToken); + Task> CreateUserAsync( + string userName, + string password, + CancellationToken cancellationToken + ); - Task> ValidateCredentialsAsync(string userName, string password, CancellationToken cancellationToken); + Task> ValidateCredentialsAsync( + string userName, + string password, + CancellationToken cancellationToken + ); Task GetProfileAsync(Guid userId, CancellationToken cancellationToken); - Task ChangePasswordAsync(Guid userId, string currentPassword, string newPassword, CancellationToken cancellationToken); + Task ChangePasswordAsync( + Guid userId, + string currentPassword, + string newPassword, + CancellationToken cancellationToken + ); - Task ChangeUserNameAsync(Guid userId, string newUserName, CancellationToken cancellationToken); + Task ChangeUserNameAsync( + Guid userId, + string newUserName, + CancellationToken cancellationToken + ); /// /// Помечает пользователя активированным. Изменение не коммитится немедленно (в отличие от /// CreateUserAsync/ChangePasswordAsync) — оно попадает в трекер того же DbContext и сохраняется /// вместе с изменением ActivationRequest одной транзакцией через UnitOfWorkBehavior. /// - Task ActivateUserAsync(Guid userId, Guid activatedBy, CancellationToken cancellationToken); + Task ActivateUserAsync( + Guid userId, + Guid activatedBy, + CancellationToken cancellationToken + ); - Task> GetUserNamesAsync(IReadOnlyCollection userIds, CancellationToken cancellationToken); + Task> GetUserNamesAsync( + IReadOnlyCollection userIds, + CancellationToken cancellationToken + ); /// Удаляет аккаунт (самоудаление). Конфиги должны быть отозваны заранее вызывающей стороной. Task DeleteUserAsync(Guid userId, CancellationToken cancellationToken); /// Для агрегированной подписки /sub/{userToken} (все активные конфиги пользователя). - Task FindUserIdBySubscriptionTokenAsync(string token, CancellationToken cancellationToken); + Task FindUserIdBySubscriptionTokenAsync( + string token, + CancellationToken cancellationToken + ); /// Блокировка: вход запрещён (см. ValidateCredentialsAsync). Конфиги гасит вызывающая сторона. Task BlockUserAsync(Guid userId, CancellationToken cancellationToken); @@ -47,21 +89,43 @@ public interface IIdentityService Task UnblockUserAsync(Guid userId, CancellationToken cancellationToken); /// Сброс пароля админом — для пользователей без привязанного Telegram (M7). - Task ResetPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken); + Task ResetPasswordAsync( + Guid userId, + string newPassword, + CancellationToken cancellationToken + ); - Task> ListUsersAsync(int page, int pageSize, string? search, CancellationToken cancellationToken); + Task> ListUsersAsync( + int page, + int pageSize, + string? search, + CancellationToken cancellationToken + ); Task GetUserStatsAsync(CancellationToken cancellationToken); - Task LinkTelegramAsync(Guid userId, long telegramUserId, string? telegramUsername, CancellationToken cancellationToken); + Task LinkTelegramAsync( + Guid userId, + long telegramUserId, + string? telegramUsername, + CancellationToken cancellationToken + ); Task UnlinkTelegramAsync(Guid userId, CancellationToken cancellationToken); - Task FindUserIdByTelegramUserIdAsync(long telegramUserId, CancellationToken cancellationToken); + Task FindUserIdByTelegramUserIdAsync( + long telegramUserId, + CancellationToken cancellationToken + ); - Task GetTelegramLinkInfoAsync(Guid userId, CancellationToken cancellationToken); + Task GetTelegramLinkInfoAsync( + Guid userId, + CancellationToken cancellationToken + ); /// Telegram ID активных (активированных, не заблокированных) пользователей с привязкой — /// для рассылки уведомлений вроде публикации новости. - Task> GetActivatedLinkedTelegramUserIdsAsync(CancellationToken cancellationToken); + Task> GetActivatedLinkedTelegramUserIdsAsync( + CancellationToken cancellationToken + ); } diff --git a/backend/src/PnvPanel.Application/Common/Interfaces/IRealtimeNotifier.cs b/backend/src/PnvPanel.Application/Common/Interfaces/IRealtimeNotifier.cs index abcb868..a50f928 100644 --- a/backend/src/PnvPanel.Application/Common/Interfaces/IRealtimeNotifier.cs +++ b/backend/src/PnvPanel.Application/Common/Interfaces/IRealtimeNotifier.cs @@ -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); /// Единственное широковещательное событие (всем подключенным клиентам), а не по группе. - 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 + ); /// Новый комментарий или смена статуса — пушится автору тикета (не всем участникам треда). Task NotifyTicketUpdatedAsync(Guid ticketId, Guid userId, CancellationToken cancellationToken); diff --git a/backend/src/PnvPanel.Application/Common/Interfaces/IRefreshTokenService.cs b/backend/src/PnvPanel.Application/Common/Interfaces/IRefreshTokenService.cs index 78b3908..d62f64c 100644 --- a/backend/src/PnvPanel.Application/Common/Interfaces/IRefreshTokenService.cs +++ b/backend/src/PnvPanel.Application/Common/Interfaces/IRefreshTokenService.cs @@ -10,7 +10,10 @@ public interface IRefreshTokenService { Task IssueAsync(Guid userId, CancellationToken cancellationToken); - Task> RotateAsync(string rawToken, CancellationToken cancellationToken); + Task> RotateAsync( + string rawToken, + CancellationToken cancellationToken + ); Task RevokeAsync(string rawToken, CancellationToken cancellationToken); } diff --git a/backend/src/PnvPanel.Application/Common/Interfaces/IRoleService.cs b/backend/src/PnvPanel.Application/Common/Interfaces/IRoleService.cs index f191d22..272608b 100644 --- a/backend/src/PnvPanel.Application/Common/Interfaces/IRoleService.cs +++ b/backend/src/PnvPanel.Application/Common/Interfaces/IRoleService.cs @@ -6,9 +6,19 @@ public sealed record RoleDto(Guid Id, string Name, int MaxConfigs, int MaxIpLimi public interface IRoleService { - Task> CreateRoleAsync(string name, int maxConfigs, int maxIpLimit, CancellationToken cancellationToken); + Task> CreateRoleAsync( + string name, + int maxConfigs, + int maxIpLimit, + CancellationToken cancellationToken + ); - Task> UpdateRoleAsync(Guid roleId, int maxConfigs, int maxIpLimit, CancellationToken cancellationToken); + Task> UpdateRoleAsync( + Guid roleId, + int maxConfigs, + int maxIpLimit, + CancellationToken cancellationToken + ); Task DeleteRoleAsync(Guid roleId, CancellationToken cancellationToken); diff --git a/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs b/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs index a37de20..74ead5a 100644 --- a/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs +++ b/backend/src/PnvPanel.Application/Common/Interfaces/ITelegramNotifier.cs @@ -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 + ); /// Личное сообщение пользователю, если у него привязан Telegram (иначе no-op). /// Если задан PublicSiteUrl — добавляет кнопку-ссылку на сайт. @@ -18,11 +22,21 @@ public interface ITelegramNotifier /// Баг-репорт/предложение — только кнопка-ссылка на сайт (переписка и картинки — там), /// без инлайн-действий. - Task NotifyAdminsBugReportCreatedAsync(Guid ticketId, string userName, string message, CancellationToken cancellationToken); + Task NotifyAdminsBugReportCreatedAsync( + Guid ticketId, + string userName, + string message, + CancellationToken cancellationToken + ); /// Заявка на роль — инлайн-кнопки «Одобрить/Отклонить», решается полностью в Telegram. Task NotifyAdminsRoleRequestCreatedAsync( - Guid ticketId, string userName, string roleDescription, string justification, CancellationToken cancellationToken); + Guid ticketId, + string userName, + string roleDescription, + string justification, + CancellationToken cancellationToken + ); /// Рассылка о публикации новости всем активированным пользователям с привязанным Telegram /// (кнопка-ссылка на сайт, где новость показана целиком). @@ -30,5 +44,10 @@ public interface ITelegramNotifier /// Пользователь переоткрыл решённый тикет — только кнопка-ссылка на сайт, без /// инлайн-действий (аналогично баг-репортам). - Task NotifyAdminsTicketReopenedAsync(Guid ticketId, string userName, TicketType type, CancellationToken cancellationToken); + Task NotifyAdminsTicketReopenedAsync( + Guid ticketId, + string userName, + TicketType type, + CancellationToken cancellationToken + ); } diff --git a/backend/src/PnvPanel.Application/Common/Interfaces/IXuiPanelGateway.cs b/backend/src/PnvPanel.Application/Common/Interfaces/IXuiPanelGateway.cs index aea9de9..521f5bf 100644 --- a/backend/src/PnvPanel.Application/Common/Interfaces/IXuiPanelGateway.cs +++ b/backend/src/PnvPanel.Application/Common/Interfaces/IXuiPanelGateway.cs @@ -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 ProbeAsync(Node node, CancellationToken cancellationToken); - Task>> ListInboundsAsync(Node node, CancellationToken cancellationToken); + Task>> ListInboundsAsync( + Node node, + CancellationToken cancellationToken + ); void InvalidateClient(Guid nodeId); @@ -30,25 +38,49 @@ public interface IXuiPanelGateway /// -1 (RoleQuota.Unlimited) означает без лимита — гейтвей сам переводит его в нативное значение 3x-ui. /// Task> 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 RemoveClientAsync( - Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol, - CancellationToken cancellationToken); + Node node, + string inboundRemoteId, + string clientExternalId, + VpnProtocol protocol, + CancellationToken cancellationToken + ); Task 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> 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 + ); /// /// Трафик по клиентам инбаунда, ключ — ClientEmail. ThreeXui.Net не даёт типизированного метода /// для этого — извлекается из сырого clientStats[] в RawInboundJson (стандартное поле 3x-ui API). /// Task>> GetClientTrafficAsync( - Node node, string inboundRemoteId, CancellationToken cancellationToken); + Node node, + string inboundRemoteId, + CancellationToken cancellationToken + ); } diff --git a/backend/src/PnvPanel.Application/Common/Messaging/ICommandHandler.cs b/backend/src/PnvPanel.Application/Common/Messaging/ICommandHandler.cs index 963ee24..8e85271 100644 --- a/backend/src/PnvPanel.Application/Common/Messaging/ICommandHandler.cs +++ b/backend/src/PnvPanel.Application/Common/Messaging/ICommandHandler.cs @@ -1,6 +1,7 @@ namespace PnvPanel.Application.Common.Messaging; -public interface ICommandHandler where TCommand : ICommand +public interface ICommandHandler + where TCommand : ICommand { Task Handle(TCommand command, CancellationToken cancellationToken); } diff --git a/backend/src/PnvPanel.Application/Common/Messaging/IPipelineBehavior.cs b/backend/src/PnvPanel.Application/Common/Messaging/IPipelineBehavior.cs index dbbce6d..1445dc2 100644 --- a/backend/src/PnvPanel.Application/Common/Messaging/IPipelineBehavior.cs +++ b/backend/src/PnvPanel.Application/Common/Messaging/IPipelineBehavior.cs @@ -4,5 +4,9 @@ public delegate Task RequestHandlerDelegate(); public interface IPipelineBehavior { - Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken); + Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken + ); } diff --git a/backend/src/PnvPanel.Application/Common/Messaging/IQueryHandler.cs b/backend/src/PnvPanel.Application/Common/Messaging/IQueryHandler.cs index fc8f6b9..24ba3ca 100644 --- a/backend/src/PnvPanel.Application/Common/Messaging/IQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Common/Messaging/IQueryHandler.cs @@ -1,6 +1,7 @@ namespace PnvPanel.Application.Common.Messaging; -public interface IQueryHandler where TQuery : IQuery +public interface IQueryHandler + where TQuery : IQuery { Task Handle(TQuery query, CancellationToken cancellationToken); } diff --git a/backend/src/PnvPanel.Application/Common/Messaging/ISender.cs b/backend/src/PnvPanel.Application/Common/Messaging/ISender.cs index 35484f8..56782d6 100644 --- a/backend/src/PnvPanel.Application/Common/Messaging/ISender.cs +++ b/backend/src/PnvPanel.Application/Common/Messaging/ISender.cs @@ -3,6 +3,12 @@ namespace PnvPanel.Application.Common.Messaging; /// Собственный тонкий CQRS-диспетчер (без MediatR). public interface ISender { - Task Send(ICommand command, CancellationToken cancellationToken = default); - Task Send(IQuery query, CancellationToken cancellationToken = default); + Task Send( + ICommand command, + CancellationToken cancellationToken = default + ); + Task Send( + IQuery query, + CancellationToken cancellationToken = default + ); } diff --git a/backend/src/PnvPanel.Application/Common/Messaging/Sender.cs b/backend/src/PnvPanel.Application/Common/Messaging/Sender.cs index 6a706f3..252214d 100644 --- a/backend/src/PnvPanel.Application/Common/Messaging/Sender.cs +++ b/backend/src/PnvPanel.Application/Common/Messaging/Sender.cs @@ -4,22 +4,34 @@ namespace PnvPanel.Application.Common.Messaging; internal sealed class Sender(IServiceProvider serviceProvider) : ISender { - public Task Send(ICommand command, CancellationToken cancellationToken = default) - => Dispatch(command, typeof(ICommandHandler<,>), cancellationToken); + public Task Send( + ICommand command, + CancellationToken cancellationToken = default + ) => Dispatch(command, typeof(ICommandHandler<,>), cancellationToken); - public Task Send(IQuery query, CancellationToken cancellationToken = default) - => Dispatch(query, typeof(IQueryHandler<,>), cancellationToken); + public Task Send( + IQuery query, + CancellationToken cancellationToken = default + ) => Dispatch(query, typeof(IQueryHandler<,>), cancellationToken); - private Task Dispatch(object request, Type handlerOpenType, CancellationToken cancellationToken) + private Task Dispatch( + object request, + Type handlerOpenType, + CancellationToken cancellationToken + ) { var requestType = request.GetType(); var handlerType = handlerOpenType.MakeGenericType(requestType, typeof(TResponse)); - var behaviorType = typeof(IPipelineBehavior<,>).MakeGenericType(requestType, typeof(TResponse)); + var behaviorType = typeof(IPipelineBehavior<,>).MakeGenericType( + requestType, + typeof(TResponse) + ); dynamic handler = serviceProvider.GetRequiredService(handlerType); var behaviors = ((IEnumerable)serviceProvider.GetServices(behaviorType)).Reverse(); - RequestHandlerDelegate pipeline = () => handler.Handle((dynamic)request, cancellationToken); + RequestHandlerDelegate pipeline = () => + handler.Handle((dynamic)request, cancellationToken); foreach (dynamic behavior in behaviors) { diff --git a/backend/src/PnvPanel.Application/Common/Models/Error.cs b/backend/src/PnvPanel.Application/Common/Models/Error.cs index 2487383..8df77ab 100644 --- a/backend/src/PnvPanel.Application/Common/Models/Error.cs +++ b/backend/src/PnvPanel.Application/Common/Models/Error.cs @@ -14,10 +14,21 @@ public sealed record Error(string Code, string Message, ErrorType Type = ErrorTy { public static readonly Error None = new(string.Empty, string.Empty); - public static Error Validation(string code, string message) => new(code, message, ErrorType.Validation); - public static Error NotFound(string code, string message) => new(code, message, ErrorType.NotFound); - public static Error Conflict(string code, string message) => new(code, message, ErrorType.Conflict); - public static Error Unauthorized(string code, string message) => new(code, message, ErrorType.Unauthorized); - public static Error Forbidden(string code, string message) => new(code, message, ErrorType.Forbidden); - public static Error Failure(string code, string message) => new(code, message, ErrorType.Failure); + public static Error Validation(string code, string message) => + new(code, message, ErrorType.Validation); + + public static Error NotFound(string code, string message) => + new(code, message, ErrorType.NotFound); + + public static Error Conflict(string code, string message) => + new(code, message, ErrorType.Conflict); + + public static Error Unauthorized(string code, string message) => + new(code, message, ErrorType.Unauthorized); + + public static Error Forbidden(string code, string message) => + new(code, message, ErrorType.Forbidden); + + public static Error Failure(string code, string message) => + new(code, message, ErrorType.Failure); } diff --git a/backend/src/PnvPanel.Application/Common/Models/PagedListExtensions.cs b/backend/src/PnvPanel.Application/Common/Models/PagedListExtensions.cs index 148e53d..9cce6d9 100644 --- a/backend/src/PnvPanel.Application/Common/Models/PagedListExtensions.cs +++ b/backend/src/PnvPanel.Application/Common/Models/PagedListExtensions.cs @@ -5,10 +5,17 @@ namespace PnvPanel.Application.Common.Models; public static class PagedListExtensions { public static async Task> ToPagedListAsync( - this IQueryable query, int page, int pageSize, CancellationToken cancellationToken) + this IQueryable query, + int page, + int pageSize, + CancellationToken cancellationToken + ) { var total = await query.CountAsync(cancellationToken); - var items = await query.Skip((page - 1) * pageSize).Take(pageSize).ToListAsync(cancellationToken); + var items = await query + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); return new PagedList(items, total, page, pageSize); } } diff --git a/backend/src/PnvPanel.Application/Common/Models/Result.cs b/backend/src/PnvPanel.Application/Common/Models/Result.cs index 533c306..0da6b17 100644 --- a/backend/src/PnvPanel.Application/Common/Models/Result.cs +++ b/backend/src/PnvPanel.Application/Common/Models/Result.cs @@ -17,9 +17,11 @@ public class Result } public static Result Success() => new(true, Error.None); + public static Result Failure(Error error) => new(false, error); public static Result Success(T value) => new(value, true, Error.None); + public static Result Failure(Error error) => new(default, false, error); } @@ -27,11 +29,15 @@ public class Result : Result { private readonly T? _value; - internal Result(T? value, bool isSuccess, Error error) : base(isSuccess, error) => _value = value; + internal Result(T? value, bool isSuccess, Error error) + : base(isSuccess, error) => _value = value; - public T Value => IsSuccess - ? _value! - : throw new InvalidOperationException("Нельзя получить значение неуспешного результата."); + public T Value => + IsSuccess + ? _value! + : throw new InvalidOperationException( + "Нельзя получить значение неуспешного результата." + ); public static implicit operator Result(T value) => Success(value); } diff --git a/backend/src/PnvPanel.Application/Configs/ConfigErrors.cs b/backend/src/PnvPanel.Application/Configs/ConfigErrors.cs index db28c71..2a1c5da 100644 --- a/backend/src/PnvPanel.Application/Configs/ConfigErrors.cs +++ b/backend/src/PnvPanel.Application/Configs/ConfigErrors.cs @@ -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", "Конфиг не найден."); } diff --git a/backend/src/PnvPanel.Application/Configs/Create/CreateVpnConfigCommand.cs b/backend/src/PnvPanel.Application/Configs/Create/CreateVpnConfigCommand.cs index 8a5cdc7..3d87d8b 100644 --- a/backend/src/PnvPanel.Application/Configs/Create/CreateVpnConfigCommand.cs +++ b/backend/src/PnvPanel.Application/Configs/Create/CreateVpnConfigCommand.cs @@ -3,4 +3,6 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Configs.Create; -public sealed record CreateVpnConfigCommand(Guid InboundId, string? Label) : ICommand>, IRequiresActivation; +public sealed record CreateVpnConfigCommand(Guid InboundId, string? Label) + : ICommand>, + IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/Configs/Create/CreateVpnConfigCommandHandler.cs b/backend/src/PnvPanel.Application/Configs/Create/CreateVpnConfigCommandHandler.cs index 05eb9c6..7e775ab 100644 --- a/backend/src/PnvPanel.Application/Configs/Create/CreateVpnConfigCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Configs/Create/CreateVpnConfigCommandHandler.cs @@ -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> + IAppDbContext dbContext, + IIdentityService identityService, + IXuiPanelGateway gateway, + ICurrentUser currentUser +) : ICommandHandler> { - public async Task> Handle(CreateVpnConfigCommand command, CancellationToken cancellationToken) + public async Task> Handle( + CreateVpnConfigCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); @@ -20,7 +26,8 @@ public sealed class CreateVpnConfigCommandHandler( if (profile is null) return Result.Failure(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(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(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(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. /// - private async Task ReserveQuotaSlotAsync(Guid userId, int maxConfigs, VpnConfig config, CancellationToken cancellationToken) + private async Task 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) { diff --git a/backend/src/PnvPanel.Application/Configs/Edit/EditVpnConfigCommand.cs b/backend/src/PnvPanel.Application/Configs/Edit/EditVpnConfigCommand.cs index 6175a57..26ad92e 100644 --- a/backend/src/PnvPanel.Application/Configs/Edit/EditVpnConfigCommand.cs +++ b/backend/src/PnvPanel.Application/Configs/Edit/EditVpnConfigCommand.cs @@ -3,4 +3,6 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Configs.Edit; -public sealed record EditVpnConfigCommand(Guid ConfigId, string? Label) : ICommand>, IRequiresActivation; +public sealed record EditVpnConfigCommand(Guid ConfigId, string? Label) + : ICommand>, + IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/Configs/Edit/EditVpnConfigCommandHandler.cs b/backend/src/PnvPanel.Application/Configs/Edit/EditVpnConfigCommandHandler.cs index a4e5407..0226c8c 100644 --- a/backend/src/PnvPanel.Application/Configs/Edit/EditVpnConfigCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Configs/Edit/EditVpnConfigCommandHandler.cs @@ -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> +public sealed class EditVpnConfigCommandHandler( + IAppDbContext dbContext, + IXuiPanelGateway gateway, + ICurrentUser currentUser +) : ICommandHandler> { - public async Task> Handle(EditVpnConfigCommand command, CancellationToken cancellationToken) + public async Task> Handle( + EditVpnConfigCommand 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); - 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(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 + ); } } diff --git a/backend/src/PnvPanel.Application/Configs/GetConfigLink/GetConfigLinkQuery.cs b/backend/src/PnvPanel.Application/Configs/GetConfigLink/GetConfigLinkQuery.cs index 9ac9e21..17463cb 100644 --- a/backend/src/PnvPanel.Application/Configs/GetConfigLink/GetConfigLinkQuery.cs +++ b/backend/src/PnvPanel.Application/Configs/GetConfigLink/GetConfigLinkQuery.cs @@ -3,7 +3,9 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Configs.GetConfigLink; -public sealed record GetConfigLinkQuery(Guid ConfigId) : IQuery>, IRequiresActivation; +public sealed record GetConfigLinkQuery(Guid ConfigId) + : IQuery>, + IRequiresActivation; /// SubscriptionToken — Api-слой строит из него абсолютный URL (знает scheme/host запроса). public sealed record ConfigLinkDto(string ConnectionString, string SubscriptionToken); diff --git a/backend/src/PnvPanel.Application/Configs/GetConfigLink/GetConfigLinkQueryHandler.cs b/backend/src/PnvPanel.Application/Configs/GetConfigLink/GetConfigLinkQueryHandler.cs index 542ddeb..e23fdb8 100644 --- a/backend/src/PnvPanel.Application/Configs/GetConfigLink/GetConfigLinkQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Configs/GetConfigLink/GetConfigLinkQueryHandler.cs @@ -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> +public sealed class GetConfigLinkQueryHandler( + IAppDbContext dbContext, + IXuiPanelGateway gateway, + ICurrentUser currentUser +) : IQueryHandler> { - public async Task> Handle(GetConfigLinkQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetConfigLinkQuery query, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(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(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(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(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(linkResult.Error); diff --git a/backend/src/PnvPanel.Application/Configs/GetMyConfigs/GetMyConfigsQueryHandler.cs b/backend/src/PnvPanel.Application/Configs/GetMyConfigs/GetMyConfigsQueryHandler.cs index d97b66e..0f2303d 100644 --- a/backend/src/PnvPanel.Application/Configs/GetMyConfigs/GetMyConfigsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Configs/GetMyConfigs/GetMyConfigsQueryHandler.cs @@ -7,10 +7,16 @@ using PnvPanel.Domain.Configs; namespace PnvPanel.Application.Configs.GetMyConfigs; -public sealed class GetMyConfigsQueryHandler(IAppDbContext dbContext, IIdentityService identityService, ICurrentUser currentUser) - : IQueryHandler> +public sealed class GetMyConfigsQueryHandler( + IAppDbContext dbContext, + IIdentityService identityService, + ICurrentUser currentUser +) : IQueryHandler> { - public async Task> Handle(GetMyConfigsQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetMyConfigsQuery query, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); @@ -19,9 +25,15 @@ public sealed class GetMyConfigsQueryHandler(IAppDbContext dbContext, IIdentityS if (profile is null) return Result.Failure(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); diff --git a/backend/src/PnvPanel.Application/Configs/GetMySubscription/GetMySubscriptionQuery.cs b/backend/src/PnvPanel.Application/Configs/GetMySubscription/GetMySubscriptionQuery.cs index b9fd5b5..f8bf485 100644 --- a/backend/src/PnvPanel.Application/Configs/GetMySubscription/GetMySubscriptionQuery.cs +++ b/backend/src/PnvPanel.Application/Configs/GetMySubscription/GetMySubscriptionQuery.cs @@ -3,7 +3,9 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Configs.GetMySubscription; -public sealed record GetMySubscriptionQuery : IQuery>, IRequiresActivation; +public sealed record GetMySubscriptionQuery + : IQuery>, + IRequiresActivation; /// SubscriptionToken — Api-слой строит из него абсолютный URL (знает scheme/host запроса), см. GetConfigLinkQuery. public sealed record MySubscriptionDto(string SubscriptionToken); diff --git a/backend/src/PnvPanel.Application/Configs/GetMySubscription/GetMySubscriptionQueryHandler.cs b/backend/src/PnvPanel.Application/Configs/GetMySubscription/GetMySubscriptionQueryHandler.cs index 509e2a9..7b6309a 100644 --- a/backend/src/PnvPanel.Application/Configs/GetMySubscription/GetMySubscriptionQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Configs/GetMySubscription/GetMySubscriptionQueryHandler.cs @@ -5,10 +5,15 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Configs.GetMySubscription; -public sealed class GetMySubscriptionQueryHandler(IIdentityService identityService, ICurrentUser currentUser) - : IQueryHandler> +public sealed class GetMySubscriptionQueryHandler( + IIdentityService identityService, + ICurrentUser currentUser +) : IQueryHandler> { - public async Task> Handle(GetMySubscriptionQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetMySubscriptionQuery query, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); diff --git a/backend/src/PnvPanel.Application/Configs/ListAvailableInbounds/ListAvailableInboundsQuery.cs b/backend/src/PnvPanel.Application/Configs/ListAvailableInbounds/ListAvailableInboundsQuery.cs index 27e1d5d..8d0049b 100644 --- a/backend/src/PnvPanel.Application/Configs/ListAvailableInbounds/ListAvailableInboundsQuery.cs +++ b/backend/src/PnvPanel.Application/Configs/ListAvailableInbounds/ListAvailableInboundsQuery.cs @@ -4,7 +4,9 @@ using PnvPanel.Domain.Inbounds; namespace PnvPanel.Application.Configs.ListAvailableInbounds; -public sealed record ListAvailableInboundsQuery : IQuery>>, IRequiresActivation; +public sealed record ListAvailableInboundsQuery + : IQuery>>, + IRequiresActivation; /// Витринная карточка инбаунда для выбора при создании конфига — без деталей 3x-ui. public sealed record AvailableInboundDto(Guid InboundId, string DisplayName, VpnProtocol Protocol); diff --git a/backend/src/PnvPanel.Application/Configs/ListAvailableInbounds/ListAvailableInboundsQueryHandler.cs b/backend/src/PnvPanel.Application/Configs/ListAvailableInbounds/ListAvailableInboundsQueryHandler.cs index 387841c..cedb6e6 100644 --- a/backend/src/PnvPanel.Application/Configs/ListAvailableInbounds/ListAvailableInboundsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Configs/ListAvailableInbounds/ListAvailableInboundsQueryHandler.cs @@ -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>> +public sealed class ListAvailableInboundsQueryHandler( + IAppDbContext dbContext, + IIdentityService identityService, + ICurrentUser currentUser +) : IQueryHandler>> { - public async Task>> Handle(ListAvailableInboundsQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListAvailableInboundsQuery query, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure>(AuthErrors.Unauthorized); @@ -18,10 +24,18 @@ public sealed class ListAvailableInboundsQueryHandler(IAppDbContext dbContext, I if (profile is null) return Result.Failure>(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); diff --git a/backend/src/PnvPanel.Application/Configs/Revoke/RevokeVpnConfigCommandHandler.cs b/backend/src/PnvPanel.Application/Configs/Revoke/RevokeVpnConfigCommandHandler.cs index 26cb40f..4d64b60 100644 --- a/backend/src/PnvPanel.Application/Configs/Revoke/RevokeVpnConfigCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Configs/Revoke/RevokeVpnConfigCommandHandler.cs @@ -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 + IAppDbContext dbContext, + IXuiPanelGateway gateway, + IRealtimeNotifier notifier, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(RevokeVpnConfigCommand command, CancellationToken cancellationToken) + public async Task 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(); } diff --git a/backend/src/PnvPanel.Application/Configs/Rotate/RotateVpnConfigCommand.cs b/backend/src/PnvPanel.Application/Configs/Rotate/RotateVpnConfigCommand.cs index 21ee8b0..74533cb 100644 --- a/backend/src/PnvPanel.Application/Configs/Rotate/RotateVpnConfigCommand.cs +++ b/backend/src/PnvPanel.Application/Configs/Rotate/RotateVpnConfigCommand.cs @@ -3,4 +3,6 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Configs.Rotate; -public sealed record RotateVpnConfigCommand(Guid ConfigId) : ICommand>, IRequiresActivation; +public sealed record RotateVpnConfigCommand(Guid ConfigId) + : ICommand>, + IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/Configs/Rotate/RotateVpnConfigCommandHandler.cs b/backend/src/PnvPanel.Application/Configs/Rotate/RotateVpnConfigCommandHandler.cs index 380e85a..2f6f99f 100644 --- a/backend/src/PnvPanel.Application/Configs/Rotate/RotateVpnConfigCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Configs/Rotate/RotateVpnConfigCommandHandler.cs @@ -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> + IAppDbContext dbContext, + IXuiPanelGateway gateway, + IIdentityService identityService, + ICurrentUser currentUser +) : ICommandHandler> { - public async Task> Handle(RotateVpnConfigCommand command, CancellationToken cancellationToken) + public async Task> Handle( + RotateVpnConfigCommand 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.Active) return Result.Failure(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(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(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(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)); } diff --git a/backend/src/PnvPanel.Application/Configs/VpnConfigDto.cs b/backend/src/PnvPanel.Application/Configs/VpnConfigDto.cs index aaa8053..a85ec48 100644 --- a/backend/src/PnvPanel.Application/Configs/VpnConfigDto.cs +++ b/backend/src/PnvPanel.Application/Configs/VpnConfigDto.cs @@ -10,10 +10,29 @@ namespace PnvPanel.Application.Configs; /// (сверка конфига с записью в панели у админа). /// 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 + ); } diff --git a/backend/src/PnvPanel.Application/DependencyInjection.cs b/backend/src/PnvPanel.Application/DependencyInjection.cs index 5786ca5..7c8109e 100644 --- a/backend/src/PnvPanel.Application/DependencyInjection.cs +++ b/backend/src/PnvPanel.Application/DependencyInjection.cs @@ -31,13 +31,20 @@ public static class DependencyInjection return services; } - private static void RegisterClosedGeneric(IServiceCollection services, Assembly assembly, Type openInterface) + private static void RegisterClosedGeneric( + IServiceCollection services, + Assembly assembly, + Type openInterface + ) { - var implementations = assembly.GetTypes() + var implementations = assembly + .GetTypes() .Where(t => t is { IsClass: true, IsAbstract: false }) - .SelectMany(t => t.GetInterfaces() - .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == openInterface) - .Select(i => (Service: i, Implementation: t))); + .SelectMany(t => + t.GetInterfaces() + .Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == openInterface) + .Select(i => (Service: i, Implementation: t)) + ); foreach (var (service, implementation) in implementations) services.AddScoped(service, implementation); diff --git a/backend/src/PnvPanel.Application/News/ListNewsQuery.cs b/backend/src/PnvPanel.Application/News/ListNewsQuery.cs index 611667f..2313e71 100644 --- a/backend/src/PnvPanel.Application/News/ListNewsQuery.cs +++ b/backend/src/PnvPanel.Application/News/ListNewsQuery.cs @@ -3,4 +3,6 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.News; -public sealed record ListNewsQuery(int Page, int PageSize) : IQuery>>, IRequiresActivation; +public sealed record ListNewsQuery(int Page, int PageSize) + : IQuery>>, + IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/News/ListNewsQueryHandler.cs b/backend/src/PnvPanel.Application/News/ListNewsQueryHandler.cs index 386eb96..eed8d8f 100644 --- a/backend/src/PnvPanel.Application/News/ListNewsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/News/ListNewsQueryHandler.cs @@ -5,14 +5,19 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.News; -public sealed class ListNewsQueryHandler(IAppDbContext dbContext) : IQueryHandler>> +public sealed class ListNewsQueryHandler(IAppDbContext dbContext) + : IQueryHandler>> { - public async Task>> Handle(ListNewsQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListNewsQuery query, + CancellationToken cancellationToken + ) { var page = query.Page <= 0 ? 1 : query.Page; var pageSize = query.PageSize is <= 0 or > 100 ? 20 : query.PageSize; - var result = await dbContext.NewsPosts.AsNoTracking() + var result = await dbContext + .NewsPosts.AsNoTracking() .OrderByDescending(p => p.CreatedAt) .Select(p => new NewsPostDto(p.Id, p.Title, p.Body, p.CreatedAt, p.UpdatedAt)) .ToPagedListAsync(page, pageSize, cancellationToken); diff --git a/backend/src/PnvPanel.Application/News/NewsPostDto.cs b/backend/src/PnvPanel.Application/News/NewsPostDto.cs index 2bf3ff2..47abb2c 100644 --- a/backend/src/PnvPanel.Application/News/NewsPostDto.cs +++ b/backend/src/PnvPanel.Application/News/NewsPostDto.cs @@ -2,7 +2,14 @@ using PnvPanel.Domain.News; namespace PnvPanel.Application.News; -public sealed record NewsPostDto(Guid Id, string Title, string Body, DateTimeOffset CreatedAt, DateTimeOffset? UpdatedAt) +public sealed record NewsPostDto( + Guid Id, + string Title, + string Body, + DateTimeOffset CreatedAt, + DateTimeOffset? UpdatedAt +) { - public static NewsPostDto FromDomain(NewsPost post) => new(post.Id, post.Title, post.Body, post.CreatedAt, post.UpdatedAt); + public static NewsPostDto FromDomain(NewsPost post) => + new(post.Id, post.Title, post.Body, post.CreatedAt, post.UpdatedAt); } diff --git a/backend/src/PnvPanel.Application/PnvPanel.Application.csproj b/backend/src/PnvPanel.Application/PnvPanel.Application.csproj index 80636e6..213c57b 100644 --- a/backend/src/PnvPanel.Application/PnvPanel.Application.csproj +++ b/backend/src/PnvPanel.Application/PnvPanel.Application.csproj @@ -1,5 +1,4 @@  - @@ -17,5 +16,4 @@ enable enable - diff --git a/backend/src/PnvPanel.Application/Subscriptions/GetConfigSubscriptionQueryHandler.cs b/backend/src/PnvPanel.Application/Subscriptions/GetConfigSubscriptionQueryHandler.cs index eaf9da1..70b0ac1 100644 --- a/backend/src/PnvPanel.Application/Subscriptions/GetConfigSubscriptionQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Subscriptions/GetConfigSubscriptionQueryHandler.cs @@ -6,17 +6,31 @@ using PnvPanel.Domain.Configs; namespace PnvPanel.Application.Subscriptions; -public sealed class GetConfigSubscriptionQueryHandler(IAppDbContext dbContext, IXuiPanelGateway gateway) - : IQueryHandler> +public sealed class GetConfigSubscriptionQueryHandler( + IAppDbContext dbContext, + IXuiPanelGateway gateway +) : IQueryHandler> { - public async Task> Handle(GetConfigSubscriptionQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetConfigSubscriptionQuery query, + CancellationToken cancellationToken + ) { - var config = await dbContext.VpnConfigs.AsNoTracking() - .FirstOrDefaultAsync(c => c.SubscriptionToken == query.Token && c.Status == ConfigStatus.Active, cancellationToken); + var config = await dbContext + .VpnConfigs.AsNoTracking() + .FirstOrDefaultAsync( + c => c.SubscriptionToken == query.Token && c.Status == ConfigStatus.Active, + cancellationToken + ); if (config is null) return Result.Failure(SubscriptionErrors.NotFound); - return await SubscriptionAssembler.BuildAsync([config], dbContext, gateway, cancellationToken); + return await SubscriptionAssembler.BuildAsync( + [config], + dbContext, + gateway, + cancellationToken + ); } } diff --git a/backend/src/PnvPanel.Application/Subscriptions/GetUserSubscriptionQueryHandler.cs b/backend/src/PnvPanel.Application/Subscriptions/GetUserSubscriptionQueryHandler.cs index bd64411..771f422 100644 --- a/backend/src/PnvPanel.Application/Subscriptions/GetUserSubscriptionQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Subscriptions/GetUserSubscriptionQueryHandler.cs @@ -6,19 +6,34 @@ using PnvPanel.Domain.Configs; namespace PnvPanel.Application.Subscriptions; -public sealed class GetUserSubscriptionQueryHandler(IAppDbContext dbContext, IIdentityService identityService, IXuiPanelGateway gateway) - : IQueryHandler> +public sealed class GetUserSubscriptionQueryHandler( + IAppDbContext dbContext, + IIdentityService identityService, + IXuiPanelGateway gateway +) : IQueryHandler> { - public async Task> Handle(GetUserSubscriptionQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetUserSubscriptionQuery query, + CancellationToken cancellationToken + ) { - var userId = await identityService.FindUserIdBySubscriptionTokenAsync(query.Token, cancellationToken); + var userId = await identityService.FindUserIdBySubscriptionTokenAsync( + query.Token, + cancellationToken + ); if (userId is null) return Result.Failure(SubscriptionErrors.NotFound); - var configs = await dbContext.VpnConfigs.AsNoTracking() + var configs = await dbContext + .VpnConfigs.AsNoTracking() .Where(c => c.UserId == userId && c.Status == ConfigStatus.Active) .ToListAsync(cancellationToken); - return await SubscriptionAssembler.BuildAsync(configs, dbContext, gateway, cancellationToken); + return await SubscriptionAssembler.BuildAsync( + configs, + dbContext, + gateway, + cancellationToken + ); } } diff --git a/backend/src/PnvPanel.Application/Subscriptions/SubscriptionAssembler.cs b/backend/src/PnvPanel.Application/Subscriptions/SubscriptionAssembler.cs index 211658f..1022bd8 100644 --- a/backend/src/PnvPanel.Application/Subscriptions/SubscriptionAssembler.cs +++ b/backend/src/PnvPanel.Application/Subscriptions/SubscriptionAssembler.cs @@ -9,7 +9,11 @@ namespace PnvPanel.Application.Subscriptions; internal static class SubscriptionAssembler { public static async Task> BuildAsync( - IReadOnlyList configs, IAppDbContext dbContext, IXuiPanelGateway gateway, CancellationToken cancellationToken) + IReadOnlyList configs, + IAppDbContext dbContext, + IXuiPanelGateway gateway, + CancellationToken cancellationToken + ) { var lines = new List(); long usedUp = 0; @@ -18,18 +22,26 @@ internal static class SubscriptionAssembler foreach (var config in configs) { - var inbound = await dbContext.Inbounds.AsNoTracking() + var inbound = await dbContext + .Inbounds.AsNoTracking() .FirstOrDefaultAsync(i => i.Id == config.InboundId, cancellationToken); if (inbound is null) continue; - var node = await dbContext.Nodes.AsNoTracking() + var node = await dbContext + .Nodes.AsNoTracking() .FirstOrDefaultAsync(n => n.Id == inbound.NodeId, cancellationToken); if (node is null || !node.IsEnabled) continue; 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) lines.Add(linkResult.Value); diff --git a/backend/src/PnvPanel.Application/Subscriptions/SubscriptionDto.cs b/backend/src/PnvPanel.Application/Subscriptions/SubscriptionDto.cs index cbe3743..c6d4ba5 100644 --- a/backend/src/PnvPanel.Application/Subscriptions/SubscriptionDto.cs +++ b/backend/src/PnvPanel.Application/Subscriptions/SubscriptionDto.cs @@ -1,4 +1,8 @@ namespace PnvPanel.Application.Subscriptions; public sealed record SubscriptionDto( - IReadOnlyList ConnectionStrings, long UsedUpBytes, long UsedDownBytes, DateTimeOffset? ExpiresAt); + IReadOnlyList ConnectionStrings, + long UsedUpBytes, + long UsedDownBytes, + DateTimeOffset? ExpiresAt +); diff --git a/backend/src/PnvPanel.Application/Subscriptions/SubscriptionErrors.cs b/backend/src/PnvPanel.Application/Subscriptions/SubscriptionErrors.cs index ee80330..7774937 100644 --- a/backend/src/PnvPanel.Application/Subscriptions/SubscriptionErrors.cs +++ b/backend/src/PnvPanel.Application/Subscriptions/SubscriptionErrors.cs @@ -4,5 +4,8 @@ namespace PnvPanel.Application.Subscriptions; public static class SubscriptionErrors { - public static readonly Error NotFound = Error.NotFound("Subscription.NotFound", "Подписка не найдена."); + public static readonly Error NotFound = Error.NotFound( + "Subscription.NotFound", + "Подписка не найдена." + ); } diff --git a/backend/src/PnvPanel.Application/Support/AddComment/AddTicketCommentCommand.cs b/backend/src/PnvPanel.Application/Support/AddComment/AddTicketCommentCommand.cs index 51d78f9..864704e 100644 --- a/backend/src/PnvPanel.Application/Support/AddComment/AddTicketCommentCommand.cs +++ b/backend/src/PnvPanel.Application/Support/AddComment/AddTicketCommentCommand.cs @@ -5,5 +5,8 @@ namespace PnvPanel.Application.Support.AddComment; /// Общая команда для реплики владельца тикета и админа — используется и с /// /api/support/tickets/{id}/comments, и с /api/admin/support/tickets/{id}/comments. -public sealed record AddTicketCommentCommand(Guid TicketId, string Body, IReadOnlyList Attachments) - : ICommand>, IRequiresActivation; +public sealed record AddTicketCommentCommand( + Guid TicketId, + string Body, + IReadOnlyList Attachments +) : ICommand>, IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/Support/AddComment/AddTicketCommentCommandHandler.cs b/backend/src/PnvPanel.Application/Support/AddComment/AddTicketCommentCommandHandler.cs index 2c39d56..2118c04 100644 --- a/backend/src/PnvPanel.Application/Support/AddComment/AddTicketCommentCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Support/AddComment/AddTicketCommentCommandHandler.cs @@ -8,16 +8,25 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Support.AddComment; public sealed class AddTicketCommentCommandHandler( - IAppDbContext dbContext, IIdentityService identityService, IFileStorage fileStorage, - IRealtimeNotifier notifier, ICurrentUser currentUser) - : ICommandHandler> + IAppDbContext dbContext, + IIdentityService identityService, + IFileStorage fileStorage, + IRealtimeNotifier notifier, + ICurrentUser currentUser +) : ICommandHandler> { - public async Task> Handle(AddTicketCommentCommand command, CancellationToken cancellationToken) + public async Task> Handle( + AddTicketCommentCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); - var ticket = await dbContext.SupportTickets.FirstOrDefaultAsync(t => t.Id == command.TicketId, cancellationToken); + var ticket = await dbContext.SupportTickets.FirstOrDefaultAsync( + t => t.Id == command.TicketId, + cancellationToken + ); if (ticket is null) return Result.Failure(SupportErrors.NotFound); @@ -38,7 +47,12 @@ public sealed class AddTicketCommentCommandHandler( var comment = TicketComment.Create(ticket.Id, userId, command.Body); dbContext.TicketComments.Add(comment); - var attachments = await TicketAttachmentPersistence.SaveAllAsync(fileStorage, comment.Id, command.Attachments, cancellationToken); + var attachments = await TicketAttachmentPersistence.SaveAllAsync( + fileStorage, + comment.Id, + command.Attachments, + cancellationToken + ); dbContext.TicketAttachments.AddRange(attachments); // Пушим автору тикета, только если комментирует не он сам (иначе он и так это видит у себя). @@ -47,8 +61,15 @@ public sealed class AddTicketCommentCommandHandler( var userName = currentUser.UserName ?? userId.ToString(); var dto = new TicketCommentDto( - comment.Id, userId, userName, comment.Body, comment.CreatedAt, - attachments.Select(a => new TicketAttachmentDto(a.Id, a.FileName, a.ContentType, a.SizeBytes)).ToList()); + comment.Id, + userId, + userName, + comment.Body, + comment.CreatedAt, + attachments + .Select(a => new TicketAttachmentDto(a.Id, a.FileName, a.ContentType, a.SizeBytes)) + .ToList() + ); return Result.Success(dto); } diff --git a/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommand.cs b/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommand.cs index 17b7350..16a1868 100644 --- a/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommand.cs +++ b/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommand.cs @@ -3,5 +3,7 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Support.CreateBugReport; -public sealed record CreateBugReportTicketCommand(string Message, IReadOnlyList Attachments) - : ICommand>, IRequiresActivation; +public sealed record CreateBugReportTicketCommand( + string Message, + IReadOnlyList Attachments +) : ICommand>, IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommandHandler.cs b/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommandHandler.cs index ca3076b..943d22d 100644 --- a/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommandHandler.cs @@ -7,11 +7,17 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Support.CreateBugReport; public sealed class CreateBugReportTicketCommandHandler( - IAppDbContext dbContext, IFileStorage fileStorage, IRealtimeNotifier notifier, - ITelegramNotifier telegramNotifier, ICurrentUser currentUser) - : ICommandHandler> + IAppDbContext dbContext, + IFileStorage fileStorage, + IRealtimeNotifier notifier, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser +) : ICommandHandler> { - public async Task> Handle(CreateBugReportTicketCommand command, CancellationToken cancellationToken) + public async Task> Handle( + CreateBugReportTicketCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); @@ -25,21 +31,55 @@ public sealed class CreateBugReportTicketCommandHandler( var comment = TicketComment.Create(ticket.Id, userId, command.Message); dbContext.TicketComments.Add(comment); - var attachments = await TicketAttachmentPersistence.SaveAllAsync(fileStorage, comment.Id, command.Attachments, cancellationToken); + var attachments = await TicketAttachmentPersistence.SaveAllAsync( + fileStorage, + comment.Id, + command.Attachments, + cancellationToken + ); dbContext.TicketAttachments.AddRange(attachments); var userName = currentUser.UserName ?? userId.ToString(); - await notifier.NotifyTicketCreatedAsync(ticket.Id, userId, userName, ticket.Type, cancellationToken); - await telegramNotifier.NotifyAdminsBugReportCreatedAsync(ticket.Id, userName, command.Message, cancellationToken); + await notifier.NotifyTicketCreatedAsync( + ticket.Id, + userId, + userName, + ticket.Type, + cancellationToken + ); + await telegramNotifier.NotifyAdminsBugReportCreatedAsync( + ticket.Id, + userName, + command.Message, + cancellationToken + ); var commentDto = new TicketCommentDto( - comment.Id, userId, userName, comment.Body, comment.CreatedAt, - attachments.Select(a => new TicketAttachmentDto(a.Id, a.FileName, a.ContentType, a.SizeBytes)).ToList()); + comment.Id, + userId, + userName, + comment.Body, + comment.CreatedAt, + attachments + .Select(a => new TicketAttachmentDto(a.Id, a.FileName, a.ContentType, a.SizeBytes)) + .ToList() + ); var dto = new TicketDetailDto( - ticket.Id, ticket.UserId, userName, ticket.Type, ticket.Status, - null, null, null, null, null, ticket.CreatedAt, [commentDto]); + ticket.Id, + ticket.UserId, + userName, + ticket.Type, + ticket.Status, + null, + null, + null, + null, + null, + ticket.CreatedAt, + [commentDto] + ); return Result.Success(dto); } diff --git a/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommandValidator.cs b/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommandValidator.cs index 0235bc1..2374c44 100644 --- a/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommandValidator.cs +++ b/backend/src/PnvPanel.Application/Support/CreateBugReport/CreateBugReportTicketCommandValidator.cs @@ -2,7 +2,8 @@ using FluentValidation; namespace PnvPanel.Application.Support.CreateBugReport; -public sealed class CreateBugReportTicketCommandValidator : AbstractValidator +public sealed class CreateBugReportTicketCommandValidator + : AbstractValidator { public CreateBugReportTicketCommandValidator() { diff --git a/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommand.cs b/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommand.cs index 954c9c7..54f01b1 100644 --- a/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommand.cs +++ b/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommand.cs @@ -6,5 +6,9 @@ namespace PnvPanel.Application.Support.CreateRoleRequest; /// Ровно один из вариантов: ExistingRoleId, либо NewRoleName+NewRoleMaxConfigs+NewRoleMaxIpLimit /// (проверяется валидатором). public sealed record CreateRoleRequestTicketCommand( - Guid? ExistingRoleId, string? NewRoleName, int? NewRoleMaxConfigs, int? NewRoleMaxIpLimit, string Justification) - : ICommand>, IRequiresActivation; + Guid? ExistingRoleId, + string? NewRoleName, + int? NewRoleMaxConfigs, + int? NewRoleMaxIpLimit, + string Justification +) : ICommand>, IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommandHandler.cs b/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommandHandler.cs index e99a46b..079599e 100644 --- a/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommandHandler.cs @@ -8,22 +8,32 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Support.CreateRoleRequest; public sealed class CreateRoleRequestTicketCommandHandler( - IAppDbContext dbContext, IRoleService roleService, IRealtimeNotifier notifier, - ITelegramNotifier telegramNotifier, ICurrentUser currentUser) - : ICommandHandler> + IAppDbContext dbContext, + IRoleService roleService, + IRealtimeNotifier notifier, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser +) : ICommandHandler> { // Совпадает со значением Infrastructure.Identity.RoleNames.Admin — Application не может ссылаться // на Infrastructure (направление зависимостей), поэтому системное имя роли продублировано здесь. private const string AdminRoleName = "admin"; - public async Task> Handle(CreateRoleRequestTicketCommand command, CancellationToken cancellationToken) + public async Task> Handle( + CreateRoleRequestTicketCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); var hasPending = await dbContext.SupportTickets.AnyAsync( - t => t.UserId == userId && t.Type == TicketType.RoleRequest && t.Status == TicketStatus.Open, - cancellationToken); + t => + t.UserId == userId + && t.Type == TicketType.RoleRequest + && t.Status == TicketStatus.Open, + cancellationToken + ); if (hasPending) return Result.Failure(SupportErrors.RoleRequestAlreadyPending); @@ -46,7 +56,11 @@ public sealed class CreateRoleRequestTicketCommandHandler( else { ticket = SupportTicket.CreateRoleRequestForNewRole( - userId, command.NewRoleName!, command.NewRoleMaxConfigs!.Value, command.NewRoleMaxIpLimit!.Value); + userId, + command.NewRoleName!, + command.NewRoleMaxConfigs!.Value, + command.NewRoleMaxIpLimit!.Value + ); } dbContext.SupportTickets.Add(ticket); @@ -55,18 +69,48 @@ public sealed class CreateRoleRequestTicketCommandHandler( dbContext.TicketComments.Add(comment); var userName = currentUser.UserName ?? userId.ToString(); - var roleDescription = requestedRoleName + var roleDescription = + requestedRoleName ?? $"новая роль «{command.NewRoleName}» (конфигов: {command.NewRoleMaxConfigs}, IP: {command.NewRoleMaxIpLimit})"; - await notifier.NotifyTicketCreatedAsync(ticket.Id, userId, userName, ticket.Type, cancellationToken); - await telegramNotifier.NotifyAdminsRoleRequestCreatedAsync(ticket.Id, userName, roleDescription, command.Justification, cancellationToken); + await notifier.NotifyTicketCreatedAsync( + ticket.Id, + userId, + userName, + ticket.Type, + cancellationToken + ); + await telegramNotifier.NotifyAdminsRoleRequestCreatedAsync( + ticket.Id, + userName, + roleDescription, + command.Justification, + cancellationToken + ); - var commentDto = new TicketCommentDto(comment.Id, userId, userName, comment.Body, comment.CreatedAt, []); + var commentDto = new TicketCommentDto( + comment.Id, + userId, + userName, + comment.Body, + comment.CreatedAt, + [] + ); var dto = new TicketDetailDto( - ticket.Id, ticket.UserId, userName, ticket.Type, ticket.Status, - ticket.RequestedRoleId, requestedRoleName, ticket.ProposedRoleName, ticket.ProposedMaxConfigs, - ticket.ProposedMaxIpLimit, ticket.CreatedAt, [commentDto]); + ticket.Id, + ticket.UserId, + userName, + ticket.Type, + ticket.Status, + ticket.RequestedRoleId, + requestedRoleName, + ticket.ProposedRoleName, + ticket.ProposedMaxConfigs, + ticket.ProposedMaxIpLimit, + ticket.CreatedAt, + [commentDto] + ); return Result.Success(dto); } diff --git a/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommandValidator.cs b/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommandValidator.cs index f097b92..7003f54 100644 --- a/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommandValidator.cs +++ b/backend/src/PnvPanel.Application/Support/CreateRoleRequest/CreateRoleRequestTicketCommandValidator.cs @@ -2,7 +2,8 @@ using FluentValidation; namespace PnvPanel.Application.Support.CreateRoleRequest; -public sealed class CreateRoleRequestTicketCommandValidator : AbstractValidator +public sealed class CreateRoleRequestTicketCommandValidator + : AbstractValidator { public CreateRoleRequestTicketCommandValidator() { @@ -10,21 +11,28 @@ public sealed class CreateRoleRequestTicketCommandValidator : AbstractValidator< RuleFor(x => x) .Must(HaveExactlyOnePayload) - .WithMessage("Укажите либо существующую роль, либо параметры новой (не оба варианта и не ни одного)."); + .WithMessage( + "Укажите либо существующую роль, либо параметры новой (не оба варианта и не ни одного)." + ); - When(x => x.ExistingRoleId is null, () => - { - RuleFor(x => x.NewRoleName).NotEmpty().MaximumLength(100); - RuleFor(x => x.NewRoleMaxConfigs).NotNull().GreaterThanOrEqualTo(-1); - RuleFor(x => x.NewRoleMaxIpLimit).NotNull().GreaterThanOrEqualTo(-1); - }); + When( + x => x.ExistingRoleId is null, + () => + { + RuleFor(x => x.NewRoleName).NotEmpty().MaximumLength(100); + RuleFor(x => x.NewRoleMaxConfigs).NotNull().GreaterThanOrEqualTo(-1); + RuleFor(x => x.NewRoleMaxIpLimit).NotNull().GreaterThanOrEqualTo(-1); + } + ); } private static bool HaveExactlyOnePayload(CreateRoleRequestTicketCommand command) { var hasExisting = command.ExistingRoleId is not null; - var hasNew = !string.IsNullOrWhiteSpace(command.NewRoleName) - && command.NewRoleMaxConfigs is not null && command.NewRoleMaxIpLimit is not null; + var hasNew = + !string.IsNullOrWhiteSpace(command.NewRoleName) + && command.NewRoleMaxConfigs is not null + && command.NewRoleMaxIpLimit is not null; return hasExisting ^ hasNew; } diff --git a/backend/src/PnvPanel.Application/Support/GetAttachment/GetTicketAttachmentQuery.cs b/backend/src/PnvPanel.Application/Support/GetAttachment/GetTicketAttachmentQuery.cs index 36c716f..93f5640 100644 --- a/backend/src/PnvPanel.Application/Support/GetAttachment/GetTicketAttachmentQuery.cs +++ b/backend/src/PnvPanel.Application/Support/GetAttachment/GetTicketAttachmentQuery.cs @@ -3,4 +3,6 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Support.GetAttachment; -public sealed record GetTicketAttachmentQuery(Guid AttachmentId) : IQuery>, IRequiresActivation; +public sealed record GetTicketAttachmentQuery(Guid AttachmentId) + : IQuery>, + IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/Support/GetAttachment/GetTicketAttachmentQueryHandler.cs b/backend/src/PnvPanel.Application/Support/GetAttachment/GetTicketAttachmentQueryHandler.cs index 94d5c9b..f86dcf8 100644 --- a/backend/src/PnvPanel.Application/Support/GetAttachment/GetTicketAttachmentQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Support/GetAttachment/GetTicketAttachmentQueryHandler.cs @@ -7,10 +7,16 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Support.GetAttachment; public sealed class GetTicketAttachmentQueryHandler( - IAppDbContext dbContext, IIdentityService identityService, IFileStorage fileStorage, ICurrentUser currentUser) - : IQueryHandler> + IAppDbContext dbContext, + IIdentityService identityService, + IFileStorage fileStorage, + ICurrentUser currentUser +) : IQueryHandler> { - public async Task> Handle(GetTicketAttachmentQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetTicketAttachmentQuery query, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); @@ -30,10 +36,19 @@ public sealed class GetTicketAttachmentQueryHandler( if (info.TicketUserId != userId && !TicketAuthorization.IsAdmin(profile)) return Result.Failure(SupportErrors.AttachmentNotFound); - var stream = await fileStorage.OpenReadAsync(info.Attachment.StoredFileName, cancellationToken); + var stream = await fileStorage.OpenReadAsync( + info.Attachment.StoredFileName, + cancellationToken + ); if (stream is null) return Result.Failure(SupportErrors.AttachmentNotFound); - return Result.Success(new TicketAttachmentContent(stream, info.Attachment.ContentType, info.Attachment.FileName)); + return Result.Success( + new TicketAttachmentContent( + stream, + info.Attachment.ContentType, + info.Attachment.FileName + ) + ); } } diff --git a/backend/src/PnvPanel.Application/Support/GetTicket/GetTicketQuery.cs b/backend/src/PnvPanel.Application/Support/GetTicket/GetTicketQuery.cs index 25d85f6..cd90948 100644 --- a/backend/src/PnvPanel.Application/Support/GetTicket/GetTicketQuery.cs +++ b/backend/src/PnvPanel.Application/Support/GetTicket/GetTicketQuery.cs @@ -3,4 +3,6 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Support.GetTicket; -public sealed record GetTicketQuery(Guid TicketId) : IQuery>, IRequiresActivation; +public sealed record GetTicketQuery(Guid TicketId) + : IQuery>, + IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/Support/GetTicket/GetTicketQueryHandler.cs b/backend/src/PnvPanel.Application/Support/GetTicket/GetTicketQueryHandler.cs index 27e2f0f..0d23e2f 100644 --- a/backend/src/PnvPanel.Application/Support/GetTicket/GetTicketQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Support/GetTicket/GetTicketQueryHandler.cs @@ -7,20 +7,36 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Support.GetTicket; public sealed class GetTicketQueryHandler( - IAppDbContext dbContext, IIdentityService identityService, IRoleService roleService, ICurrentUser currentUser) - : IQueryHandler> + IAppDbContext dbContext, + IIdentityService identityService, + IRoleService roleService, + ICurrentUser currentUser +) : IQueryHandler> { - public async Task> Handle(GetTicketQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetTicketQuery query, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); - var ticket = await dbContext.SupportTickets.AsNoTracking() - .FirstOrDefaultAsync(t => t.Id == query.TicketId && t.UserId == userId, cancellationToken); + var ticket = await dbContext + .SupportTickets.AsNoTracking() + .FirstOrDefaultAsync( + t => t.Id == query.TicketId && t.UserId == userId, + cancellationToken + ); if (ticket is null) return Result.Failure(SupportErrors.NotFound); - var dto = await TicketMapping.ToDetailDtoAsync(dbContext, identityService, roleService, ticket, cancellationToken); + var dto = await TicketMapping.ToDetailDtoAsync( + dbContext, + identityService, + roleService, + ticket, + cancellationToken + ); return Result.Success(dto); } } diff --git a/backend/src/PnvPanel.Application/Support/ListMyTickets/ListMyTicketsQuery.cs b/backend/src/PnvPanel.Application/Support/ListMyTickets/ListMyTicketsQuery.cs index 6fbb253..6504953 100644 --- a/backend/src/PnvPanel.Application/Support/ListMyTickets/ListMyTicketsQuery.cs +++ b/backend/src/PnvPanel.Application/Support/ListMyTickets/ListMyTicketsQuery.cs @@ -4,5 +4,9 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Support.ListMyTickets; -public sealed record ListMyTicketsQuery(TicketType? TypeFilter, TicketStatus? StatusFilter, int Page, int PageSize) - : IQuery>>, IRequiresActivation; +public sealed record ListMyTicketsQuery( + TicketType? TypeFilter, + TicketStatus? StatusFilter, + int Page, + int PageSize +) : IQuery>>, IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/Support/ListMyTickets/ListMyTicketsQueryHandler.cs b/backend/src/PnvPanel.Application/Support/ListMyTickets/ListMyTicketsQueryHandler.cs index 1e822f6..8323a83 100644 --- a/backend/src/PnvPanel.Application/Support/ListMyTickets/ListMyTicketsQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Support/ListMyTickets/ListMyTicketsQueryHandler.cs @@ -6,10 +6,16 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Support.ListMyTickets; -public sealed class ListMyTicketsQueryHandler(IAppDbContext dbContext, IIdentityService identityService, ICurrentUser currentUser) - : IQueryHandler>> +public sealed class ListMyTicketsQueryHandler( + IAppDbContext dbContext, + IIdentityService identityService, + ICurrentUser currentUser +) : IQueryHandler>> { - public async Task>> Handle(ListMyTicketsQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListMyTicketsQuery query, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure>(AuthErrors.Unauthorized); @@ -23,9 +29,18 @@ public sealed class ListMyTicketsQueryHandler(IAppDbContext dbContext, IIdentity if (query.StatusFilter is { } status) ticketsQuery = ticketsQuery.Where(t => t.Status == status); - var page1 = await ticketsQuery.OrderByDescending(t => t.CreatedAt).ToPagedListAsync(page, pageSize, cancellationToken); - var items = await TicketMapping.ToSummaryDtosAsync(dbContext, identityService, page1.Items, cancellationToken); + var page1 = await ticketsQuery + .OrderByDescending(t => t.CreatedAt) + .ToPagedListAsync(page, pageSize, cancellationToken); + var items = await TicketMapping.ToSummaryDtosAsync( + dbContext, + identityService, + page1.Items, + cancellationToken + ); - return Result.Success(new PagedList(items, page1.Total, page1.Page, page1.PageSize)); + return Result.Success( + new PagedList(items, page1.Total, page1.Page, page1.PageSize) + ); } } diff --git a/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQuery.cs b/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQuery.cs index 421ca18..4e2c212 100644 --- a/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQuery.cs +++ b/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQuery.cs @@ -6,4 +6,6 @@ namespace PnvPanel.Application.Support.ListSelectableRoles; /// Список ролей для выбора в заявке на роль (без admin) — в отличие от ListRolesQuery /// (Admin/Roles), доступен любому активированному пользователю. -public sealed record ListSelectableRolesQuery : IQuery>>, IRequiresActivation; +public sealed record ListSelectableRolesQuery + : IQuery>>, + IRequiresActivation; diff --git a/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQueryHandler.cs b/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQueryHandler.cs index 2a572c6..09f3850 100644 --- a/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Support/ListSelectableRoles/ListSelectableRolesQueryHandler.cs @@ -11,10 +11,15 @@ public sealed class ListSelectableRolesQueryHandler(IRoleService roleService) // CreateRoleRequestTicketCommandHandler (Application не может ссылаться на Infrastructure). private const string AdminRoleName = "admin"; - public async Task>> Handle(ListSelectableRolesQuery query, CancellationToken cancellationToken) + public async Task>> Handle( + ListSelectableRolesQuery query, + CancellationToken cancellationToken + ) { var roles = await roleService.ListRolesAsync(cancellationToken); - var selectable = roles.Where(r => !r.Name.Equals(AdminRoleName, StringComparison.OrdinalIgnoreCase)).ToList(); + var selectable = roles + .Where(r => !r.Name.Equals(AdminRoleName, StringComparison.OrdinalIgnoreCase)) + .ToList(); return Result.Success>(selectable); } diff --git a/backend/src/PnvPanel.Application/Support/Reopen/ReopenTicketCommandHandler.cs b/backend/src/PnvPanel.Application/Support/Reopen/ReopenTicketCommandHandler.cs index dd4faec..9704271 100644 --- a/backend/src/PnvPanel.Application/Support/Reopen/ReopenTicketCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Support/Reopen/ReopenTicketCommandHandler.cs @@ -7,16 +7,24 @@ using PnvPanel.Domain.Support; namespace PnvPanel.Application.Support.Reopen; -public sealed class ReopenTicketCommandHandler(IAppDbContext dbContext, ITelegramNotifier telegramNotifier, ICurrentUser currentUser) - : ICommandHandler +public sealed class ReopenTicketCommandHandler( + IAppDbContext dbContext, + ITelegramNotifier telegramNotifier, + ICurrentUser currentUser +) : ICommandHandler { - public async Task Handle(ReopenTicketCommand command, CancellationToken cancellationToken) + public async Task Handle( + ReopenTicketCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Result.Failure(AuthErrors.Unauthorized); - var ticket = await dbContext.SupportTickets - .FirstOrDefaultAsync(t => t.Id == command.TicketId && t.UserId == userId, cancellationToken); + var ticket = await dbContext.SupportTickets.FirstOrDefaultAsync( + t => t.Id == command.TicketId && t.UserId == userId, + cancellationToken + ); if (ticket is null) return Result.Failure(SupportErrors.NotFound); @@ -26,7 +34,12 @@ public sealed class ReopenTicketCommandHandler(IAppDbContext dbContext, ITelegra ticket.Reopen(); var userName = currentUser.UserName ?? userId.ToString(); - await telegramNotifier.NotifyAdminsTicketReopenedAsync(ticket.Id, userName, ticket.Type, cancellationToken); + await telegramNotifier.NotifyAdminsTicketReopenedAsync( + ticket.Id, + userName, + ticket.Type, + cancellationToken + ); return Result.Success(); } diff --git a/backend/src/PnvPanel.Application/Support/SupportErrors.cs b/backend/src/PnvPanel.Application/Support/SupportErrors.cs index a2f28ef..6cd738f 100644 --- a/backend/src/PnvPanel.Application/Support/SupportErrors.cs +++ b/backend/src/PnvPanel.Application/Support/SupportErrors.cs @@ -5,32 +5,61 @@ namespace PnvPanel.Application.Support; public static class SupportErrors { public static readonly Error NotFound = Error.NotFound("Support.NotFound", "Тикет не найден."); - public static readonly Error AttachmentNotFound = Error.NotFound("Support.AttachmentNotFound", "Вложение не найдено."); - public static readonly Error RoleNotFound = Error.NotFound("Support.RoleNotFound", "Роль не найдена."); + public static readonly Error AttachmentNotFound = Error.NotFound( + "Support.AttachmentNotFound", + "Вложение не найдено." + ); + public static readonly Error RoleNotFound = Error.NotFound( + "Support.RoleNotFound", + "Роль не найдена." + ); - public static readonly Error CannotRequestAdminRole = - Error.Forbidden("Support.CannotRequestAdminRole", "Роль администратора нельзя запросить через заявку."); + public static readonly Error CannotRequestAdminRole = Error.Forbidden( + "Support.CannotRequestAdminRole", + "Роль администратора нельзя запросить через заявку." + ); - public static readonly Error TicketClosed = - Error.Conflict("Support.TicketClosed", "Тикет закрыт — комментарии больше не принимаются."); + public static readonly Error TicketClosed = Error.Conflict( + "Support.TicketClosed", + "Тикет закрыт — комментарии больше не принимаются." + ); - public static readonly Error NotOpen = Error.Conflict("Support.NotOpen", "Тикет уже обработан."); - public static readonly Error AlreadyClosed = Error.Conflict("Support.AlreadyClosed", "Тикет уже закрыт."); + public static readonly Error NotOpen = Error.Conflict( + "Support.NotOpen", + "Тикет уже обработан." + ); + public static readonly Error AlreadyClosed = Error.Conflict( + "Support.AlreadyClosed", + "Тикет уже закрыт." + ); - public static readonly Error NotResolved = - Error.Conflict("Support.NotResolved", "Переоткрыть можно только решённый тикет."); + public static readonly Error NotResolved = Error.Conflict( + "Support.NotResolved", + "Переоткрыть можно только решённый тикет." + ); - public static readonly Error RoleRequestAlreadyPending = - Error.Conflict("Support.RoleRequestAlreadyPending", "У вас уже есть необработанная заявка на роль."); + public static readonly Error RoleRequestAlreadyPending = Error.Conflict( + "Support.RoleRequestAlreadyPending", + "У вас уже есть необработанная заявка на роль." + ); - public static readonly Error NotRoleRequest = Error.Validation("Support.NotRoleRequest", "Это не заявка на роль."); + public static readonly Error NotRoleRequest = Error.Validation( + "Support.NotRoleRequest", + "Это не заявка на роль." + ); - public static readonly Error TooManyAttachments = - Error.Validation("Support.TooManyAttachments", $"Слишком много вложений (максимум {TicketAttachmentValidation.MaxAttachments})."); + public static readonly Error TooManyAttachments = Error.Validation( + "Support.TooManyAttachments", + $"Слишком много вложений (максимум {TicketAttachmentValidation.MaxAttachments})." + ); - public static readonly Error AttachmentTooLarge = - Error.Validation("Support.AttachmentTooLarge", "Файл превышает лимит 5 МБ."); + public static readonly Error AttachmentTooLarge = Error.Validation( + "Support.AttachmentTooLarge", + "Файл превышает лимит 5 МБ." + ); - public static readonly Error UnsupportedAttachmentType = - Error.Validation("Support.UnsupportedAttachmentType", "Поддерживаются только изображения (JPEG/PNG/WEBP/GIF)."); + public static readonly Error UnsupportedAttachmentType = Error.Validation( + "Support.UnsupportedAttachmentType", + "Поддерживаются только изображения (JPEG/PNG/WEBP/GIF)." + ); } diff --git a/backend/src/PnvPanel.Application/Support/TicketAttachmentDto.cs b/backend/src/PnvPanel.Application/Support/TicketAttachmentDto.cs index be1ba34..e341a3d 100644 --- a/backend/src/PnvPanel.Application/Support/TicketAttachmentDto.cs +++ b/backend/src/PnvPanel.Application/Support/TicketAttachmentDto.cs @@ -1,3 +1,8 @@ namespace PnvPanel.Application.Support; -public sealed record TicketAttachmentDto(Guid Id, string FileName, string ContentType, long SizeBytes); +public sealed record TicketAttachmentDto( + Guid Id, + string FileName, + string ContentType, + long SizeBytes +); diff --git a/backend/src/PnvPanel.Application/Support/TicketAttachmentPersistence.cs b/backend/src/PnvPanel.Application/Support/TicketAttachmentPersistence.cs index 085fe6b..6394086 100644 --- a/backend/src/PnvPanel.Application/Support/TicketAttachmentPersistence.cs +++ b/backend/src/PnvPanel.Application/Support/TicketAttachmentPersistence.cs @@ -6,13 +6,25 @@ namespace PnvPanel.Application.Support; internal static class TicketAttachmentPersistence { public static async Task> SaveAllAsync( - IFileStorage fileStorage, Guid commentId, IReadOnlyList uploads, CancellationToken cancellationToken) + IFileStorage fileStorage, + Guid commentId, + IReadOnlyList uploads, + CancellationToken cancellationToken + ) { var attachments = new List(); foreach (var upload in uploads) { var storedFileName = await fileStorage.SaveAsync(upload.Content, cancellationToken); - attachments.Add(TicketAttachment.Create(commentId, upload.FileName, storedFileName, upload.ContentType, upload.SizeBytes)); + attachments.Add( + TicketAttachment.Create( + commentId, + upload.FileName, + storedFileName, + upload.ContentType, + upload.SizeBytes + ) + ); } return attachments; diff --git a/backend/src/PnvPanel.Application/Support/TicketAttachmentUpload.cs b/backend/src/PnvPanel.Application/Support/TicketAttachmentUpload.cs index 60e35b0..e42f4cb 100644 --- a/backend/src/PnvPanel.Application/Support/TicketAttachmentUpload.cs +++ b/backend/src/PnvPanel.Application/Support/TicketAttachmentUpload.cs @@ -2,4 +2,9 @@ namespace PnvPanel.Application.Support; /// Вложение на входе команды — Api-слой парсит multipart и передаёт сюда открытый поток; /// Application не знает про HTTP/IFormFile. -public sealed record TicketAttachmentUpload(Stream Content, string FileName, string ContentType, long SizeBytes); +public sealed record TicketAttachmentUpload( + Stream Content, + string FileName, + string ContentType, + long SizeBytes +); diff --git a/backend/src/PnvPanel.Application/Support/TicketAttachmentValidation.cs b/backend/src/PnvPanel.Application/Support/TicketAttachmentValidation.cs index 144cace..0c6a73f 100644 --- a/backend/src/PnvPanel.Application/Support/TicketAttachmentValidation.cs +++ b/backend/src/PnvPanel.Application/Support/TicketAttachmentValidation.cs @@ -7,9 +7,14 @@ internal static class TicketAttachmentValidation public const int MaxAttachments = 5; public const long MaxSizeBytes = 5 * 1024 * 1024; - private static readonly HashSet AllowedContentTypes = new(StringComparer.OrdinalIgnoreCase) + private static readonly HashSet AllowedContentTypes = new( + StringComparer.OrdinalIgnoreCase + ) { - "image/jpeg", "image/png", "image/webp", "image/gif", + "image/jpeg", + "image/png", + "image/webp", + "image/gif", }; public static Error? Validate(IReadOnlyList attachments) diff --git a/backend/src/PnvPanel.Application/Support/TicketAuthorization.cs b/backend/src/PnvPanel.Application/Support/TicketAuthorization.cs index 14ac774..79d27d8 100644 --- a/backend/src/PnvPanel.Application/Support/TicketAuthorization.cs +++ b/backend/src/PnvPanel.Application/Support/TicketAuthorization.cs @@ -9,5 +9,6 @@ internal static class TicketAuthorization private const string AdminRoleName = "admin"; public static bool IsAdmin(CurrentUserProfile? profile) => - profile is not null && profile.Role.Equals(AdminRoleName, StringComparison.OrdinalIgnoreCase); + profile is not null + && profile.Role.Equals(AdminRoleName, StringComparison.OrdinalIgnoreCase); } diff --git a/backend/src/PnvPanel.Application/Support/TicketCommentDto.cs b/backend/src/PnvPanel.Application/Support/TicketCommentDto.cs index 0d28b6f..346136c 100644 --- a/backend/src/PnvPanel.Application/Support/TicketCommentDto.cs +++ b/backend/src/PnvPanel.Application/Support/TicketCommentDto.cs @@ -1,5 +1,10 @@ namespace PnvPanel.Application.Support; public sealed record TicketCommentDto( - Guid Id, Guid AuthorId, string AuthorName, string Body, DateTimeOffset CreatedAt, - IReadOnlyList Attachments); + Guid Id, + Guid AuthorId, + string AuthorName, + string Body, + DateTimeOffset CreatedAt, + IReadOnlyList Attachments +); diff --git a/backend/src/PnvPanel.Application/Support/TicketDetailDto.cs b/backend/src/PnvPanel.Application/Support/TicketDetailDto.cs index 59bf7af..a61e326 100644 --- a/backend/src/PnvPanel.Application/Support/TicketDetailDto.cs +++ b/backend/src/PnvPanel.Application/Support/TicketDetailDto.cs @@ -7,7 +7,16 @@ namespace PnvPanel.Application.Support; /// на SupportTicket хранится только Id). Для новой роли имя лежит прямо в ProposedRoleName. /// public sealed record TicketDetailDto( - Guid Id, Guid UserId, string UserName, TicketType Type, TicketStatus Status, - Guid? RequestedRoleId, string? RequestedRoleName, - string? ProposedRoleName, int? ProposedMaxConfigs, int? ProposedMaxIpLimit, - DateTimeOffset CreatedAt, IReadOnlyList Comments); + Guid Id, + Guid UserId, + string UserName, + TicketType Type, + TicketStatus Status, + Guid? RequestedRoleId, + string? RequestedRoleName, + string? ProposedRoleName, + int? ProposedMaxConfigs, + int? ProposedMaxIpLimit, + DateTimeOffset CreatedAt, + IReadOnlyList Comments +); diff --git a/backend/src/PnvPanel.Application/Support/TicketMapping.cs b/backend/src/PnvPanel.Application/Support/TicketMapping.cs index 3b83859..a8eddb8 100644 --- a/backend/src/PnvPanel.Application/Support/TicketMapping.cs +++ b/backend/src/PnvPanel.Application/Support/TicketMapping.cs @@ -13,18 +13,26 @@ namespace PnvPanel.Application.Support; internal static class TicketMapping { public static async Task ToDetailDtoAsync( - IAppDbContext dbContext, IIdentityService identityService, IRoleService roleService, - SupportTicket ticket, CancellationToken cancellationToken) + IAppDbContext dbContext, + IIdentityService identityService, + IRoleService roleService, + SupportTicket ticket, + CancellationToken cancellationToken + ) { - var comments = await dbContext.TicketComments.AsNoTracking() + var comments = await dbContext + .TicketComments.AsNoTracking() .Where(c => c.TicketId == ticket.Id) .OrderBy(c => c.CreatedAt) .ToListAsync(cancellationToken); var commentIds = comments.Select(c => c.Id).ToList(); - var attachmentsByComment = (await dbContext.TicketAttachments.AsNoTracking() + var attachmentsByComment = ( + await dbContext + .TicketAttachments.AsNoTracking() .Where(a => commentIds.Contains(a.CommentId)) - .ToListAsync(cancellationToken)) + .ToListAsync(cancellationToken) + ) .GroupBy(a => a.CommentId) .ToDictionary(g => g.Key, g => g.ToList()); @@ -40,27 +48,52 @@ internal static class TicketMapping var commentDtos = comments .Select(c => new TicketCommentDto( - c.Id, c.AuthorId, userNames.GetValueOrDefault(c.AuthorId, "?"), c.Body, c.CreatedAt, - attachmentsByComment.GetValueOrDefault(c.Id, []) - .Select(a => new TicketAttachmentDto(a.Id, a.FileName, a.ContentType, a.SizeBytes)) - .ToList())) + c.Id, + c.AuthorId, + userNames.GetValueOrDefault(c.AuthorId, "?"), + c.Body, + c.CreatedAt, + attachmentsByComment + .GetValueOrDefault(c.Id, []) + .Select(a => new TicketAttachmentDto( + a.Id, + a.FileName, + a.ContentType, + a.SizeBytes + )) + .ToList() + )) .ToList(); return new TicketDetailDto( - ticket.Id, ticket.UserId, userNames.GetValueOrDefault(ticket.UserId, "?"), ticket.Type, ticket.Status, - ticket.RequestedRoleId, requestedRoleName, ticket.ProposedRoleName, ticket.ProposedMaxConfigs, - ticket.ProposedMaxIpLimit, ticket.CreatedAt, commentDtos); + ticket.Id, + ticket.UserId, + userNames.GetValueOrDefault(ticket.UserId, "?"), + ticket.Type, + ticket.Status, + ticket.RequestedRoleId, + requestedRoleName, + ticket.ProposedRoleName, + ticket.ProposedMaxConfigs, + ticket.ProposedMaxIpLimit, + ticket.CreatedAt, + commentDtos + ); } public static async Task> ToSummaryDtosAsync( - IAppDbContext dbContext, IIdentityService identityService, IReadOnlyList tickets, - CancellationToken cancellationToken) + IAppDbContext dbContext, + IIdentityService identityService, + IReadOnlyList tickets, + CancellationToken cancellationToken + ) { if (tickets.Count == 0) return []; var ticketIds = tickets.Select(t => t.Id).ToList(); - var lastActivity = await dbContext.TicketComments.AsNoTracking() + var lastActivity = await dbContext + .TicketComments.AsNoTracking() .Where(c => ticketIds.Contains(c.TicketId)) .GroupBy(c => c.TicketId) .Select(g => new { TicketId = g.Key, Last = g.Max(c => c.CreatedAt) }) @@ -71,8 +104,14 @@ internal static class TicketMapping return tickets .Select(t => new TicketSummaryDto( - t.Id, t.UserId, userNames.GetValueOrDefault(t.UserId, "?"), t.Type, t.Status, t.CreatedAt, - lastActivity.GetValueOrDefault(t.Id, t.CreatedAt))) + t.Id, + t.UserId, + userNames.GetValueOrDefault(t.UserId, "?"), + t.Type, + t.Status, + t.CreatedAt, + lastActivity.GetValueOrDefault(t.Id, t.CreatedAt) + )) .ToList(); } } diff --git a/backend/src/PnvPanel.Application/Support/TicketSummaryDto.cs b/backend/src/PnvPanel.Application/Support/TicketSummaryDto.cs index fce8ce1..f189e0e 100644 --- a/backend/src/PnvPanel.Application/Support/TicketSummaryDto.cs +++ b/backend/src/PnvPanel.Application/Support/TicketSummaryDto.cs @@ -5,5 +5,11 @@ namespace PnvPanel.Application.Support; /// Строка списка тикетов — свой (ListMyTicketsQuery) и админский (ListAllTicketsQuery) списки /// используют один и тот же DTO (владелец видит только свои UserId/UserName — не секрет для себя). public sealed record TicketSummaryDto( - Guid Id, Guid UserId, string UserName, TicketType Type, TicketStatus Status, - DateTimeOffset CreatedAt, DateTimeOffset LastActivityAt); + Guid Id, + Guid UserId, + string UserName, + TicketType Type, + TicketStatus Status, + DateTimeOffset CreatedAt, + DateTimeOffset LastActivityAt +); diff --git a/backend/src/PnvPanel.Application/Telegram/Bot/ApproveTelegramLoginCommand.cs b/backend/src/PnvPanel.Application/Telegram/Bot/ApproveTelegramLoginCommand.cs index 92f7313..6b0b0c2 100644 --- a/backend/src/PnvPanel.Application/Telegram/Bot/ApproveTelegramLoginCommand.cs +++ b/backend/src/PnvPanel.Application/Telegram/Bot/ApproveTelegramLoginCommand.cs @@ -3,4 +3,5 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Telegram.Bot; -public sealed record ApproveTelegramLoginCommand(Guid RequestId, long TelegramUserId) : ICommand; +public sealed record ApproveTelegramLoginCommand(Guid RequestId, long TelegramUserId) + : ICommand; diff --git a/backend/src/PnvPanel.Application/Telegram/Bot/ApproveTelegramLoginCommandHandler.cs b/backend/src/PnvPanel.Application/Telegram/Bot/ApproveTelegramLoginCommandHandler.cs index f62ccdb..fc7dcb0 100644 --- a/backend/src/PnvPanel.Application/Telegram/Bot/ApproveTelegramLoginCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Telegram/Bot/ApproveTelegramLoginCommandHandler.cs @@ -6,16 +6,27 @@ using PnvPanel.Domain.Exceptions; namespace PnvPanel.Application.Telegram.Bot; -public sealed class ApproveTelegramLoginCommandHandler(IAppDbContext dbContext, IIdentityService identityService) - : ICommandHandler +public sealed class ApproveTelegramLoginCommandHandler( + IAppDbContext dbContext, + IIdentityService identityService +) : ICommandHandler { - public async Task Handle(ApproveTelegramLoginCommand command, CancellationToken cancellationToken) + public async Task Handle( + ApproveTelegramLoginCommand command, + CancellationToken cancellationToken + ) { - var userId = await identityService.FindUserIdByTelegramUserIdAsync(command.TelegramUserId, cancellationToken); + var userId = await identityService.FindUserIdByTelegramUserIdAsync( + command.TelegramUserId, + cancellationToken + ); if (userId is null) return Result.Failure(TelegramErrors.NotLinked); - var request = await dbContext.TelegramLoginRequests.FirstOrDefaultAsync(r => r.Id == command.RequestId, cancellationToken); + var request = await dbContext.TelegramLoginRequests.FirstOrDefaultAsync( + r => r.Id == command.RequestId, + cancellationToken + ); if (request is null) return Result.Failure(TelegramErrors.LoginRequestNotFound); diff --git a/backend/src/PnvPanel.Application/Telegram/Bot/LinkTelegramCommand.cs b/backend/src/PnvPanel.Application/Telegram/Bot/LinkTelegramCommand.cs index 44d72be..629c768 100644 --- a/backend/src/PnvPanel.Application/Telegram/Bot/LinkTelegramCommand.cs +++ b/backend/src/PnvPanel.Application/Telegram/Bot/LinkTelegramCommand.cs @@ -4,4 +4,8 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Telegram.Bot; /// Вызывается только из TelegramBotHostedService (обработка "/start link_<token>"). -public sealed record LinkTelegramCommand(string Token, long TelegramUserId, string? TelegramUsername) : ICommand>; +public sealed record LinkTelegramCommand( + string Token, + long TelegramUserId, + string? TelegramUsername +) : ICommand>; diff --git a/backend/src/PnvPanel.Application/Telegram/Bot/LinkTelegramCommandHandler.cs b/backend/src/PnvPanel.Application/Telegram/Bot/LinkTelegramCommandHandler.cs index 42f6e2f..df60831 100644 --- a/backend/src/PnvPanel.Application/Telegram/Bot/LinkTelegramCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Telegram/Bot/LinkTelegramCommandHandler.cs @@ -5,17 +5,29 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Telegram.Bot; -public sealed class LinkTelegramCommandHandler(IAppDbContext dbContext, IIdentityService identityService) - : ICommandHandler> +public sealed class LinkTelegramCommandHandler( + IAppDbContext dbContext, + IIdentityService identityService +) : ICommandHandler> { - public async Task> Handle(LinkTelegramCommand command, CancellationToken cancellationToken) + public async Task> Handle( + LinkTelegramCommand command, + CancellationToken cancellationToken + ) { - var linkToken = await dbContext.TelegramLinkTokens.FirstOrDefaultAsync(t => t.Token == command.Token, cancellationToken); + var linkToken = await dbContext.TelegramLinkTokens.FirstOrDefaultAsync( + t => t.Token == command.Token, + cancellationToken + ); if (linkToken is null || !linkToken.IsValid) return Result.Failure(TelegramErrors.LinkTokenNotFound); var linkResult = await identityService.LinkTelegramAsync( - linkToken.UserId, command.TelegramUserId, command.TelegramUsername, cancellationToken); + linkToken.UserId, + command.TelegramUserId, + command.TelegramUsername, + cancellationToken + ); if (!linkResult.IsSuccess) return Result.Failure(linkResult.Error); diff --git a/backend/src/PnvPanel.Application/Telegram/Bot/RejectTelegramLoginCommand.cs b/backend/src/PnvPanel.Application/Telegram/Bot/RejectTelegramLoginCommand.cs index 273af20..f974327 100644 --- a/backend/src/PnvPanel.Application/Telegram/Bot/RejectTelegramLoginCommand.cs +++ b/backend/src/PnvPanel.Application/Telegram/Bot/RejectTelegramLoginCommand.cs @@ -3,4 +3,5 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Telegram.Bot; -public sealed record RejectTelegramLoginCommand(Guid RequestId, long TelegramUserId) : ICommand; +public sealed record RejectTelegramLoginCommand(Guid RequestId, long TelegramUserId) + : ICommand; diff --git a/backend/src/PnvPanel.Application/Telegram/Bot/RejectTelegramLoginCommandHandler.cs b/backend/src/PnvPanel.Application/Telegram/Bot/RejectTelegramLoginCommandHandler.cs index e8506a3..e0eefac 100644 --- a/backend/src/PnvPanel.Application/Telegram/Bot/RejectTelegramLoginCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Telegram/Bot/RejectTelegramLoginCommandHandler.cs @@ -6,16 +6,27 @@ using PnvPanel.Domain.Exceptions; namespace PnvPanel.Application.Telegram.Bot; -public sealed class RejectTelegramLoginCommandHandler(IAppDbContext dbContext, IIdentityService identityService) - : ICommandHandler +public sealed class RejectTelegramLoginCommandHandler( + IAppDbContext dbContext, + IIdentityService identityService +) : ICommandHandler { - public async Task Handle(RejectTelegramLoginCommand command, CancellationToken cancellationToken) + public async Task Handle( + RejectTelegramLoginCommand command, + CancellationToken cancellationToken + ) { - var userId = await identityService.FindUserIdByTelegramUserIdAsync(command.TelegramUserId, cancellationToken); + var userId = await identityService.FindUserIdByTelegramUserIdAsync( + command.TelegramUserId, + cancellationToken + ); if (userId is null) return Result.Failure(TelegramErrors.NotLinked); - var request = await dbContext.TelegramLoginRequests.FirstOrDefaultAsync(r => r.Id == command.RequestId, cancellationToken); + var request = await dbContext.TelegramLoginRequests.FirstOrDefaultAsync( + r => r.Id == command.RequestId, + cancellationToken + ); if (request is null) return Result.Failure(TelegramErrors.LoginRequestNotFound); diff --git a/backend/src/PnvPanel.Application/Telegram/CreateLinkTokenCommandHandler.cs b/backend/src/PnvPanel.Application/Telegram/CreateLinkTokenCommandHandler.cs index 5c26dfd..27e7f69 100644 --- a/backend/src/PnvPanel.Application/Telegram/CreateLinkTokenCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Telegram/CreateLinkTokenCommandHandler.cs @@ -11,7 +11,10 @@ public sealed class CreateLinkTokenCommandHandler(IAppDbContext dbContext, ICurr { private static readonly TimeSpan Ttl = TimeSpan.FromMinutes(5); - public Task> Handle(CreateLinkTokenCommand command, CancellationToken cancellationToken) + public Task> Handle( + CreateLinkTokenCommand command, + CancellationToken cancellationToken + ) { if (currentUser.UserId is not { } userId) return Task.FromResult(Result.Failure(AuthErrors.Unauthorized)); diff --git a/backend/src/PnvPanel.Application/Telegram/CreateLoginRequestCommandHandler.cs b/backend/src/PnvPanel.Application/Telegram/CreateLoginRequestCommandHandler.cs index fe9810c..fc6e2fd 100644 --- a/backend/src/PnvPanel.Application/Telegram/CreateLoginRequestCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Telegram/CreateLoginRequestCommandHandler.cs @@ -10,7 +10,10 @@ public sealed class CreateLoginRequestCommandHandler(IAppDbContext dbContext) { private static readonly TimeSpan Ttl = TimeSpan.FromMinutes(5); - public Task> Handle(CreateLoginRequestCommand command, CancellationToken cancellationToken) + public Task> Handle( + CreateLoginRequestCommand command, + CancellationToken cancellationToken + ) { var request = TelegramLoginRequest.Create(Ttl, command.Context); dbContext.TelegramLoginRequests.Add(request); diff --git a/backend/src/PnvPanel.Application/Telegram/GetLoginRequestStatusQuery.cs b/backend/src/PnvPanel.Application/Telegram/GetLoginRequestStatusQuery.cs index 2d77ab2..619ac5a 100644 --- a/backend/src/PnvPanel.Application/Telegram/GetLoginRequestStatusQuery.cs +++ b/backend/src/PnvPanel.Application/Telegram/GetLoginRequestStatusQuery.cs @@ -5,6 +5,7 @@ using PnvPanel.Domain.Telegram; namespace PnvPanel.Application.Telegram; -public sealed record GetLoginRequestStatusQuery(Guid RequestId) : IQuery>; +public sealed record GetLoginRequestStatusQuery(Guid RequestId) + : IQuery>; public sealed record LoginRequestStatusDto(TelegramLoginStatus Status, AuthResult? Auth); diff --git a/backend/src/PnvPanel.Application/Telegram/GetLoginRequestStatusQueryHandler.cs b/backend/src/PnvPanel.Application/Telegram/GetLoginRequestStatusQueryHandler.cs index 0d79dba..c492b9f 100644 --- a/backend/src/PnvPanel.Application/Telegram/GetLoginRequestStatusQueryHandler.cs +++ b/backend/src/PnvPanel.Application/Telegram/GetLoginRequestStatusQueryHandler.cs @@ -13,12 +13,21 @@ namespace PnvPanel.Application.Telegram; /// Осознанное отступление от чистого CQRS ради простого поллинга без отдельного claim-эндпоинта. /// public sealed class GetLoginRequestStatusQueryHandler( - IAppDbContext dbContext, IIdentityService identityService, IJwtTokenService jwtTokenService, IRefreshTokenService refreshTokenService) - : IQueryHandler> + IAppDbContext dbContext, + IIdentityService identityService, + IJwtTokenService jwtTokenService, + IRefreshTokenService refreshTokenService +) : IQueryHandler> { - public async Task> Handle(GetLoginRequestStatusQuery query, CancellationToken cancellationToken) + public async Task> Handle( + GetLoginRequestStatusQuery query, + CancellationToken cancellationToken + ) { - var request = await dbContext.TelegramLoginRequests.FirstOrDefaultAsync(r => r.Id == query.RequestId, cancellationToken); + var request = await dbContext.TelegramLoginRequests.FirstOrDefaultAsync( + r => r.Id == query.RequestId, + cancellationToken + ); if (request is null) return Result.Failure(TelegramErrors.LoginRequestNotFound); @@ -28,7 +37,10 @@ public sealed class GetLoginRequestStatusQueryHandler( if (request.Status != TelegramLoginStatus.Approved) return Result.Success(new LoginRequestStatusDto(request.Status, null)); - var profile = await identityService.GetProfileAsync(request.UserId!.Value, cancellationToken); + var profile = await identityService.GetProfileAsync( + request.UserId!.Value, + cancellationToken + ); if (profile is null) return Result.Failure(AuthErrors.Unauthorized); @@ -40,8 +52,20 @@ public sealed class GetLoginRequestStatusQueryHandler( var refreshToken = await refreshTokenService.IssueAsync(profile.Id, cancellationToken); // Пользователь только что подтвердил вход через бота — Telegram точно привязан. - var dto = new CurrentUserDto(profile.Id, profile.UserName, profile.Role, profile.IsActivated, TelegramLinked: true); - var auth = new AuthResult(accessToken, accessExpiresAt, refreshToken.RawToken, refreshToken.ExpiresAt, dto); + var dto = new CurrentUserDto( + profile.Id, + profile.UserName, + profile.Role, + profile.IsActivated, + TelegramLinked: true + ); + var auth = new AuthResult( + accessToken, + accessExpiresAt, + refreshToken.RawToken, + refreshToken.ExpiresAt, + dto + ); return Result.Success(new LoginRequestStatusDto(TelegramLoginStatus.Approved, auth)); } diff --git a/backend/src/PnvPanel.Application/Telegram/RegisterViaTelegramCommandHandler.cs b/backend/src/PnvPanel.Application/Telegram/RegisterViaTelegramCommandHandler.cs index c4ba81b..0d58be1 100644 --- a/backend/src/PnvPanel.Application/Telegram/RegisterViaTelegramCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Telegram/RegisterViaTelegramCommandHandler.cs @@ -10,9 +10,14 @@ public sealed class RegisterViaTelegramCommandHandler(IIdentityService identityS : ICommandHandler> { public async Task> Handle( - RegisterViaTelegramCommand command, CancellationToken cancellationToken) + RegisterViaTelegramCommand command, + CancellationToken cancellationToken + ) { - var existingUserId = await identityService.FindUserIdByTelegramUserIdAsync(command.TelegramUserId, cancellationToken); + var existingUserId = await identityService.FindUserIdByTelegramUserIdAsync( + command.TelegramUserId, + cancellationToken + ); if (existingUserId is not null) return Result.Failure(TelegramErrors.AlreadyLinked); @@ -28,14 +33,25 @@ public sealed class RegisterViaTelegramCommandHandler(IIdentityService identityS if (!createResult.IsSuccess) { userName = command.TelegramUserId.ToString(); - createResult = await identityService.CreateUserAsync(userName, password, cancellationToken); + createResult = await identityService.CreateUserAsync( + userName, + password, + cancellationToken + ); if (!createResult.IsSuccess) return Result.Failure(createResult.Error); } - await identityService.LinkTelegramAsync(createResult.Value, command.TelegramUserId, command.TelegramUsername, cancellationToken); + await identityService.LinkTelegramAsync( + createResult.Value, + command.TelegramUserId, + command.TelegramUsername, + cancellationToken + ); - return Result.Success(new RegisterViaTelegramResult(createResult.Value, userName!, password)); + return Result.Success( + new RegisterViaTelegramResult(createResult.Value, userName!, password) + ); } /// diff --git a/backend/src/PnvPanel.Application/Telegram/TelegramErrors.cs b/backend/src/PnvPanel.Application/Telegram/TelegramErrors.cs index c22dada..346534d 100644 --- a/backend/src/PnvPanel.Application/Telegram/TelegramErrors.cs +++ b/backend/src/PnvPanel.Application/Telegram/TelegramErrors.cs @@ -4,15 +4,23 @@ namespace PnvPanel.Application.Telegram; public static class TelegramErrors { - public static readonly Error LinkTokenNotFound = - Error.NotFound("Telegram.LinkTokenNotFound", "Токен привязки не найден или истёк."); + public static readonly Error LinkTokenNotFound = Error.NotFound( + "Telegram.LinkTokenNotFound", + "Токен привязки не найден или истёк." + ); - public static readonly Error LoginRequestNotFound = - Error.NotFound("Telegram.LoginRequestNotFound", "Запрос на вход не найден."); + public static readonly Error LoginRequestNotFound = Error.NotFound( + "Telegram.LoginRequestNotFound", + "Запрос на вход не найден." + ); - public static readonly Error NotLinked = - Error.Conflict("Telegram.NotLinked", "Telegram не привязан ни к одному аккаунту."); + public static readonly Error NotLinked = Error.Conflict( + "Telegram.NotLinked", + "Telegram не привязан ни к одному аккаунту." + ); - public static readonly Error AlreadyLinked = - Error.Conflict("Telegram.AlreadyLinked", "Этот Telegram уже привязан к аккаунту — используйте вход через Telegram."); + public static readonly Error AlreadyLinked = Error.Conflict( + "Telegram.AlreadyLinked", + "Этот Telegram уже привязан к аккаунту — используйте вход через Telegram." + ); } diff --git a/backend/src/PnvPanel.Application/Telegram/UnlinkTelegramCommandHandler.cs b/backend/src/PnvPanel.Application/Telegram/UnlinkTelegramCommandHandler.cs index 7dd4fb8..4992656 100644 --- a/backend/src/PnvPanel.Application/Telegram/UnlinkTelegramCommandHandler.cs +++ b/backend/src/PnvPanel.Application/Telegram/UnlinkTelegramCommandHandler.cs @@ -5,8 +5,10 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Application.Telegram; -public sealed class UnlinkTelegramCommandHandler(IIdentityService identityService, ICurrentUser currentUser) - : ICommandHandler +public sealed class UnlinkTelegramCommandHandler( + IIdentityService identityService, + ICurrentUser currentUser +) : ICommandHandler { public Task Handle(UnlinkTelegramCommand command, CancellationToken cancellationToken) { diff --git a/backend/src/PnvPanel.Domain/Activation/ActivationRequest.cs b/backend/src/PnvPanel.Domain/Activation/ActivationRequest.cs index e1a037f..62ff5db 100644 --- a/backend/src/PnvPanel.Domain/Activation/ActivationRequest.cs +++ b/backend/src/PnvPanel.Domain/Activation/ActivationRequest.cs @@ -17,9 +17,7 @@ public sealed class ActivationRequest : Entity public string? RejectionReason { get; private set; } public DateTimeOffset CreatedAt { get; private set; } - private ActivationRequest() - { - } + private ActivationRequest() { } public static ActivationRequest Create(Guid userId, string? comment) { diff --git a/backend/src/PnvPanel.Domain/Apps/ClientApp.cs b/backend/src/PnvPanel.Domain/Apps/ClientApp.cs index b9d8182..6abae43 100644 --- a/backend/src/PnvPanel.Domain/Apps/ClientApp.cs +++ b/backend/src/PnvPanel.Domain/Apps/ClientApp.cs @@ -13,11 +13,16 @@ public sealed class ClientApp : Entity public int SortOrder { get; private set; } public bool IsEnabled { get; private set; } - private ClientApp() - { - } + private ClientApp() { } - public static ClientApp Create(string name, Uri downloadUrl, OsPlatform operatingSystem, string? description, string? iconUrl, int sortOrder) + public static ClientApp Create( + string name, + Uri downloadUrl, + OsPlatform operatingSystem, + string? description, + string? iconUrl, + int sortOrder + ) { return new ClientApp { @@ -32,7 +37,15 @@ public sealed class ClientApp : Entity }; } - public void Update(string name, Uri downloadUrl, OsPlatform operatingSystem, string? description, string? iconUrl, int sortOrder, bool isEnabled) + public void Update( + string name, + Uri downloadUrl, + OsPlatform operatingSystem, + string? description, + string? iconUrl, + int sortOrder, + bool isEnabled + ) { Name = name; DownloadUrl = downloadUrl; diff --git a/backend/src/PnvPanel.Domain/Audit/AuditLog.cs b/backend/src/PnvPanel.Domain/Audit/AuditLog.cs index b5d5177..db70a3c 100644 --- a/backend/src/PnvPanel.Domain/Audit/AuditLog.cs +++ b/backend/src/PnvPanel.Domain/Audit/AuditLog.cs @@ -12,12 +12,16 @@ public sealed class AuditLog public AuditSource Source { get; private set; } public DateTimeOffset CreatedAt { get; private set; } - private AuditLog() - { - } + private AuditLog() { } public static AuditLog Create( - Guid? actorId, string action, string targetType, string targetId, string? metadata, AuditSource source) + Guid? actorId, + string action, + string targetType, + string targetId, + string? metadata, + AuditSource source + ) { return new AuditLog { diff --git a/backend/src/PnvPanel.Domain/Common/Entity.cs b/backend/src/PnvPanel.Domain/Common/Entity.cs index 01ede97..693980a 100644 --- a/backend/src/PnvPanel.Domain/Common/Entity.cs +++ b/backend/src/PnvPanel.Domain/Common/Entity.cs @@ -4,7 +4,8 @@ public abstract class Entity { public Guid Id { get; protected init; } - public override bool Equals(object? obj) => obj is Entity other && other.GetType() == GetType() && other.Id == Id; + public override bool Equals(object? obj) => + obj is Entity other && other.GetType() == GetType() && other.Id == Id; public override int GetHashCode() => HashCode.Combine(GetType(), Id); diff --git a/backend/src/PnvPanel.Domain/Configs/TrafficSample.cs b/backend/src/PnvPanel.Domain/Configs/TrafficSample.cs index a19000e..273a167 100644 --- a/backend/src/PnvPanel.Domain/Configs/TrafficSample.cs +++ b/backend/src/PnvPanel.Domain/Configs/TrafficSample.cs @@ -12,11 +12,14 @@ public sealed class TrafficSample public long UpBytes { get; private set; } public long DownBytes { get; private set; } - private TrafficSample() - { - } + private TrafficSample() { } - public static TrafficSample Create(Guid configId, DateTimeOffset timestamp, long upBytes, long downBytes) + public static TrafficSample Create( + Guid configId, + DateTimeOffset timestamp, + long upBytes, + long downBytes + ) { return new TrafficSample { diff --git a/backend/src/PnvPanel.Domain/Configs/VpnConfig.cs b/backend/src/PnvPanel.Domain/Configs/VpnConfig.cs index d521221..4b684b7 100644 --- a/backend/src/PnvPanel.Domain/Configs/VpnConfig.cs +++ b/backend/src/PnvPanel.Domain/Configs/VpnConfig.cs @@ -26,9 +26,7 @@ public sealed class VpnConfig : Entity public DateTimeOffset? LastSyncAt { get; private set; } public DateTimeOffset CreatedAt { get; private set; } - private VpnConfig() - { - } + private VpnConfig() { } public static VpnConfig Create(Guid userId, Guid inboundId, VpnProtocol protocol, string? label) { @@ -103,5 +101,6 @@ public sealed class VpnConfig : Entity return $"pnv_{shortId}_{rand}"; } - private static string GenerateToken() => Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); + private static string GenerateToken() => + Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); } diff --git a/backend/src/PnvPanel.Domain/Inbounds/Inbound.cs b/backend/src/PnvPanel.Domain/Inbounds/Inbound.cs index ac372ae..a001d71 100644 --- a/backend/src/PnvPanel.Domain/Inbounds/Inbound.cs +++ b/backend/src/PnvPanel.Domain/Inbounds/Inbound.cs @@ -21,11 +21,15 @@ public sealed class Inbound : Entity public IReadOnlyList AllowedRoleIds { get; private set; } = []; public DateTimeOffset? LastSyncAt { get; private set; } - private Inbound() - { - } + private Inbound() { } - public static Inbound FromRemote(Guid nodeId, string remoteInboundId, VpnProtocol protocol, string remark, int port) + public static Inbound FromRemote( + Guid nodeId, + string remoteInboundId, + VpnProtocol protocol, + string remark, + int port + ) { return new Inbound { @@ -48,7 +52,11 @@ public sealed class Inbound : Entity LastSyncAt = DateTimeOffset.UtcNow; } - public void Publish(string? displayName, IReadOnlyCollection allowedRoleIds, int? maxClients) + public void Publish( + string? displayName, + IReadOnlyCollection allowedRoleIds, + int? maxClients + ) { IsPublished = true; DisplayName = displayName; diff --git a/backend/src/PnvPanel.Domain/News/NewsPost.cs b/backend/src/PnvPanel.Domain/News/NewsPost.cs index afda70a..e191e6b 100644 --- a/backend/src/PnvPanel.Domain/News/NewsPost.cs +++ b/backend/src/PnvPanel.Domain/News/NewsPost.cs @@ -10,9 +10,7 @@ public sealed class NewsPost : Entity public DateTimeOffset CreatedAt { get; private set; } public DateTimeOffset? UpdatedAt { get; private set; } - private NewsPost() - { - } + private NewsPost() { } public static NewsPost Create(string title, string body) { diff --git a/backend/src/PnvPanel.Domain/Nodes/Node.cs b/backend/src/PnvPanel.Domain/Nodes/Node.cs index 50f40c2..c9c00df 100644 --- a/backend/src/PnvPanel.Domain/Nodes/Node.cs +++ b/backend/src/PnvPanel.Domain/Nodes/Node.cs @@ -18,11 +18,14 @@ public sealed class Node : Entity public DateTimeOffset? LastSyncAt { get; private set; } public DateTimeOffset CreatedAt { get; private set; } - private Node() - { - } + private Node() { } - public static Node Register(string name, Uri baseAddress, NodeCredentials credentials, string? location) + public static Node Register( + string name, + Uri baseAddress, + NodeCredentials credentials, + string? location + ) { if (!baseAddress.IsAbsoluteUri) throw new DomainException("Адрес ноды должен быть абсолютным URI."); diff --git a/backend/src/PnvPanel.Domain/Nodes/NodeCredentials.cs b/backend/src/PnvPanel.Domain/Nodes/NodeCredentials.cs index 6dfefeb..2c811e6 100644 --- a/backend/src/PnvPanel.Domain/Nodes/NodeCredentials.cs +++ b/backend/src/PnvPanel.Domain/Nodes/NodeCredentials.cs @@ -6,5 +6,6 @@ namespace PnvPanel.Domain.Nodes; /// public sealed record NodeCredentials(string Username, string ProtectedPassword) { - public override string ToString() => $"NodeCredentials {{ Username = {Username}, ProtectedPassword = [REDACTED] }}"; + public override string ToString() => + $"NodeCredentials {{ Username = {Username}, ProtectedPassword = [REDACTED] }}"; } diff --git a/backend/src/PnvPanel.Domain/PnvPanel.Domain.csproj b/backend/src/PnvPanel.Domain/PnvPanel.Domain.csproj index 6d36c6d..256a64b 100644 --- a/backend/src/PnvPanel.Domain/PnvPanel.Domain.csproj +++ b/backend/src/PnvPanel.Domain/PnvPanel.Domain.csproj @@ -1,9 +1,7 @@  - net10.0 enable enable - diff --git a/backend/src/PnvPanel.Domain/Support/SupportTicket.cs b/backend/src/PnvPanel.Domain/Support/SupportTicket.cs index 3916cc3..6a0d82d 100644 --- a/backend/src/PnvPanel.Domain/Support/SupportTicket.cs +++ b/backend/src/PnvPanel.Domain/Support/SupportTicket.cs @@ -21,9 +21,7 @@ public sealed class SupportTicket : Entity public int? ProposedMaxIpLimit { get; private set; } public DateTimeOffset CreatedAt { get; private set; } - private SupportTicket() - { - } + private SupportTicket() { } public static SupportTicket CreateBugReport(Guid userId) { @@ -50,7 +48,12 @@ public sealed class SupportTicket : Entity }; } - public static SupportTicket CreateRoleRequestForNewRole(Guid userId, string name, int maxConfigs, int maxIpLimit) + public static SupportTicket CreateRoleRequestForNewRole( + Guid userId, + string name, + int maxConfigs, + int maxIpLimit + ) { return new SupportTicket { diff --git a/backend/src/PnvPanel.Domain/Support/TicketAttachment.cs b/backend/src/PnvPanel.Domain/Support/TicketAttachment.cs index cc925c9..1d646c4 100644 --- a/backend/src/PnvPanel.Domain/Support/TicketAttachment.cs +++ b/backend/src/PnvPanel.Domain/Support/TicketAttachment.cs @@ -16,11 +16,15 @@ public sealed class TicketAttachment : Entity public long SizeBytes { get; private set; } public DateTimeOffset CreatedAt { get; private set; } - private TicketAttachment() - { - } + private TicketAttachment() { } - public static TicketAttachment Create(Guid commentId, string fileName, string storedFileName, string contentType, long sizeBytes) + public static TicketAttachment Create( + Guid commentId, + string fileName, + string storedFileName, + string contentType, + long sizeBytes + ) { return new TicketAttachment { diff --git a/backend/src/PnvPanel.Domain/Support/TicketComment.cs b/backend/src/PnvPanel.Domain/Support/TicketComment.cs index 7e40939..f7a2bd4 100644 --- a/backend/src/PnvPanel.Domain/Support/TicketComment.cs +++ b/backend/src/PnvPanel.Domain/Support/TicketComment.cs @@ -14,9 +14,7 @@ public sealed class TicketComment : Entity public string Body { get; private set; } = string.Empty; public DateTimeOffset CreatedAt { get; private set; } - private TicketComment() - { - } + private TicketComment() { } public static TicketComment Create(Guid ticketId, Guid authorId, string body) { diff --git a/backend/src/PnvPanel.Domain/Telegram/TelegramLinkToken.cs b/backend/src/PnvPanel.Domain/Telegram/TelegramLinkToken.cs index 72aad1f..f5f3f1b 100644 --- a/backend/src/PnvPanel.Domain/Telegram/TelegramLinkToken.cs +++ b/backend/src/PnvPanel.Domain/Telegram/TelegramLinkToken.cs @@ -12,9 +12,7 @@ public sealed class TelegramLinkToken : Entity public DateTimeOffset ExpiresAt { get; private set; } public DateTimeOffset? ConsumedAt { get; private set; } - private TelegramLinkToken() - { - } + private TelegramLinkToken() { } public static TelegramLinkToken Create(Guid userId, TimeSpan ttl) { @@ -37,5 +35,6 @@ public sealed class TelegramLinkToken : Entity ConsumedAt = DateTimeOffset.UtcNow; } - private static string GenerateToken() => Convert.ToHexString(RandomNumberGenerator.GetBytes(24)).ToLowerInvariant(); + private static string GenerateToken() => + Convert.ToHexString(RandomNumberGenerator.GetBytes(24)).ToLowerInvariant(); } diff --git a/backend/src/PnvPanel.Domain/Telegram/TelegramLoginRequest.cs b/backend/src/PnvPanel.Domain/Telegram/TelegramLoginRequest.cs index 7f40253..0ac8dec 100644 --- a/backend/src/PnvPanel.Domain/Telegram/TelegramLoginRequest.cs +++ b/backend/src/PnvPanel.Domain/Telegram/TelegramLoginRequest.cs @@ -15,9 +15,7 @@ public sealed class TelegramLoginRequest : Entity public DateTimeOffset CreatedAt { get; private set; } public DateTimeOffset ExpiresAt { get; private set; } - private TelegramLoginRequest() - { - } + private TelegramLoginRequest() { } public static TelegramLoginRequest Create(TimeSpan ttl, string? context) { diff --git a/backend/src/PnvPanel.Infrastructure/BackgroundJobs/NodeHealthCheckService.cs b/backend/src/PnvPanel.Infrastructure/BackgroundJobs/NodeHealthCheckService.cs index af10d14..287f5c1 100644 --- a/backend/src/PnvPanel.Infrastructure/BackgroundJobs/NodeHealthCheckService.cs +++ b/backend/src/PnvPanel.Infrastructure/BackgroundJobs/NodeHealthCheckService.cs @@ -8,7 +8,10 @@ using PnvPanel.Infrastructure.Persistence; namespace PnvPanel.Infrastructure.BackgroundJobs; -public sealed class NodeHealthCheckService(IServiceScopeFactory scopeFactory, ILogger logger) : BackgroundService +public sealed class NodeHealthCheckService( + IServiceScopeFactory scopeFactory, + ILogger logger +) : BackgroundService { private static readonly TimeSpan Interval = TimeSpan.FromMinutes(2); @@ -25,8 +28,7 @@ public sealed class NodeHealthCheckService(IServiceScopeFactory scopeFactory, IL { logger.LogError(ex, "Node health-check failed"); } - } - while (await timer.WaitForNextTickAsync(stoppingToken)); + } while (await timer.WaitForNextTickAsync(stoppingToken)); } private async Task CheckAllAsync(CancellationToken cancellationToken) @@ -46,7 +48,12 @@ public sealed class NodeHealthCheckService(IServiceScopeFactory scopeFactory, IL if (node.Status != newStatus) { node.UpdateStatus(newStatus); - await notifier.NotifyNodeStatusChangedAsync(node.Id, newStatus, node.LastSyncAt, cancellationToken); + await notifier.NotifyNodeStatusChangedAsync( + node.Id, + newStatus, + node.LastSyncAt, + cancellationToken + ); } } diff --git a/backend/src/PnvPanel.Infrastructure/BackgroundJobs/TrafficRetentionService.cs b/backend/src/PnvPanel.Infrastructure/BackgroundJobs/TrafficRetentionService.cs index 19428df..e6a224c 100644 --- a/backend/src/PnvPanel.Infrastructure/BackgroundJobs/TrafficRetentionService.cs +++ b/backend/src/PnvPanel.Infrastructure/BackgroundJobs/TrafficRetentionService.cs @@ -9,8 +9,10 @@ namespace PnvPanel.Infrastructure.BackgroundJobs; /// TTL-чистка истории трафика (TrafficRetention__RetentionDays, по умолчанию 30 дней). public sealed class TrafficRetentionService( - IServiceScopeFactory scopeFactory, IOptions options, ILogger logger) - : BackgroundService + IServiceScopeFactory scopeFactory, + IOptions options, + ILogger logger +) : BackgroundService { private static readonly TimeSpan Interval = TimeSpan.FromHours(24); @@ -27,8 +29,7 @@ public sealed class TrafficRetentionService( { logger.LogError(ex, "Traffic history retention failed"); } - } - while (await timer.WaitForNextTickAsync(stoppingToken)); + } while (await timer.WaitForNextTickAsync(stoppingToken)); } private async Task CleanupAsync(CancellationToken cancellationToken) @@ -37,7 +38,9 @@ public sealed class TrafficRetentionService( var dbContext = scope.ServiceProvider.GetRequiredService(); var cutoff = DateTimeOffset.UtcNow.AddDays(-options.Value.RetentionDays); - var deleted = await dbContext.TrafficSamples.Where(s => s.Timestamp < cutoff).ExecuteDeleteAsync(cancellationToken); + var deleted = await dbContext + .TrafficSamples.Where(s => s.Timestamp < cutoff) + .ExecuteDeleteAsync(cancellationToken); if (deleted > 0) logger.LogInformation("Deleted {Count} expired traffic history records", deleted); diff --git a/backend/src/PnvPanel.Infrastructure/BackgroundJobs/TrafficSyncService.cs b/backend/src/PnvPanel.Infrastructure/BackgroundJobs/TrafficSyncService.cs index 0784d34..d00152d 100644 --- a/backend/src/PnvPanel.Infrastructure/BackgroundJobs/TrafficSyncService.cs +++ b/backend/src/PnvPanel.Infrastructure/BackgroundJobs/TrafficSyncService.cs @@ -13,7 +13,10 @@ namespace PnvPanel.Infrastructure.BackgroundJobs; /// VpnConfig + пишет TrafficSample. Реконсиляция дрейфа: если панель недоступна — просто пропускаем /// эту ноду в этом цикле, не роняем весь сервис и не трогаем локальные данные. /// -public sealed class TrafficSyncService(IServiceScopeFactory scopeFactory, ILogger logger) : BackgroundService +public sealed class TrafficSyncService( + IServiceScopeFactory scopeFactory, + ILogger logger +) : BackgroundService { private static readonly TimeSpan Interval = TimeSpan.FromMinutes(5); @@ -30,8 +33,7 @@ public sealed class TrafficSyncService(IServiceScopeFactory scopeFactory, ILogge { logger.LogError(ex, "Traffic sync failed"); } - } - while (await timer.WaitForNextTickAsync(stoppingToken)); + } while (await timer.WaitForNextTickAsync(stoppingToken)); } private async Task SyncAllAsync(CancellationToken cancellationToken) @@ -45,18 +47,26 @@ public sealed class TrafficSyncService(IServiceScopeFactory scopeFactory, ILogge foreach (var node in nodes) { - var inbounds = await dbContext.Inbounds.Where(i => i.NodeId == node.Id).ToListAsync(cancellationToken); + var inbounds = await dbContext + .Inbounds.Where(i => i.NodeId == node.Id) + .ToListAsync(cancellationToken); foreach (var inbound in inbounds) { - var configs = await dbContext.VpnConfigs - .Where(c => c.InboundId == inbound.Id && c.Status == ConfigStatus.Active) + var configs = await dbContext + .VpnConfigs.Where(c => + c.InboundId == inbound.Id && c.Status == ConfigStatus.Active + ) .ToListAsync(cancellationToken); if (configs.Count == 0) continue; - var trafficResult = await gateway.GetClientTrafficAsync(node, inbound.RemoteInboundId, cancellationToken); + var trafficResult = await gateway.GetClientTrafficAsync( + node, + inbound.RemoteInboundId, + cancellationToken + ); if (!trafficResult.IsSuccess) continue; @@ -66,10 +76,22 @@ public sealed class TrafficSyncService(IServiceScopeFactory scopeFactory, ILogge continue; config.UpdateTraffic(traffic.UpBytes, traffic.DownBytes); - dbContext.TrafficSamples.Add(TrafficSample.Create(config.Id, DateTimeOffset.UtcNow, traffic.UpBytes, traffic.DownBytes)); + dbContext.TrafficSamples.Add( + TrafficSample.Create( + config.Id, + DateTimeOffset.UtcNow, + traffic.UpBytes, + traffic.DownBytes + ) + ); await notifier.NotifyConfigTrafficUpdatedAsync( - config.UserId, config.Id, traffic.UpBytes, traffic.DownBytes, cancellationToken); + config.UserId, + config.Id, + traffic.UpBytes, + traffic.DownBytes, + cancellationToken + ); } } } diff --git a/backend/src/PnvPanel.Infrastructure/DependencyInjection.cs b/backend/src/PnvPanel.Infrastructure/DependencyInjection.cs index a596d56..00a41f2 100644 --- a/backend/src/PnvPanel.Infrastructure/DependencyInjection.cs +++ b/backend/src/PnvPanel.Infrastructure/DependencyInjection.cs @@ -25,14 +25,22 @@ namespace PnvPanel.Infrastructure; /// public static class DependencyInjection { - public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddInfrastructure( + this IServiceCollection services, + IConfiguration configuration + ) { // Строка подключения читается лениво внутри лямбды (а не в локальную переменную сразу), иначе // в тестах WebApplicationFactory.ConfigureAppConfiguration (Testcontainers-порт) не успевает // примениться до регистрации DbContext — окажется закэширован дефолт из appsettings.json. - services.AddDbContext(options => options.UseNpgsql( - configuration["ConnectionStrings:Default"] - ?? throw new InvalidOperationException("Строка подключения 'ConnectionStrings:Default' не сконфигурирована."))); + services.AddDbContext(options => + options.UseNpgsql( + configuration["ConnectionStrings:Default"] + ?? throw new InvalidOperationException( + "Строка подключения 'ConnectionStrings:Default' не сконфигурирована." + ) + ) + ); services.AddScoped(sp => sp.GetRequiredService()); services @@ -53,10 +61,13 @@ public static class DependencyInjection .AddDefaultTokenProviders(); services.Configure(configuration.GetSection(JwtOptions.SectionName)); - services.Configure(configuration.GetSection(AdminSeedOptions.SectionName)); + services.Configure( + configuration.GetSection(AdminSeedOptions.SectionName) + ); services.Configure(configuration.GetSection(RolesOptions.SectionName)); - var jwtOptions = configuration.GetSection(JwtOptions.SectionName).Get() + var jwtOptions = + configuration.GetSection(JwtOptions.SectionName).Get() ?? throw new InvalidOperationException("Секция конфигурации 'Jwt' не задана."); services @@ -70,7 +81,9 @@ public static class DependencyInjection ValidateAudience = true, ValidAudience = jwtOptions.Audience, ValidateIssuerSigningKey = true, - IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.SigningKey)), + IssuerSigningKey = new SymmetricSecurityKey( + Encoding.UTF8.GetBytes(jwtOptions.SigningKey) + ), ValidateLifetime = true, ClockSkew = TimeSpan.FromSeconds(30), }; @@ -82,7 +95,10 @@ public static class DependencyInjection OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; - if (!string.IsNullOrEmpty(accessToken) && context.HttpContext.Request.Path.StartsWithSegments("/hubs")) + if ( + !string.IsNullOrEmpty(accessToken) + && context.HttpContext.Request.Path.StartsWithSegments("/hubs") + ) context.Token = accessToken; return Task.CompletedTask; @@ -106,7 +122,10 @@ public static class DependencyInjection services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddSingleton< + IXuiConnectionStringBuilderResolver, + XuiConnectionStringBuilderResolver + >(); services.AddSingleton(); // Singleton: держит кэш per-node XUI-клиентов между запросами (см. XuiPanelGateway). @@ -123,14 +142,18 @@ public static class DependencyInjection services.AddScoped(sp => sp.GetRequiredService()); services.AddScoped(); - services.Configure(configuration.GetSection(TrafficRetentionOptions.SectionName)); + services.Configure( + configuration.GetSection(TrafficRetentionOptions.SectionName) + ); services.AddHostedService(); services.AddHostedService(); services.AddHostedService(); services.Configure(configuration.GetSection(TelegramOptions.SectionName)); - services.Configure(configuration.GetSection(FileStorageOptions.SectionName)); + services.Configure( + configuration.GetSection(FileStorageOptions.SectionName) + ); services.AddSingleton(); return services; diff --git a/backend/src/PnvPanel.Infrastructure/Identity/AppRole.cs b/backend/src/PnvPanel.Infrastructure/Identity/AppRole.cs index b9dc2d3..2515534 100644 --- a/backend/src/PnvPanel.Infrastructure/Identity/AppRole.cs +++ b/backend/src/PnvPanel.Infrastructure/Identity/AppRole.cs @@ -18,11 +18,8 @@ public class AppRole : IdentityRole public bool IsSystem { get; set; } - public AppRole() - { - } + public AppRole() { } - public AppRole(string name) : base(name) - { - } + public AppRole(string name) + : base(name) { } } diff --git a/backend/src/PnvPanel.Infrastructure/Identity/CurrentUser.cs b/backend/src/PnvPanel.Infrastructure/Identity/CurrentUser.cs index dea9fbc..ff1fd59 100644 --- a/backend/src/PnvPanel.Infrastructure/Identity/CurrentUser.cs +++ b/backend/src/PnvPanel.Infrastructure/Identity/CurrentUser.cs @@ -8,7 +8,9 @@ namespace PnvPanel.Infrastructure.Identity; /// В HTTP-запросах читает JWT-claims. В Telegram-боте (нет HttpContext) вызывающая сторона /// заранее задаёт пользователя через ICurrentUserSetter.SetUser(...) в рамках DI-scope апдейта. /// -internal sealed class CurrentUser(IHttpContextAccessor httpContextAccessor) : ICurrentUser, ICurrentUserSetter +internal sealed class CurrentUser(IHttpContextAccessor httpContextAccessor) + : ICurrentUser, + ICurrentUserSetter { private (Guid Id, string Name)? _override; @@ -18,7 +20,8 @@ internal sealed class CurrentUser(IHttpContextAccessor httpContextAccessor) : IC public string? UserName => _override?.Name ?? Principal?.FindFirstValue(ClaimTypes.Name); - public bool IsAuthenticated => _override is not null || (Principal?.Identity?.IsAuthenticated ?? false); + public bool IsAuthenticated => + _override is not null || (Principal?.Identity?.IsAuthenticated ?? false); public void SetUser(Guid userId, string userName) => _override = (userId, userName); diff --git a/backend/src/PnvPanel.Infrastructure/Identity/DbInitializer.cs b/backend/src/PnvPanel.Infrastructure/Identity/DbInitializer.cs index 917c6cd..c0548d4 100644 --- a/backend/src/PnvPanel.Infrastructure/Identity/DbInitializer.cs +++ b/backend/src/PnvPanel.Infrastructure/Identity/DbInitializer.cs @@ -16,14 +16,28 @@ public sealed class DbInitializer( AppDbContext dbContext, IOptions adminSeedOptions, IOptions rolesOptions, - ILogger logger) + ILogger logger +) { - private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true }; + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNameCaseInsensitive = true, + }; public async Task SeedAsync(CancellationToken cancellationToken = default) { - await EnsureRoleAsync(RoleNames.Admin, AppRole.UnlimitedMaxConfigs, AppRole.UnlimitedMaxIpLimit, isSystem: true); - await EnsureRoleAsync(RoleNames.User, rolesOptions.Value.DefaultUserMaxConfigs, rolesOptions.Value.DefaultUserMaxIpLimit, isSystem: true); + await EnsureRoleAsync( + RoleNames.Admin, + AppRole.UnlimitedMaxConfigs, + AppRole.UnlimitedMaxIpLimit, + isSystem: true + ); + await EnsureRoleAsync( + RoleNames.User, + rolesOptions.Value.DefaultUserMaxConfigs, + rolesOptions.Value.DefaultUserMaxIpLimit, + isSystem: true + ); await SeedAdminAsync(); await SeedClientAppsAsync(cancellationToken); } @@ -33,12 +47,18 @@ public sealed class DbInitializer( if (await roleManager.RoleExistsAsync(name)) return; - var role = new AppRole(name) { MaxConfigs = maxConfigs, MaxIpLimit = maxIpLimit, IsSystem = isSystem }; + var role = new AppRole(name) + { + MaxConfigs = maxConfigs, + MaxIpLimit = maxIpLimit, + IsSystem = isSystem, + }; var result = await roleManager.CreateAsync(role); if (!result.Succeeded) { throw new InvalidOperationException( - $"Failed to create role '{name}': {string.Join(", ", result.Errors.Select(e => e.Description))}"); + $"Failed to create role '{name}': {string.Join(", ", result.Errors.Select(e => e.Description))}" + ); } logger.LogInformation("Created system role {RoleName}", name); @@ -47,9 +67,14 @@ public sealed class DbInitializer( private async Task SeedAdminAsync() { var options = adminSeedOptions.Value; - if (string.IsNullOrWhiteSpace(options.Username) || string.IsNullOrWhiteSpace(options.Password)) + if ( + string.IsNullOrWhiteSpace(options.Username) + || string.IsNullOrWhiteSpace(options.Password) + ) { - logger.LogWarning("AdminSeed__Username/AdminSeed__Password not set — admin account not created"); + logger.LogWarning( + "AdminSeed__Username/AdminSeed__Password not set — admin account not created" + ); return; } @@ -68,7 +93,8 @@ public sealed class DbInitializer( if (!createResult.Succeeded) { throw new InvalidOperationException( - $"Failed to create admin account: {string.Join(", ", createResult.Errors.Select(e => e.Description))}"); + $"Failed to create admin account: {string.Join(", ", createResult.Errors.Select(e => e.Description))}" + ); } await userManager.AddToRoleAsync(admin, RoleNames.Admin); @@ -94,12 +120,21 @@ public sealed class DbInitializer( { if (!Enum.TryParse(entry.OperatingSystem, ignoreCase: true, out var os)) { - logger.LogWarning("Unknown OS '{Os}' in client app catalog seed — skipped", entry.OperatingSystem); + logger.LogWarning( + "Unknown OS '{Os}' in client app catalog seed — skipped", + entry.OperatingSystem + ); continue; } var app = ClientApp.Create( - entry.Name, new Uri(entry.DownloadUrl, UriKind.Absolute), os, entry.Description, iconUrl: null, entry.SortOrder); + entry.Name, + new Uri(entry.DownloadUrl, UriKind.Absolute), + os, + entry.Description, + iconUrl: null, + entry.SortOrder + ); dbContext.ClientApps.Add(app); } @@ -108,5 +143,11 @@ public sealed class DbInitializer( } private sealed record ClientAppSeedEntry( - string Name, string OperatingSystem, string DownloadUrl, string? Description, int SortOrder, bool IsEnabled); + string Name, + string OperatingSystem, + string DownloadUrl, + string? Description, + int SortOrder, + bool IsEnabled + ); } diff --git a/backend/src/PnvPanel.Infrastructure/Identity/DbInitializerExtensions.cs b/backend/src/PnvPanel.Infrastructure/Identity/DbInitializerExtensions.cs index e1ed44a..8bba308 100644 --- a/backend/src/PnvPanel.Infrastructure/Identity/DbInitializerExtensions.cs +++ b/backend/src/PnvPanel.Infrastructure/Identity/DbInitializerExtensions.cs @@ -4,7 +4,10 @@ namespace PnvPanel.Infrastructure.Identity; public static class DbInitializerExtensions { - public static async Task SeedDataAsync(this IServiceProvider services, CancellationToken cancellationToken = default) + public static async Task SeedDataAsync( + this IServiceProvider services, + CancellationToken cancellationToken = default + ) { await using var scope = services.CreateAsyncScope(); var initializer = scope.ServiceProvider.GetRequiredService(); diff --git a/backend/src/PnvPanel.Infrastructure/Identity/IdentityService.cs b/backend/src/PnvPanel.Infrastructure/Identity/IdentityService.cs index 7c78afe..3315c5f 100644 --- a/backend/src/PnvPanel.Infrastructure/Identity/IdentityService.cs +++ b/backend/src/PnvPanel.Infrastructure/Identity/IdentityService.cs @@ -7,10 +7,17 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Infrastructure.Identity; -internal sealed class IdentityService(UserManager userManager, SignInManager signInManager, RoleManager roleManager) - : IIdentityService +internal sealed class IdentityService( + UserManager userManager, + SignInManager signInManager, + RoleManager roleManager +) : IIdentityService { - public async Task> CreateUserAsync(string userName, string password, CancellationToken cancellationToken) + public async Task> CreateUserAsync( + string userName, + string password, + CancellationToken cancellationToken + ) { var user = new AppUser { @@ -22,18 +29,27 @@ internal sealed class IdentityService(UserManager userManager, SignInMa if (!createResult.Succeeded) { - return createResult.Errors.Any(e => e.Code == nameof(IdentityErrorDescriber.DuplicateUserName)) + return createResult.Errors.Any(e => + e.Code == nameof(IdentityErrorDescriber.DuplicateUserName) + ) ? Result.Failure(AuthErrors.DuplicateUserName) - : Result.Failure(Error.Validation( - "Auth.RegistrationFailed", - string.Join("; ", createResult.Errors.Select(e => e.Description)))); + : Result.Failure( + Error.Validation( + "Auth.RegistrationFailed", + string.Join("; ", createResult.Errors.Select(e => e.Description)) + ) + ); } await userManager.AddToRoleAsync(user, RoleNames.User); return Result.Success(user.Id); } - public async Task> ValidateCredentialsAsync(string userName, string password, CancellationToken cancellationToken) + public async Task> ValidateCredentialsAsync( + string userName, + string password, + CancellationToken cancellationToken + ) { var user = await userManager.FindByNameAsync(userName); if (user is null) @@ -42,7 +58,11 @@ internal sealed class IdentityService(UserManager userManager, SignInMa if (user.IsBlocked) return Result.Failure(AuthErrors.UserBlocked); - var checkResult = await signInManager.CheckPasswordSignInAsync(user, password, lockoutOnFailure: true); + var checkResult = await signInManager.CheckPasswordSignInAsync( + user, + password, + lockoutOnFailure: true + ); if (checkResult.IsLockedOut) return Result.Failure(AuthErrors.LockedOut); if (!checkResult.Succeeded) @@ -52,7 +72,10 @@ internal sealed class IdentityService(UserManager userManager, SignInMa return Result.Success(new AuthenticatedUser(user.Id, user.UserName!, roleName)); } - public async Task GetProfileAsync(Guid userId, CancellationToken cancellationToken) + public async Task GetProfileAsync( + Guid userId, + CancellationToken cancellationToken + ) { var user = await userManager.FindByIdAsync(userId.ToString()); if (user is null) @@ -60,11 +83,24 @@ internal sealed class IdentityService(UserManager userManager, SignInMa var role = await GetPrimaryRoleAsync(user); return new CurrentUserProfile( - user.Id, user.UserName!, role.Id, role.Name!, user.IsActivated, user.IsBlocked, role.MaxConfigs, role.MaxIpLimit, - user.SubscriptionToken); + user.Id, + user.UserName!, + role.Id, + role.Name!, + user.IsActivated, + user.IsBlocked, + role.MaxConfigs, + role.MaxIpLimit, + user.SubscriptionToken + ); } - public async Task ChangePasswordAsync(Guid userId, string currentPassword, string newPassword, CancellationToken cancellationToken) + public async Task ChangePasswordAsync( + Guid userId, + string currentPassword, + string newPassword, + CancellationToken cancellationToken + ) { var user = await userManager.FindByIdAsync(userId.ToString()); if (user is null) @@ -73,12 +109,19 @@ internal sealed class IdentityService(UserManager userManager, SignInMa var result = await userManager.ChangePasswordAsync(user, currentPassword, newPassword); return result.Succeeded ? Result.Success() - : Result.Failure(Error.Validation( - "Auth.PasswordChangeFailed", - string.Join("; ", result.Errors.Select(e => e.Description)))); + : Result.Failure( + Error.Validation( + "Auth.PasswordChangeFailed", + string.Join("; ", result.Errors.Select(e => e.Description)) + ) + ); } - public async Task ChangeUserNameAsync(Guid userId, string newUserName, CancellationToken cancellationToken) + public async Task ChangeUserNameAsync( + Guid userId, + string newUserName, + CancellationToken cancellationToken + ) { var user = await userManager.FindByIdAsync(userId.ToString()); if (user is null) @@ -90,12 +133,19 @@ internal sealed class IdentityService(UserManager userManager, SignInMa return result.Errors.Any(e => e.Code == nameof(IdentityErrorDescriber.DuplicateUserName)) ? Result.Failure(AuthErrors.DuplicateUserName) - : Result.Failure(Error.Validation( - "Auth.UserNameChangeFailed", - string.Join("; ", result.Errors.Select(e => e.Description)))); + : Result.Failure( + Error.Validation( + "Auth.UserNameChangeFailed", + string.Join("; ", result.Errors.Select(e => e.Description)) + ) + ); } - public async Task ActivateUserAsync(Guid userId, Guid activatedBy, CancellationToken cancellationToken) + public async Task ActivateUserAsync( + Guid userId, + Guid activatedBy, + CancellationToken cancellationToken + ) { var user = await userManager.FindByIdAsync(userId.ToString()); if (user is null) @@ -110,13 +160,16 @@ internal sealed class IdentityService(UserManager userManager, SignInMa return Result.Success(); } - public async Task> GetUserNamesAsync(IReadOnlyCollection userIds, CancellationToken cancellationToken) + public async Task> GetUserNamesAsync( + IReadOnlyCollection userIds, + CancellationToken cancellationToken + ) { if (userIds.Count == 0) return new Dictionary(); - return await userManager.Users - .Where(u => userIds.Contains(u.Id)) + return await userManager + .Users.Where(u => userIds.Contains(u.Id)) .ToDictionaryAsync(u => u.Id, u => u.UserName!, cancellationToken); } @@ -129,13 +182,21 @@ internal sealed class IdentityService(UserManager userManager, SignInMa var result = await userManager.DeleteAsync(user); return result.Succeeded ? Result.Success() - : Result.Failure(Error.Failure( - "Auth.DeleteAccountFailed", string.Join("; ", result.Errors.Select(e => e.Description)))); + : Result.Failure( + Error.Failure( + "Auth.DeleteAccountFailed", + string.Join("; ", result.Errors.Select(e => e.Description)) + ) + ); } - public async Task FindUserIdBySubscriptionTokenAsync(string token, CancellationToken cancellationToken) + public async Task FindUserIdBySubscriptionTokenAsync( + string token, + CancellationToken cancellationToken + ) { - var user = await userManager.Users.AsNoTracking() + var user = await userManager + .Users.AsNoTracking() .FirstOrDefaultAsync(u => u.SubscriptionToken == token, cancellationToken); return user?.Id; } @@ -161,7 +222,11 @@ internal sealed class IdentityService(UserManager userManager, SignInMa return Result.Success(); } - public async Task ResetPasswordAsync(Guid userId, string newPassword, CancellationToken cancellationToken) + public async Task ResetPasswordAsync( + Guid userId, + string newPassword, + CancellationToken cancellationToken + ) { var user = await userManager.FindByIdAsync(userId.ToString()); if (user is null) @@ -172,11 +237,20 @@ internal sealed class IdentityService(UserManager userManager, SignInMa return result.Succeeded ? Result.Success() - : Result.Failure(Error.Validation( - "Auth.PasswordResetFailed", string.Join("; ", result.Errors.Select(e => e.Description)))); + : Result.Failure( + Error.Validation( + "Auth.PasswordResetFailed", + string.Join("; ", result.Errors.Select(e => e.Description)) + ) + ); } - public async Task> ListUsersAsync(int page, int pageSize, string? search, CancellationToken cancellationToken) + public async Task> ListUsersAsync( + int page, + int pageSize, + string? search, + CancellationToken cancellationToken + ) { var query = userManager.Users.AsNoTracking(); if (!string.IsNullOrWhiteSpace(search)) @@ -193,7 +267,16 @@ internal sealed class IdentityService(UserManager userManager, SignInMa foreach (var user in users) { var roleName = await GetPrimaryRoleNameAsync(user); - items.Add(new UserSummaryDto(user.Id, user.UserName!, roleName, user.IsActivated, user.IsBlocked, user.ActivatedAt)); + items.Add( + new UserSummaryDto( + user.Id, + user.UserName!, + roleName, + user.IsActivated, + user.IsBlocked, + user.ActivatedAt + ) + ); } return new PagedList(items, total, page, pageSize); @@ -206,16 +289,27 @@ internal sealed class IdentityService(UserManager userManager, SignInMa return new UserStatsDto(total, activated); } - public async Task LinkTelegramAsync(Guid userId, long telegramUserId, string? telegramUsername, CancellationToken cancellationToken) + public async Task LinkTelegramAsync( + Guid userId, + long telegramUserId, + string? telegramUsername, + CancellationToken cancellationToken + ) { var user = await userManager.FindByIdAsync(userId.ToString()); if (user is null) return Result.Failure(AuthErrors.Unauthorized); - var alreadyLinked = await userManager.Users.AsNoTracking() + var alreadyLinked = await userManager + .Users.AsNoTracking() .AnyAsync(u => u.TelegramUserId == telegramUserId && u.Id != userId, cancellationToken); if (alreadyLinked) - return Result.Failure(Error.Conflict("Telegram.AlreadyLinked", "Этот Telegram уже привязан к другому аккаунту.")); + return Result.Failure( + Error.Conflict( + "Telegram.AlreadyLinked", + "Этот Telegram уже привязан к другому аккаунту." + ) + ); user.TelegramUserId = telegramUserId; user.TelegramUsername = telegramUsername; @@ -237,14 +331,21 @@ internal sealed class IdentityService(UserManager userManager, SignInMa return Result.Success(); } - public async Task FindUserIdByTelegramUserIdAsync(long telegramUserId, CancellationToken cancellationToken) + public async Task FindUserIdByTelegramUserIdAsync( + long telegramUserId, + CancellationToken cancellationToken + ) { - var user = await userManager.Users.AsNoTracking() + var user = await userManager + .Users.AsNoTracking() .FirstOrDefaultAsync(u => u.TelegramUserId == telegramUserId, cancellationToken); return user?.Id; } - public async Task GetTelegramLinkInfoAsync(Guid userId, CancellationToken cancellationToken) + public async Task GetTelegramLinkInfoAsync( + Guid userId, + CancellationToken cancellationToken + ) { var user = await userManager.FindByIdAsync(userId.ToString()); return user?.TelegramUserId is not null @@ -252,9 +353,12 @@ internal sealed class IdentityService(UserManager userManager, SignInMa : new TelegramLinkInfo(false, null, null); } - public async Task> GetActivatedLinkedTelegramUserIdsAsync(CancellationToken cancellationToken) + public async Task> GetActivatedLinkedTelegramUserIdsAsync( + CancellationToken cancellationToken + ) { - return await userManager.Users.AsNoTracking() + return await userManager + .Users.AsNoTracking() .Where(u => u.TelegramUserId != null && u.IsActivated && !u.IsBlocked) .Select(u => u.TelegramUserId!.Value) .ToListAsync(cancellationToken); @@ -273,5 +377,6 @@ internal sealed class IdentityService(UserManager userManager, SignInMa ?? throw new InvalidOperationException($"Роль '{roleName}' не найдена."); } - private static string GenerateSubscriptionToken() => Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); + private static string GenerateSubscriptionToken() => + Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); } diff --git a/backend/src/PnvPanel.Infrastructure/Identity/JwtTokenService.cs b/backend/src/PnvPanel.Infrastructure/Identity/JwtTokenService.cs index 4d03e40..81a213a 100644 --- a/backend/src/PnvPanel.Infrastructure/Identity/JwtTokenService.cs +++ b/backend/src/PnvPanel.Infrastructure/Identity/JwtTokenService.cs @@ -11,7 +11,9 @@ internal sealed class JwtTokenService(IOptions options) : IJwtTokenS { private readonly JwtOptions _options = options.Value; - public (string AccessToken, DateTimeOffset ExpiresAt) GenerateAccessToken(AuthenticatedUser user) + public (string AccessToken, DateTimeOffset ExpiresAt) GenerateAccessToken( + AuthenticatedUser user + ) { var expiresAt = DateTimeOffset.UtcNow.AddMinutes(_options.AccessTokenMinutes); @@ -32,7 +34,8 @@ internal sealed class JwtTokenService(IOptions options) : IJwtTokenS audience: _options.Audience, claims: claims, expires: expiresAt.UtcDateTime, - signingCredentials: credentials); + signingCredentials: credentials + ); return (new JwtSecurityTokenHandler().WriteToken(token), expiresAt); } diff --git a/backend/src/PnvPanel.Infrastructure/Identity/RefreshTokenService.cs b/backend/src/PnvPanel.Infrastructure/Identity/RefreshTokenService.cs index 48a1097..802d98c 100644 --- a/backend/src/PnvPanel.Infrastructure/Identity/RefreshTokenService.cs +++ b/backend/src/PnvPanel.Infrastructure/Identity/RefreshTokenService.cs @@ -9,32 +9,44 @@ using PnvPanel.Infrastructure.Persistence; namespace PnvPanel.Infrastructure.Identity; -internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions options) : IRefreshTokenService +internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions options) + : IRefreshTokenService { private readonly JwtOptions _options = options.Value; - public async Task IssueAsync(Guid userId, CancellationToken cancellationToken) + public async Task IssueAsync( + Guid userId, + CancellationToken cancellationToken + ) { var rawToken = GenerateRawToken(); var expiresAt = DateTimeOffset.UtcNow.AddDays(_options.RefreshTokenDays); - dbContext.RefreshTokens.Add(new RefreshToken - { - Id = Guid.NewGuid(), - UserId = userId, - TokenHash = Hash(rawToken), - ExpiresAt = expiresAt, - CreatedAt = DateTimeOffset.UtcNow, - }); + dbContext.RefreshTokens.Add( + new RefreshToken + { + Id = Guid.NewGuid(), + UserId = userId, + TokenHash = Hash(rawToken), + ExpiresAt = expiresAt, + CreatedAt = DateTimeOffset.UtcNow, + } + ); await dbContext.SaveChangesAsync(cancellationToken); return new IssuedRefreshToken(rawToken, expiresAt); } - public async Task> RotateAsync(string rawToken, CancellationToken cancellationToken) + public async Task> RotateAsync( + string rawToken, + CancellationToken cancellationToken + ) { var hash = Hash(rawToken); - var existing = await dbContext.RefreshTokens.SingleOrDefaultAsync(t => t.TokenHash == hash, cancellationToken); + var existing = await dbContext.RefreshTokens.SingleOrDefaultAsync( + t => t.TokenHash == hash, + cancellationToken + ); if (existing is null) return Result.Failure(AuthErrors.InvalidRefreshToken); @@ -56,14 +68,16 @@ internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions t.TokenHash == hash, cancellationToken); + var existing = await dbContext.RefreshTokens.SingleOrDefaultAsync( + t => t.TokenHash == hash, + cancellationToken + ); if (existing is null || existing.RevokedAt is not null) return; @@ -83,8 +100,8 @@ internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions t.UserId == userId && t.RevokedAt == null) + var activeTokens = await dbContext + .RefreshTokens.Where(t => t.UserId == userId && t.RevokedAt == null) .ToListAsync(cancellationToken); foreach (var token in activeTokens) @@ -93,7 +110,9 @@ internal sealed class RefreshTokenService(AppDbContext dbContext, IOptions Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); + private static string GenerateRawToken() => + Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); - private static string Hash(string rawToken) => Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(rawToken))); + private static string Hash(string rawToken) => + Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(rawToken))); } diff --git a/backend/src/PnvPanel.Infrastructure/Identity/RoleService.cs b/backend/src/PnvPanel.Infrastructure/Identity/RoleService.cs index 23cdf8f..9c55a23 100644 --- a/backend/src/PnvPanel.Infrastructure/Identity/RoleService.cs +++ b/backend/src/PnvPanel.Infrastructure/Identity/RoleService.cs @@ -7,25 +7,47 @@ using PnvPanel.Application.Common.Models; namespace PnvPanel.Infrastructure.Identity; -internal sealed class RoleService(RoleManager roleManager, UserManager userManager) : IRoleService +internal sealed class RoleService( + RoleManager roleManager, + UserManager userManager +) : IRoleService { - public async Task> CreateRoleAsync(string name, int maxConfigs, int maxIpLimit, CancellationToken cancellationToken) + public async Task> CreateRoleAsync( + string name, + int maxConfigs, + int maxIpLimit, + CancellationToken cancellationToken + ) { if (await roleManager.RoleExistsAsync(name)) return Result.Failure(RoleErrors.DuplicateName); - var role = new AppRole(name) { MaxConfigs = maxConfigs, MaxIpLimit = maxIpLimit, IsSystem = false }; + var role = new AppRole(name) + { + MaxConfigs = maxConfigs, + MaxIpLimit = maxIpLimit, + IsSystem = false, + }; var result = await roleManager.CreateAsync(role); if (!result.Succeeded) { - return Result.Failure(Error.Validation( - "Roles.CreateFailed", string.Join("; ", result.Errors.Select(e => e.Description)))); + return Result.Failure( + Error.Validation( + "Roles.CreateFailed", + string.Join("; ", result.Errors.Select(e => e.Description)) + ) + ); } return Result.Success(ToDto(role)); } - public async Task> UpdateRoleAsync(Guid roleId, int maxConfigs, int maxIpLimit, CancellationToken cancellationToken) + public async Task> UpdateRoleAsync( + Guid roleId, + int maxConfigs, + int maxIpLimit, + CancellationToken cancellationToken + ) { var role = await roleManager.FindByIdAsync(roleId.ToString()); if (role is null) @@ -57,13 +79,17 @@ internal sealed class RoleService(RoleManager roleManager, UserManager< public async Task> ListRolesAsync(CancellationToken cancellationToken) { - return await roleManager.Roles - .OrderBy(r => r.Name) + return await roleManager + .Roles.OrderBy(r => r.Name) .Select(r => new RoleDto(r.Id, r.Name!, r.MaxConfigs, r.MaxIpLimit, r.IsSystem)) .ToListAsync(cancellationToken); } - public async Task ChangeUserRoleAsync(Guid userId, Guid roleId, CancellationToken cancellationToken) + public async Task ChangeUserRoleAsync( + Guid userId, + Guid roleId, + CancellationToken cancellationToken + ) { var user = await userManager.FindByIdAsync(userId.ToString()); if (user is null) @@ -81,5 +107,6 @@ internal sealed class RoleService(RoleManager roleManager, UserManager< return Result.Success(); } - private static RoleDto ToDto(AppRole role) => new(role.Id, role.Name!, role.MaxConfigs, role.MaxIpLimit, role.IsSystem); + private static RoleDto ToDto(AppRole role) => + new(role.Id, role.Name!, role.MaxConfigs, role.MaxIpLimit, role.IsSystem); } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/AppDbContext.cs b/backend/src/PnvPanel.Infrastructure/Persistence/AppDbContext.cs index 6928178..bd9d5fe 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/AppDbContext.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/AppDbContext.cs @@ -19,7 +19,8 @@ namespace PnvPanel.Infrastructure.Persistence; /// (добавляются по мере реализации фич, см. docs/roadmap.md). /// public class AppDbContext(DbContextOptions options) - : IdentityDbContext(options), IAppDbContext + : IdentityDbContext(options), + IAppDbContext { public DbSet RefreshTokens => Set(); diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/ClientAppConfiguration.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/ClientAppConfiguration.cs index 01b28bf..3db4a72 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/ClientAppConfiguration.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/ClientAppConfiguration.cs @@ -13,7 +13,8 @@ public class ClientAppConfiguration : IEntityTypeConfiguration builder.Property(x => x.Name).IsRequired().HasMaxLength(100); - builder.Property(x => x.DownloadUrl) + builder + .Property(x => x.DownloadUrl) .IsRequired() .HasMaxLength(500) .HasConversion(uri => uri.ToString(), s => new Uri(s, UriKind.Absolute)); diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/NodeConfiguration.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/NodeConfiguration.cs index d02ddd8..8fb374a 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/NodeConfiguration.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/NodeConfiguration.cs @@ -13,7 +13,8 @@ public class NodeConfiguration : IEntityTypeConfiguration builder.Property(x => x.Name).IsRequired().HasMaxLength(100); - builder.Property(x => x.BaseAddress) + builder + .Property(x => x.BaseAddress) .IsRequired() .HasMaxLength(500) .HasConversion(uri => uri.ToString(), s => new Uri(s, UriKind.Absolute)); @@ -21,10 +22,20 @@ public class NodeConfiguration : IEntityTypeConfiguration builder.Property(x => x.Location).HasMaxLength(100); builder.Property(x => x.Status).HasConversion().HasMaxLength(32); - builder.OwnsOne(x => x.Credentials, credentials => - { - credentials.Property(c => c.Username).HasColumnName("CredentialsUsername").IsRequired().HasMaxLength(200); - credentials.Property(c => c.ProtectedPassword).HasColumnName("CredentialsProtectedPassword").IsRequired(); - }); + builder.OwnsOne( + x => x.Credentials, + credentials => + { + credentials + .Property(c => c.Username) + .HasColumnName("CredentialsUsername") + .IsRequired() + .HasMaxLength(200); + credentials + .Property(c => c.ProtectedPassword) + .HasColumnName("CredentialsProtectedPassword") + .IsRequired(); + } + ); } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs index ad88d83..b36f1e7 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Configurations/RefreshTokenConfiguration.cs @@ -11,9 +11,7 @@ public class RefreshTokenConfiguration : IEntityTypeConfiguration builder.ToTable("RefreshTokens"); builder.HasKey(x => x.Id); - builder.Property(x => x.TokenHash) - .IsRequired() - .HasMaxLength(128); + builder.Property(x => x.TokenHash).IsRequired().HasMaxLength(128); builder.HasIndex(x => x.TokenHash).IsUnique(); builder.HasIndex(x => x.UserId); diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/MigrationExtensions.cs b/backend/src/PnvPanel.Infrastructure/Persistence/MigrationExtensions.cs index dc86a3e..f252fb7 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/MigrationExtensions.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/MigrationExtensions.cs @@ -8,7 +8,10 @@ namespace PnvPanel.Infrastructure.Persistence; /// public static class MigrationExtensions { - public static async Task ApplyMigrationsAsync(this IServiceProvider services, CancellationToken cancellationToken = default) + public static async Task ApplyMigrationsAsync( + this IServiceProvider services, + CancellationToken cancellationToken = default + ) { await using var scope = services.CreateAsyncScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701161345_InitialCreate.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701161345_InitialCreate.cs index 3152b3c..22e11d3 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701161345_InitialCreate.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701161345_InitialCreate.cs @@ -8,15 +8,9 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations public partial class InitialCreate : Migration { /// - protected override void Up(MigrationBuilder migrationBuilder) - { - - } + protected override void Up(MigrationBuilder migrationBuilder) { } /// - protected override void Down(MigrationBuilder migrationBuilder) - { - - } + protected override void Down(MigrationBuilder migrationBuilder) { } } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701171444_AddIdentity.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701171444_AddIdentity.cs index 53bbbdc..950a2f7 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701171444_AddIdentity.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701171444_AddIdentity.cs @@ -19,14 +19,23 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations Id = table.Column(type: "uuid", nullable: false), MaxConfigs = table.Column(type: "integer", nullable: false), IsSystem = table.Column(type: "boolean", nullable: false), - Name = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - ConcurrencyStamp = table.Column(type: "text", nullable: true) + Name = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: true + ), + NormalizedName = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: true + ), + ConcurrencyStamp = table.Column(type: "text", nullable: true), }, constraints: table => { table.PrimaryKey("PK_AspNetRoles", x => x.Id); - }); + } + ); migrationBuilder.CreateTable( name: "AspNetUsers", @@ -34,12 +43,31 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations { Id = table.Column(type: "uuid", nullable: false), IsActivated = table.Column(type: "boolean", nullable: false), - ActivatedAt = table.Column(type: "timestamp with time zone", nullable: true), + ActivatedAt = table.Column( + type: "timestamp with time zone", + nullable: true + ), ActivatedBy = table.Column(type: "uuid", nullable: true), - UserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedUserName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - Email = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), - NormalizedEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + UserName = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: true + ), + NormalizedUserName = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: true + ), + Email = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: true + ), + NormalizedEmail = table.Column( + type: "character varying(256)", + maxLength: 256, + nullable: true + ), EmailConfirmed = table.Column(type: "boolean", nullable: false), PasswordHash = table.Column(type: "text", nullable: true), SecurityStamp = table.Column(type: "text", nullable: true), @@ -47,14 +75,18 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations PhoneNumber = table.Column(type: "text", nullable: true), PhoneNumberConfirmed = table.Column(type: "boolean", nullable: false), TwoFactorEnabled = table.Column(type: "boolean", nullable: false), - LockoutEnd = table.Column(type: "timestamp with time zone", nullable: true), + LockoutEnd = table.Column( + type: "timestamp with time zone", + nullable: true + ), LockoutEnabled = table.Column(type: "boolean", nullable: false), - AccessFailedCount = table.Column(type: "integer", nullable: false) + AccessFailedCount = table.Column(type: "integer", nullable: false), }, constraints: table => { table.PrimaryKey("PK_AspNetUsers", x => x.Id); - }); + } + ); migrationBuilder.CreateTable( name: "RefreshTokens", @@ -62,26 +94,44 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations { Id = table.Column(type: "uuid", nullable: false), UserId = table.Column(type: "uuid", nullable: false), - TokenHash = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - RevokedAt = table.Column(type: "timestamp with time zone", nullable: true), - ReplacedByTokenHash = table.Column(type: "text", nullable: true) + TokenHash = table.Column( + type: "character varying(128)", + maxLength: 128, + nullable: false + ), + ExpiresAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), + CreatedAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), + RevokedAt = table.Column( + type: "timestamp with time zone", + nullable: true + ), + ReplacedByTokenHash = table.Column(type: "text", nullable: true), }, constraints: table => { table.PrimaryKey("PK_RefreshTokens", x => x.Id); - }); + } + ); migrationBuilder.CreateTable( name: "AspNetRoleClaims", columns: table => new { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Id = table + .Column(type: "integer", nullable: false) + .Annotation( + "Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.IdentityByDefaultColumn + ), RoleId = table.Column(type: "uuid", nullable: false), ClaimType = table.Column(type: "text", nullable: true), - ClaimValue = table.Column(type: "text", nullable: true) + ClaimValue = table.Column(type: "text", nullable: true), }, constraints: table => { @@ -91,18 +141,24 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations column: x => x.RoleId, principalTable: "AspNetRoles", principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateTable( name: "AspNetUserClaims", columns: table => new { - Id = table.Column(type: "integer", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Id = table + .Column(type: "integer", nullable: false) + .Annotation( + "Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.IdentityByDefaultColumn + ), UserId = table.Column(type: "uuid", nullable: false), ClaimType = table.Column(type: "text", nullable: true), - ClaimValue = table.Column(type: "text", nullable: true) + ClaimValue = table.Column(type: "text", nullable: true), }, constraints: table => { @@ -112,8 +168,10 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations column: x => x.UserId, principalTable: "AspNetUsers", principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateTable( name: "AspNetUserLogins", @@ -122,25 +180,30 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations LoginProvider = table.Column(type: "text", nullable: false), ProviderKey = table.Column(type: "text", nullable: false), ProviderDisplayName = table.Column(type: "text", nullable: true), - UserId = table.Column(type: "uuid", nullable: false) + UserId = table.Column(type: "uuid", nullable: false), }, constraints: table => { - table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.PrimaryKey( + "PK_AspNetUserLogins", + x => new { x.LoginProvider, x.ProviderKey } + ); table.ForeignKey( name: "FK_AspNetUserLogins_AspNetUsers_UserId", column: x => x.UserId, principalTable: "AspNetUsers", principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateTable( name: "AspNetUserRoles", columns: table => new { UserId = table.Column(type: "uuid", nullable: false), - RoleId = table.Column(type: "uuid", nullable: false) + RoleId = table.Column(type: "uuid", nullable: false), }, constraints: table => { @@ -150,14 +213,17 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations column: x => x.RoleId, principalTable: "AspNetRoles", principalColumn: "Id", - onDelete: ReferentialAction.Cascade); + onDelete: ReferentialAction.Cascade + ); table.ForeignKey( name: "FK_AspNetUserRoles_AspNetUsers_UserId", column: x => x.UserId, principalTable: "AspNetUsers", principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateTable( name: "AspNetUserTokens", @@ -166,94 +232,105 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations UserId = table.Column(type: "uuid", nullable: false), LoginProvider = table.Column(type: "text", nullable: false), Name = table.Column(type: "text", nullable: false), - Value = table.Column(type: "text", nullable: true) + Value = table.Column(type: "text", nullable: true), }, constraints: table => { - table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.PrimaryKey( + "PK_AspNetUserTokens", + x => new + { + x.UserId, + x.LoginProvider, + x.Name, + } + ); table.ForeignKey( name: "FK_AspNetUserTokens_AspNetUsers_UserId", column: x => x.UserId, principalTable: "AspNetUsers", principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - }); + onDelete: ReferentialAction.Cascade + ); + } + ); migrationBuilder.CreateIndex( name: "IX_AspNetRoleClaims_RoleId", table: "AspNetRoleClaims", - column: "RoleId"); + column: "RoleId" + ); migrationBuilder.CreateIndex( name: "RoleNameIndex", table: "AspNetRoles", column: "NormalizedName", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "IX_AspNetUserClaims_UserId", table: "AspNetUserClaims", - column: "UserId"); + column: "UserId" + ); migrationBuilder.CreateIndex( name: "IX_AspNetUserLogins_UserId", table: "AspNetUserLogins", - column: "UserId"); + column: "UserId" + ); migrationBuilder.CreateIndex( name: "IX_AspNetUserRoles_RoleId", table: "AspNetUserRoles", - column: "RoleId"); + column: "RoleId" + ); migrationBuilder.CreateIndex( name: "EmailIndex", table: "AspNetUsers", - column: "NormalizedEmail"); + column: "NormalizedEmail" + ); migrationBuilder.CreateIndex( name: "UserNameIndex", table: "AspNetUsers", column: "NormalizedUserName", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "IX_RefreshTokens_TokenHash", table: "RefreshTokens", column: "TokenHash", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "IX_RefreshTokens_UserId", table: "RefreshTokens", - column: "UserId"); + column: "UserId" + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "AspNetRoleClaims"); + migrationBuilder.DropTable(name: "AspNetRoleClaims"); - migrationBuilder.DropTable( - name: "AspNetUserClaims"); + migrationBuilder.DropTable(name: "AspNetUserClaims"); - migrationBuilder.DropTable( - name: "AspNetUserLogins"); + migrationBuilder.DropTable(name: "AspNetUserLogins"); - migrationBuilder.DropTable( - name: "AspNetUserRoles"); + migrationBuilder.DropTable(name: "AspNetUserRoles"); - migrationBuilder.DropTable( - name: "AspNetUserTokens"); + migrationBuilder.DropTable(name: "AspNetUserTokens"); - migrationBuilder.DropTable( - name: "RefreshTokens"); + migrationBuilder.DropTable(name: "RefreshTokens"); - migrationBuilder.DropTable( - name: "AspNetRoles"); + migrationBuilder.DropTable(name: "AspNetRoles"); - migrationBuilder.DropTable( - name: "AspNetUsers"); + migrationBuilder.DropTable(name: "AspNetUsers"); } } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701175024_AddActivationRequests.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701175024_AddActivationRequests.cs index b3fe79d..b714e8f 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701175024_AddActivationRequests.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701175024_AddActivationRequests.cs @@ -17,29 +17,48 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations { Id = table.Column(type: "uuid", nullable: false), UserId = table.Column(type: "uuid", nullable: false), - Comment = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - Status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Comment = table.Column( + type: "character varying(500)", + maxLength: 500, + nullable: true + ), + Status = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: false + ), DecidedBy = table.Column(type: "uuid", nullable: true), - DecidedAt = table.Column(type: "timestamp with time zone", nullable: true), - RejectionReason = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + DecidedAt = table.Column( + type: "timestamp with time zone", + nullable: true + ), + RejectionReason = table.Column( + type: "character varying(500)", + maxLength: 500, + nullable: true + ), + CreatedAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), }, constraints: table => { table.PrimaryKey("PK_ActivationRequests", x => x.Id); - }); + } + ); migrationBuilder.CreateIndex( name: "IX_ActivationRequests_UserId_Status", table: "ActivationRequests", - columns: new[] { "UserId", "Status" }); + columns: new[] { "UserId", "Status" } + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "ActivationRequests"); + migrationBuilder.DropTable(name: "ActivationRequests"); } } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701181642_AddNodesAndInbounds.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701181642_AddNodesAndInbounds.cs index c72eb3d..0faa6be 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701181642_AddNodesAndInbounds.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701181642_AddNodesAndInbounds.cs @@ -17,56 +17,105 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations { Id = table.Column(type: "uuid", nullable: false), NodeId = table.Column(type: "uuid", nullable: false), - RemoteInboundId = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), - Protocol = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Remark = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), + RemoteInboundId = table.Column( + type: "character varying(64)", + maxLength: 64, + nullable: false + ), + Protocol = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: false + ), + Remark = table.Column( + type: "character varying(200)", + maxLength: 200, + nullable: false + ), Port = table.Column(type: "integer", nullable: false), IsPublished = table.Column(type: "boolean", nullable: false), - DisplayName = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + DisplayName = table.Column( + type: "character varying(100)", + maxLength: 100, + nullable: true + ), MaxClients = table.Column(type: "integer", nullable: true), AllowedRoleIds = table.Column(type: "uuid[]", nullable: false), - LastSyncAt = table.Column(type: "timestamp with time zone", nullable: true) + LastSyncAt = table.Column( + type: "timestamp with time zone", + nullable: true + ), }, constraints: table => { table.PrimaryKey("PK_Inbounds", x => x.Id); - }); + } + ); migrationBuilder.CreateTable( name: "Nodes", columns: table => new { Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - BaseAddress = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - CredentialsUsername = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - CredentialsProtectedPassword = table.Column(type: "text", nullable: false), - Location = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - Status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Name = table.Column( + type: "character varying(100)", + maxLength: 100, + nullable: false + ), + BaseAddress = table.Column( + type: "character varying(500)", + maxLength: 500, + nullable: false + ), + CredentialsUsername = table.Column( + type: "character varying(200)", + maxLength: 200, + nullable: false + ), + CredentialsProtectedPassword = table.Column( + type: "text", + nullable: false + ), + Location = table.Column( + type: "character varying(100)", + maxLength: 100, + nullable: true + ), + Status = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: false + ), IsEnabled = table.Column(type: "boolean", nullable: false), - LastSyncAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + LastSyncAt = table.Column( + type: "timestamp with time zone", + nullable: true + ), + CreatedAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), }, constraints: table => { table.PrimaryKey("PK_Nodes", x => x.Id); - }); + } + ); migrationBuilder.CreateIndex( name: "IX_Inbounds_NodeId_RemoteInboundId", table: "Inbounds", columns: new[] { "NodeId", "RemoteInboundId" }, - unique: true); + unique: true + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "Inbounds"); + migrationBuilder.DropTable(name: "Inbounds"); - migrationBuilder.DropTable( - name: "Nodes"); + migrationBuilder.DropTable(name: "Nodes"); } } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701185508_AddConfigsAndApps.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701185508_AddConfigsAndApps.cs index bc7b92e..f83d41c 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701185508_AddConfigsAndApps.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701185508_AddConfigsAndApps.cs @@ -16,25 +16,47 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations table: "AspNetUsers", type: "text", nullable: false, - defaultValue: ""); + defaultValue: "" + ); migrationBuilder.CreateTable( name: "ClientApps", columns: table => new { Id = table.Column(type: "uuid", nullable: false), - Name = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - DownloadUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: false), - OperatingSystem = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Description = table.Column(type: "character varying(300)", maxLength: 300, nullable: true), - IconUrl = table.Column(type: "character varying(500)", maxLength: 500, nullable: true), + Name = table.Column( + type: "character varying(100)", + maxLength: 100, + nullable: false + ), + DownloadUrl = table.Column( + type: "character varying(500)", + maxLength: 500, + nullable: false + ), + OperatingSystem = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: false + ), + Description = table.Column( + type: "character varying(300)", + maxLength: 300, + nullable: true + ), + IconUrl = table.Column( + type: "character varying(500)", + maxLength: 500, + nullable: true + ), SortOrder = table.Column(type: "integer", nullable: false), - IsEnabled = table.Column(type: "boolean", nullable: false) + IsEnabled = table.Column(type: "boolean", nullable: false), }, constraints: table => { table.PrimaryKey("PK_ClientApps", x => x.Id); - }); + } + ); migrationBuilder.CreateTable( name: "VpnConfigs", @@ -43,53 +65,86 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations Id = table.Column(type: "uuid", nullable: false), UserId = table.Column(type: "uuid", nullable: false), InboundId = table.Column(type: "uuid", nullable: false), - Label = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), - ClientEmail = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - ClientExternalId = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Protocol = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Label = table.Column( + type: "character varying(100)", + maxLength: 100, + nullable: true + ), + ClientEmail = table.Column( + type: "character varying(200)", + maxLength: 200, + nullable: false + ), + ClientExternalId = table.Column( + type: "character varying(200)", + maxLength: 200, + nullable: false + ), + Protocol = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: false + ), DeviceLimit = table.Column(type: "integer", nullable: false), UsedUpBytes = table.Column(type: "bigint", nullable: false), UsedDownBytes = table.Column(type: "bigint", nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: true), - Status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - SubscriptionToken = table.Column(type: "character varying(128)", maxLength: 128, nullable: false), - LastSyncAt = table.Column(type: "timestamp with time zone", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + ExpiresAt = table.Column( + type: "timestamp with time zone", + nullable: true + ), + Status = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: false + ), + SubscriptionToken = table.Column( + type: "character varying(128)", + maxLength: 128, + nullable: false + ), + LastSyncAt = table.Column( + type: "timestamp with time zone", + nullable: true + ), + CreatedAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), }, constraints: table => { table.PrimaryKey("PK_VpnConfigs", x => x.Id); - }); + } + ); migrationBuilder.CreateIndex( name: "IX_VpnConfigs_InboundId", table: "VpnConfigs", - column: "InboundId"); + column: "InboundId" + ); migrationBuilder.CreateIndex( name: "IX_VpnConfigs_SubscriptionToken", table: "VpnConfigs", column: "SubscriptionToken", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "IX_VpnConfigs_UserId_Status", table: "VpnConfigs", - columns: new[] { "UserId", "Status" }); + columns: new[] { "UserId", "Status" } + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "ClientApps"); + migrationBuilder.DropTable(name: "ClientApps"); - migrationBuilder.DropTable( - name: "VpnConfigs"); + migrationBuilder.DropTable(name: "VpnConfigs"); - migrationBuilder.DropColumn( - name: "SubscriptionToken", - table: "AspNetUsers"); + migrationBuilder.DropColumn(name: "SubscriptionToken", table: "AspNetUsers"); } } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701194500_AddTrafficSamples.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701194500_AddTrafficSamples.cs index f59b935..2d05f02 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701194500_AddTrafficSamples.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701194500_AddTrafficSamples.cs @@ -16,29 +16,37 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations name: "TrafficSamples", columns: table => new { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Id = table + .Column(type: "bigint", nullable: false) + .Annotation( + "Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.IdentityByDefaultColumn + ), ConfigId = table.Column(type: "uuid", nullable: false), - Timestamp = table.Column(type: "timestamp with time zone", nullable: false), + Timestamp = table.Column( + type: "timestamp with time zone", + nullable: false + ), UpBytes = table.Column(type: "bigint", nullable: false), - DownBytes = table.Column(type: "bigint", nullable: false) + DownBytes = table.Column(type: "bigint", nullable: false), }, constraints: table => { table.PrimaryKey("PK_TrafficSamples", x => x.Id); - }); + } + ); migrationBuilder.CreateIndex( name: "IX_TrafficSamples_ConfigId_Timestamp", table: "TrafficSamples", - columns: new[] { "ConfigId", "Timestamp" }); + columns: new[] { "ConfigId", "Timestamp" } + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "TrafficSamples"); + migrationBuilder.DropTable(name: "TrafficSamples"); } } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701195456_AddAuditAndBlocking.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701195456_AddAuditAndBlocking.cs index 915f6e8..fb0aee5 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701195456_AddAuditAndBlocking.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701195456_AddAuditAndBlocking.cs @@ -17,42 +17,65 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations table: "AspNetUsers", type: "boolean", nullable: false, - defaultValue: false); + defaultValue: false + ); migrationBuilder.CreateTable( name: "AuditLogs", columns: table => new { - Id = table.Column(type: "bigint", nullable: false) - .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + Id = table + .Column(type: "bigint", nullable: false) + .Annotation( + "Npgsql:ValueGenerationStrategy", + NpgsqlValueGenerationStrategy.IdentityByDefaultColumn + ), ActorId = table.Column(type: "uuid", nullable: true), - Action = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - TargetType = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - TargetId = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + Action = table.Column( + type: "character varying(100)", + maxLength: 100, + nullable: false + ), + TargetType = table.Column( + type: "character varying(50)", + maxLength: 50, + nullable: false + ), + TargetId = table.Column( + type: "character varying(100)", + maxLength: 100, + nullable: false + ), Metadata = table.Column(type: "jsonb", nullable: true), - Source = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + Source = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: false + ), + CreatedAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), }, constraints: table => { table.PrimaryKey("PK_AuditLogs", x => x.Id); - }); + } + ); migrationBuilder.CreateIndex( name: "IX_AuditLogs_CreatedAt", table: "AuditLogs", - column: "CreatedAt"); + column: "CreatedAt" + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "AuditLogs"); + migrationBuilder.DropTable(name: "AuditLogs"); - migrationBuilder.DropColumn( - name: "IsBlocked", - table: "AspNetUsers"); + migrationBuilder.DropColumn(name: "IsBlocked", table: "AspNetUsers"); } } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701205836_AddTelegram.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701205836_AddTelegram.cs index a6a8e14..c9ae09c 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701205836_AddTelegram.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260701205836_AddTelegram.cs @@ -15,98 +15,121 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations name: "TelegramLinkedAt", table: "AspNetUsers", type: "timestamp with time zone", - nullable: true); + nullable: true + ); migrationBuilder.AddColumn( name: "TelegramUserId", table: "AspNetUsers", type: "bigint", - nullable: true); + nullable: true + ); migrationBuilder.AddColumn( name: "TelegramUsername", table: "AspNetUsers", type: "text", - nullable: true); + nullable: true + ); migrationBuilder.CreateTable( name: "TelegramLinkTokens", columns: table => new { Id = table.Column(type: "uuid", nullable: false), - Token = table.Column(type: "character varying(64)", maxLength: 64, nullable: false), + Token = table.Column( + type: "character varying(64)", + maxLength: 64, + nullable: false + ), UserId = table.Column(type: "uuid", nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false), - ConsumedAt = table.Column(type: "timestamp with time zone", nullable: true) + ExpiresAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), + ConsumedAt = table.Column( + type: "timestamp with time zone", + nullable: true + ), }, constraints: table => { table.PrimaryKey("PK_TelegramLinkTokens", x => x.Id); - }); + } + ); migrationBuilder.CreateTable( name: "TelegramLoginRequests", columns: table => new { Id = table.Column(type: "uuid", nullable: false), - Status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Status = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: false + ), UserId = table.Column(type: "uuid", nullable: true), - Context = table.Column(type: "character varying(200)", maxLength: 200, nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - ExpiresAt = table.Column(type: "timestamp with time zone", nullable: false) + Context = table.Column( + type: "character varying(200)", + maxLength: 200, + nullable: true + ), + CreatedAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), + ExpiresAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), }, constraints: table => { table.PrimaryKey("PK_TelegramLoginRequests", x => x.Id); - }); + } + ); migrationBuilder.CreateIndex( name: "IX_AspNetUsers_SubscriptionToken", table: "AspNetUsers", column: "SubscriptionToken", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "IX_AspNetUsers_TelegramUserId", table: "AspNetUsers", column: "TelegramUserId", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "IX_TelegramLinkTokens_Token", table: "TelegramLinkTokens", column: "Token", - unique: true); + unique: true + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "TelegramLinkTokens"); + migrationBuilder.DropTable(name: "TelegramLinkTokens"); - migrationBuilder.DropTable( - name: "TelegramLoginRequests"); + migrationBuilder.DropTable(name: "TelegramLoginRequests"); migrationBuilder.DropIndex( name: "IX_AspNetUsers_SubscriptionToken", - table: "AspNetUsers"); + table: "AspNetUsers" + ); - migrationBuilder.DropIndex( - name: "IX_AspNetUsers_TelegramUserId", - table: "AspNetUsers"); + migrationBuilder.DropIndex(name: "IX_AspNetUsers_TelegramUserId", table: "AspNetUsers"); - migrationBuilder.DropColumn( - name: "TelegramLinkedAt", - table: "AspNetUsers"); + migrationBuilder.DropColumn(name: "TelegramLinkedAt", table: "AspNetUsers"); - migrationBuilder.DropColumn( - name: "TelegramUserId", - table: "AspNetUsers"); + migrationBuilder.DropColumn(name: "TelegramUserId", table: "AspNetUsers"); - migrationBuilder.DropColumn( - name: "TelegramUsername", - table: "AspNetUsers"); + migrationBuilder.DropColumn(name: "TelegramUsername", table: "AspNetUsers"); } } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260702201236_RemoveDeviceLimit.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260702201236_RemoveDeviceLimit.cs index 141bf55..459a6fe 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260702201236_RemoveDeviceLimit.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260702201236_RemoveDeviceLimit.cs @@ -10,9 +10,7 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations /// protected override void Up(MigrationBuilder migrationBuilder) { - migrationBuilder.DropColumn( - name: "DeviceLimit", - table: "VpnConfigs"); + migrationBuilder.DropColumn(name: "DeviceLimit", table: "VpnConfigs"); } /// @@ -23,7 +21,8 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations table: "VpnConfigs", type: "integer", nullable: false, - defaultValue: 0); + defaultValue: 0 + ); } } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260703114324_AddNewsPosts.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260703114324_AddNewsPosts.cs index c0339d9..f8967f7 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260703114324_AddNewsPosts.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260703114324_AddNewsPosts.cs @@ -16,27 +16,42 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations columns: table => new { Id = table.Column(type: "uuid", nullable: false), - Title = table.Column(type: "character varying(200)", maxLength: 200, nullable: false), - Body = table.Column(type: "character varying(20000)", maxLength: 20000, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), - UpdatedAt = table.Column(type: "timestamp with time zone", nullable: true) + Title = table.Column( + type: "character varying(200)", + maxLength: 200, + nullable: false + ), + Body = table.Column( + type: "character varying(20000)", + maxLength: 20000, + nullable: false + ), + CreatedAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), + UpdatedAt = table.Column( + type: "timestamp with time zone", + nullable: true + ), }, constraints: table => { table.PrimaryKey("PK_NewsPosts", x => x.Id); - }); + } + ); migrationBuilder.CreateIndex( name: "IX_NewsPosts_CreatedAt", table: "NewsPosts", - column: "CreatedAt"); + column: "CreatedAt" + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "NewsPosts"); + migrationBuilder.DropTable(name: "NewsPosts"); } } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260713035342_AddRoleMaxIpLimit.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260713035342_AddRoleMaxIpLimit.cs index 1138bd3..12afa37 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260713035342_AddRoleMaxIpLimit.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260713035342_AddRoleMaxIpLimit.cs @@ -18,17 +18,18 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations table: "AspNetRoles", type: "integer", nullable: false, - defaultValue: -1); + defaultValue: -1 + ); - migrationBuilder.Sql("UPDATE \"AspNetRoles\" SET \"MaxIpLimit\" = 2 WHERE \"Name\" = 'user';"); + migrationBuilder.Sql( + "UPDATE \"AspNetRoles\" SET \"MaxIpLimit\" = 2 WHERE \"Name\" = 'user';" + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropColumn( - name: "MaxIpLimit", - table: "AspNetRoles"); + migrationBuilder.DropColumn(name: "MaxIpLimit", table: "AspNetRoles"); } } } diff --git a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260714031703_AddSupportTickets.cs b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260714031703_AddSupportTickets.cs index 0f98ecf..02229fb 100644 --- a/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260714031703_AddSupportTickets.cs +++ b/backend/src/PnvPanel.Infrastructure/Persistence/Migrations/20260714031703_AddSupportTickets.cs @@ -17,18 +17,34 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations { Id = table.Column(type: "uuid", nullable: false), UserId = table.Column(type: "uuid", nullable: false), - Type = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), - Status = table.Column(type: "character varying(32)", maxLength: 32, nullable: false), + Type = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: false + ), + Status = table.Column( + type: "character varying(32)", + maxLength: 32, + nullable: false + ), RequestedRoleId = table.Column(type: "uuid", nullable: true), - ProposedRoleName = table.Column(type: "character varying(100)", maxLength: 100, nullable: true), + ProposedRoleName = table.Column( + type: "character varying(100)", + maxLength: 100, + nullable: true + ), ProposedMaxConfigs = table.Column(type: "integer", nullable: true), ProposedMaxIpLimit = table.Column(type: "integer", nullable: true), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + CreatedAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), }, constraints: table => { table.PrimaryKey("PK_SupportTickets", x => x.Id); - }); + } + ); migrationBuilder.CreateTable( name: "TicketAttachments", @@ -36,16 +52,32 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations { Id = table.Column(type: "uuid", nullable: false), CommentId = table.Column(type: "uuid", nullable: false), - FileName = table.Column(type: "character varying(255)", maxLength: 255, nullable: false), - StoredFileName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), - ContentType = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + FileName = table.Column( + type: "character varying(255)", + maxLength: 255, + nullable: false + ), + StoredFileName = table.Column( + type: "character varying(100)", + maxLength: 100, + nullable: false + ), + ContentType = table.Column( + type: "character varying(100)", + maxLength: 100, + nullable: false + ), SizeBytes = table.Column(type: "bigint", nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + CreatedAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), }, constraints: table => { table.PrimaryKey("PK_TicketAttachments", x => x.Id); - }); + } + ); migrationBuilder.CreateTable( name: "TicketComments", @@ -54,52 +86,62 @@ namespace PnvPanel.Infrastructure.Persistence.Migrations Id = table.Column(type: "uuid", nullable: false), TicketId = table.Column(type: "uuid", nullable: false), AuthorId = table.Column(type: "uuid", nullable: false), - Body = table.Column(type: "character varying(4000)", maxLength: 4000, nullable: false), - CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + Body = table.Column( + type: "character varying(4000)", + maxLength: 4000, + nullable: false + ), + CreatedAt = table.Column( + type: "timestamp with time zone", + nullable: false + ), }, constraints: table => { table.PrimaryKey("PK_TicketComments", x => x.Id); - }); + } + ); migrationBuilder.CreateIndex( name: "IX_SupportTickets_Type_Status", table: "SupportTickets", - columns: new[] { "Type", "Status" }); + columns: new[] { "Type", "Status" } + ); migrationBuilder.CreateIndex( name: "IX_SupportTickets_UserId_Status", table: "SupportTickets", - columns: new[] { "UserId", "Status" }); + columns: new[] { "UserId", "Status" } + ); migrationBuilder.CreateIndex( name: "IX_TicketAttachments_CommentId", table: "TicketAttachments", - column: "CommentId"); + column: "CommentId" + ); migrationBuilder.CreateIndex( name: "IX_TicketAttachments_StoredFileName", table: "TicketAttachments", column: "StoredFileName", - unique: true); + unique: true + ); migrationBuilder.CreateIndex( name: "IX_TicketComments_TicketId_CreatedAt", table: "TicketComments", - columns: new[] { "TicketId", "CreatedAt" }); + columns: new[] { "TicketId", "CreatedAt" } + ); } /// protected override void Down(MigrationBuilder migrationBuilder) { - migrationBuilder.DropTable( - name: "SupportTickets"); + migrationBuilder.DropTable(name: "SupportTickets"); - migrationBuilder.DropTable( - name: "TicketAttachments"); + migrationBuilder.DropTable(name: "TicketAttachments"); - migrationBuilder.DropTable( - name: "TicketComments"); + migrationBuilder.DropTable(name: "TicketComments"); } } } diff --git a/backend/src/PnvPanel.Infrastructure/PnvPanel.Infrastructure.csproj b/backend/src/PnvPanel.Infrastructure/PnvPanel.Infrastructure.csproj index 03c5adb..160c068 100644 --- a/backend/src/PnvPanel.Infrastructure/PnvPanel.Infrastructure.csproj +++ b/backend/src/PnvPanel.Infrastructure/PnvPanel.Infrastructure.csproj @@ -1,5 +1,4 @@  - @@ -22,5 +21,4 @@ enable enable - diff --git a/backend/src/PnvPanel.Infrastructure/Xui/XuiPanelGateway.cs b/backend/src/PnvPanel.Infrastructure/Xui/XuiPanelGateway.cs index cba834d..575b573 100644 --- a/backend/src/PnvPanel.Infrastructure/Xui/XuiPanelGateway.cs +++ b/backend/src/PnvPanel.Infrastructure/Xui/XuiPanelGateway.cs @@ -21,8 +21,8 @@ internal sealed class XuiPanelGateway( IXuiHttpClientFactory httpClientFactory, IXuiConnectionStringBuilderResolver connectionStringResolver, ISecretProtector secretProtector, - ILoggerFactory loggerFactory) - : IXuiPanelGateway, IDisposable + ILoggerFactory loggerFactory +) : IXuiPanelGateway, IDisposable { private readonly ConcurrentDictionary> _clients = new(); @@ -30,7 +30,12 @@ internal sealed class XuiPanelGateway( { return XuiBaseUrlValidator.IsAllowed(baseAddress.ToString(), out var reason) ? Result.Success() - : Result.Failure(Error.Validation("Nodes.BaseAddressNotAllowed", reason ?? "Адрес панели не разрешён.")); + : Result.Failure( + Error.Validation( + "Nodes.BaseAddressNotAllowed", + reason ?? "Адрес панели не разрешён." + ) + ); } public async Task ProbeAsync(Node node, CancellationToken cancellationToken) @@ -47,7 +52,10 @@ internal sealed class XuiPanelGateway( } } - public async Task>> ListInboundsAsync(Node node, CancellationToken cancellationToken) + public async Task>> ListInboundsAsync( + Node node, + CancellationToken cancellationToken + ) { try { @@ -57,7 +65,12 @@ internal sealed class XuiPanelGateway( var mapped = remoteInbounds .Select(i => (Summary: i, Protocol: TryParseProtocol(i.Protocol))) .Where(x => x.Protocol is not null) - .Select(x => new RemoteInboundInfo(x.Summary.ExternalId, x.Protocol!.Value, x.Summary.Remark, x.Summary.Port)) + .Select(x => new RemoteInboundInfo( + x.Summary.ExternalId, + x.Protocol!.Value, + x.Summary.Remark, + x.Summary.Port + )) .ToList(); return Result.Success>(mapped); @@ -65,13 +78,20 @@ internal sealed class XuiPanelGateway( catch (Exception ex) { return Result.Failure>( - Error.Failure("Xui.Unreachable", $"Нода недоступна: {ex.Message}")); + Error.Failure("Xui.Unreachable", $"Нода недоступна: {ex.Message}") + ); } } public async Task> 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 + ) { try { @@ -79,83 +99,146 @@ internal sealed class XuiPanelGateway( // Наш domain-sentinel RoleQuota.Unlimited (-1) переводим в нативное "без лимита" 3x-ui (0) — // ThreeXui.Net.AddClientRequest.LimitIp обязателен (не nullable), 0 в самом 3x-ui означает unlimited. var request = new AddClientRequest( - clientName, clientEmail, ToRemoteProtocol(protocol), limitIp == RoleQuota.Unlimited ? 0 : limitIp, null); + clientName, + clientEmail, + ToRemoteProtocol(protocol), + limitIp == RoleQuota.Unlimited ? 0 : limitIp, + null + ); var result = await client.AddClientAsync(inboundRemoteId, request, cancellationToken); return Result.Success(result.ExternalClientId); } catch (Exception ex) { - return Result.Failure(Error.Failure("Xui.AddClientFailed", $"Не удалось создать клиента: {ex.Message}")); + return Result.Failure( + Error.Failure("Xui.AddClientFailed", $"Не удалось создать клиента: {ex.Message}") + ); } } public async Task RemoveClientAsync( - Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol, CancellationToken cancellationToken) + Node node, + string inboundRemoteId, + string clientExternalId, + VpnProtocol protocol, + CancellationToken cancellationToken + ) { try { var client = GetClient(node); - await client.RemoveClientAsync(inboundRemoteId, clientExternalId, ToRemoteProtocol(protocol), cancellationToken); + await client.RemoveClientAsync( + inboundRemoteId, + clientExternalId, + ToRemoteProtocol(protocol), + cancellationToken + ); return Result.Success(); } catch (Exception ex) { - return Result.Failure(Error.Failure("Xui.RemoveClientFailed", $"Не удалось удалить клиента: {ex.Message}")); + return Result.Failure( + Error.Failure("Xui.RemoveClientFailed", $"Не удалось удалить клиента: {ex.Message}") + ); } } public async Task 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 + ) { try { var client = GetClient(node); // deviceLimit: null — не трогаем то, что уже стоит на клиенте в панели (см. AddClientAsync). var request = new UpdateClientRequest(null, null, enable, name); - await client.UpdateClientAsync(inboundRemoteId, clientExternalId, ToRemoteProtocol(protocol), request, cancellationToken); + await client.UpdateClientAsync( + inboundRemoteId, + clientExternalId, + ToRemoteProtocol(protocol), + request, + cancellationToken + ); return Result.Success(); } catch (Exception ex) { - return Result.Failure(Error.Failure("Xui.UpdateClientFailed", $"Не удалось изменить клиента: {ex.Message}")); + return Result.Failure( + Error.Failure( + "Xui.UpdateClientFailed", + $"Не удалось изменить клиента: {ex.Message}" + ) + ); } } public async Task> 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 + ) { try { var client = GetClient(node); - var remoteInbound = await client.GetInboundAsync(inbound.RemoteInboundId, cancellationToken); + var remoteInbound = await client.GetInboundAsync( + inbound.RemoteInboundId, + cancellationToken + ); if (remoteInbound is null) { return Result.Failure( - Error.Failure("Xui.ConnectionStringFailed", "Inbound не найден на панели.")); + Error.Failure("Xui.ConnectionStringFailed", "Inbound не найден на панели.") + ); } var builder = connectionStringResolver.Resolve(ToRemoteProtocol(inbound.Protocol)); if (builder is null) { return Result.Failure( - Error.Failure("Xui.ConnectionStringFailed", $"Протокол {inbound.Protocol} не поддерживается.")); + Error.Failure( + "Xui.ConnectionStringFailed", + $"Протокол {inbound.Protocol} не поддерживается." + ) + ); } var request = new XuiConnectionStringRequest( - clientExternalId, clientName, inbound.Port, publicHost, node.BaseAddress.ToString(), remoteInbound); + clientExternalId, + clientName, + inbound.Port, + publicHost, + node.BaseAddress.ToString(), + remoteInbound + ); return Result.Success(builder.Build(request)); } catch (Exception ex) { - return Result.Failure(Error.Failure("Xui.ConnectionStringFailed", $"Не удалось построить ссылку: {ex.Message}")); + return Result.Failure( + Error.Failure( + "Xui.ConnectionStringFailed", + $"Не удалось построить ссылку: {ex.Message}" + ) + ); } } public async Task>> GetClientTrafficAsync( - Node node, string inboundRemoteId, CancellationToken cancellationToken) + Node node, + string inboundRemoteId, + CancellationToken cancellationToken + ) { try { @@ -164,7 +247,8 @@ internal sealed class XuiPanelGateway( if (remoteInbound is null) { return Result.Failure>( - Error.Failure("Xui.InboundNotFound", "Inbound не найден на панели.")); + Error.Failure("Xui.InboundNotFound", "Inbound не найден на панели.") + ); } return Result.Success(ParseClientStats(remoteInbound.RawInboundJson)); @@ -172,7 +256,8 @@ internal sealed class XuiPanelGateway( catch (Exception ex) { return Result.Failure>( - Error.Failure("Xui.TrafficFetchFailed", $"Не удалось получить трафик: {ex.Message}")); + Error.Failure("Xui.TrafficFetchFailed", $"Не удалось получить трафик: {ex.Message}") + ); } } @@ -181,7 +266,9 @@ internal sealed class XuiPanelGateway( /// стандартное поле 3x-ui API "clientStats": [{ "email": "...", "up": N, "down": N }, ...]. /// Формат форка может отличаться — при ошибке парсинга просто возвращаем пусто, не валим синхронизацию. /// - private static IReadOnlyDictionary ParseClientStats(string? rawInboundJson) + private static IReadOnlyDictionary ParseClientStats( + string? rawInboundJson + ) { var result = new Dictionary(); if (string.IsNullOrWhiteSpace(rawInboundJson)) @@ -190,12 +277,18 @@ internal sealed class XuiPanelGateway( try { using var doc = JsonDocument.Parse(rawInboundJson); - if (!doc.RootElement.TryGetProperty("clientStats", out var clientStats) || clientStats.ValueKind != JsonValueKind.Array) + if ( + !doc.RootElement.TryGetProperty("clientStats", out var clientStats) + || clientStats.ValueKind != JsonValueKind.Array + ) return result; foreach (var stat in clientStats.EnumerateArray()) { - if (!stat.TryGetProperty("email", out var emailProp) || emailProp.ValueKind != JsonValueKind.String) + if ( + !stat.TryGetProperty("email", out var emailProp) + || emailProp.ValueKind != JsonValueKind.String + ) continue; var email = emailProp.GetString(); @@ -221,34 +314,40 @@ internal sealed class XuiPanelGateway( (lazy.Value as IDisposable)?.Dispose(); } - private IXuiClient GetClient(Node node) - => _clients.GetOrAdd(node.Id, _ => new Lazy(() => CreateClient(node))).Value; + private IXuiClient GetClient(Node node) => + _clients.GetOrAdd(node.Id, _ => new Lazy(() => CreateClient(node))).Value; private IXuiClient CreateClient(Node node) { - var httpClient = httpClientFactory.Create(node.BaseAddress, allowInsecureTls: false, timeout: TimeSpan.FromSeconds(15)); + var httpClient = httpClientFactory.Create( + node.BaseAddress, + allowInsecureTls: false, + timeout: TimeSpan.FromSeconds(15) + ); var password = secretProtector.Unprotect(node.Credentials.ProtectedPassword); var logger = loggerFactory.CreateLogger(); return new XuiClient(httpClient, node.Credentials.Username, password, logger); } - private static VpnProtocol? TryParseProtocol(string raw) => raw.ToLowerInvariant() switch - { - "vless" => VpnProtocol.Vless, - "vmess" => VpnProtocol.Vmess, - "trojan" => VpnProtocol.Trojan, - "shadowsocks" => VpnProtocol.Shadowsocks, - _ => null, - }; + private static VpnProtocol? TryParseProtocol(string raw) => + raw.ToLowerInvariant() switch + { + "vless" => VpnProtocol.Vless, + "vmess" => VpnProtocol.Vmess, + "trojan" => VpnProtocol.Trojan, + "shadowsocks" => VpnProtocol.Shadowsocks, + _ => null, + }; - private static string ToRemoteProtocol(VpnProtocol protocol) => protocol switch - { - VpnProtocol.Vless => "vless", - VpnProtocol.Vmess => "vmess", - VpnProtocol.Trojan => "trojan", - VpnProtocol.Shadowsocks => "shadowsocks", - _ => throw new ArgumentOutOfRangeException(nameof(protocol)), - }; + private static string ToRemoteProtocol(VpnProtocol protocol) => + protocol switch + { + VpnProtocol.Vless => "vless", + VpnProtocol.Vmess => "vmess", + VpnProtocol.Trojan => "trojan", + VpnProtocol.Shadowsocks => "shadowsocks", + _ => throw new ArgumentOutOfRangeException(nameof(protocol)), + }; public void Dispose() { diff --git a/backend/tests/PnvPanel.Application.Tests/Activation/ApproveActivationCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Activation/ApproveActivationCommandHandlerTests.cs index 1f625e0..12cdcc4 100644 --- a/backend/tests/PnvPanel.Application.Tests/Activation/ApproveActivationCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Activation/ApproveActivationCommandHandlerTests.cs @@ -27,15 +27,28 @@ public class ApproveActivationCommandHandlerTests await dbContext.SaveChangesAsync(CancellationToken.None); _currentUser.UserId.Returns(adminId); - _identityService.ActivateUserAsync(request.UserId, adminId, Arg.Any()).Returns(Result.Success()); + _identityService + .ActivateUserAsync(request.UserId, adminId, Arg.Any()) + .Returns(Result.Success()); - var handler = new ApproveActivationCommandHandler(dbContext, _identityService, _notifier, _telegramNotifier, _currentUser); + var handler = new ApproveActivationCommandHandler( + dbContext, + _identityService, + _notifier, + _telegramNotifier, + _currentUser + ); - var result = await handler.Handle(new ApproveActivationCommand(request.Id), CancellationToken.None); + var result = await handler.Handle( + new ApproveActivationCommand(request.Id), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal(ActivationStatus.Approved, request.Status); - await _notifier.Received(1).NotifyUserActivatedAsync(request.UserId, Arg.Any()); + await _notifier + .Received(1) + .NotifyUserActivatedAsync(request.UserId, Arg.Any()); } [Fact] @@ -44,9 +57,18 @@ public class ApproveActivationCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); _currentUser.UserId.Returns(Guid.NewGuid()); - var handler = new ApproveActivationCommandHandler(dbContext, _identityService, _notifier, _telegramNotifier, _currentUser); + var handler = new ApproveActivationCommandHandler( + dbContext, + _identityService, + _notifier, + _telegramNotifier, + _currentUser + ); - var result = await handler.Handle(new ApproveActivationCommand(Guid.NewGuid()), CancellationToken.None); + var result = await handler.Handle( + new ApproveActivationCommand(Guid.NewGuid()), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(ActivationErrors.NotFound, result.Error); @@ -63,9 +85,18 @@ public class ApproveActivationCommandHandlerTests _currentUser.UserId.Returns(Guid.NewGuid()); - var handler = new ApproveActivationCommandHandler(dbContext, _identityService, _notifier, _telegramNotifier, _currentUser); + var handler = new ApproveActivationCommandHandler( + dbContext, + _identityService, + _notifier, + _telegramNotifier, + _currentUser + ); - var result = await handler.Handle(new ApproveActivationCommand(request.Id), CancellationToken.None); + var result = await handler.Handle( + new ApproveActivationCommand(request.Id), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(ActivationErrors.AlreadyDecided, result.Error); @@ -82,14 +113,27 @@ public class ApproveActivationCommandHandlerTests _currentUser.UserId.Returns(adminId); var failure = Error.NotFound("User.NotFound", "Пользователь не найден."); - _identityService.ActivateUserAsync(request.UserId, adminId, Arg.Any()).Returns(Result.Failure(failure)); + _identityService + .ActivateUserAsync(request.UserId, adminId, Arg.Any()) + .Returns(Result.Failure(failure)); - var handler = new ApproveActivationCommandHandler(dbContext, _identityService, _notifier, _telegramNotifier, _currentUser); + var handler = new ApproveActivationCommandHandler( + dbContext, + _identityService, + _notifier, + _telegramNotifier, + _currentUser + ); - var result = await handler.Handle(new ApproveActivationCommand(request.Id), CancellationToken.None); + var result = await handler.Handle( + new ApproveActivationCommand(request.Id), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(failure, result.Error); - await _notifier.DidNotReceive().NotifyUserActivatedAsync(Arg.Any(), Arg.Any()); + await _notifier + .DidNotReceive() + .NotifyUserActivatedAsync(Arg.Any(), Arg.Any()); } } diff --git a/backend/tests/PnvPanel.Application.Tests/Activation/RejectActivationCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Activation/RejectActivationCommandHandlerTests.cs index c5c5d81..c320aca 100644 --- a/backend/tests/PnvPanel.Application.Tests/Activation/RejectActivationCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Activation/RejectActivationCommandHandlerTests.cs @@ -25,7 +25,10 @@ public class RejectActivationCommandHandlerTests var handler = new RejectActivationCommandHandler(dbContext, _currentUser); - var result = await handler.Handle(new RejectActivationCommand(request.Id, "недостаточно информации"), CancellationToken.None); + var result = await handler.Handle( + new RejectActivationCommand(request.Id, "недостаточно информации"), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal(ActivationStatus.Rejected, request.Status); @@ -40,7 +43,10 @@ public class RejectActivationCommandHandlerTests var handler = new RejectActivationCommandHandler(dbContext, _currentUser); - var result = await handler.Handle(new RejectActivationCommand(Guid.NewGuid(), null), CancellationToken.None); + var result = await handler.Handle( + new RejectActivationCommand(Guid.NewGuid(), null), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(ActivationErrors.NotFound, result.Error); @@ -59,7 +65,10 @@ public class RejectActivationCommandHandlerTests var handler = new RejectActivationCommandHandler(dbContext, _currentUser); - var result = await handler.Handle(new RejectActivationCommand(request.Id, null), CancellationToken.None); + var result = await handler.Handle( + new RejectActivationCommand(request.Id, null), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(ActivationErrors.AlreadyDecided, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Activation/RequestActivationCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Activation/RequestActivationCommandHandlerTests.cs index e8d8680..d7362e2 100644 --- a/backend/tests/PnvPanel.Application.Tests/Activation/RequestActivationCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Activation/RequestActivationCommandHandlerTests.cs @@ -22,18 +22,40 @@ public class RequestActivationCommandHandlerTests _currentUser.UserId.Returns(userId); _currentUser.UserName.Returns("alice"); - var handler = new RequestActivationCommandHandler(dbContext, _notifier, _telegramNotifier, _currentUser); + var handler = new RequestActivationCommandHandler( + dbContext, + _notifier, + _telegramNotifier, + _currentUser + ); - var result = await handler.Handle(new RequestActivationCommand("Please activate"), CancellationToken.None); + var result = await handler.Handle( + new RequestActivationCommand("Please activate"), + CancellationToken.None + ); await dbContext.SaveChangesAsync(CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal("Please activate", result.Value.Comment); Assert.Single(dbContext.ActivationRequests); - await _notifier.Received(1).NotifyActivationRequestedAsync( - Arg.Any(), userId, "alice", "Please activate", Arg.Any(), Arg.Any()); - await _telegramNotifier.Received(1).NotifyAdminsActivationRequestedAsync( - Arg.Any(), "alice", "Please activate", Arg.Any()); + await _notifier + .Received(1) + .NotifyActivationRequestedAsync( + Arg.Any(), + userId, + "alice", + "Please activate", + Arg.Any(), + Arg.Any() + ); + await _telegramNotifier + .Received(1) + .NotifyAdminsActivationRequestedAsync( + Arg.Any(), + "alice", + "Please activate", + Arg.Any() + ); } [Fact] @@ -47,14 +69,30 @@ public class RequestActivationCommandHandlerTests _currentUser.UserId.Returns(userId); _currentUser.UserName.Returns("alice"); - var handler = new RequestActivationCommandHandler(dbContext, _notifier, _telegramNotifier, _currentUser); + var handler = new RequestActivationCommandHandler( + dbContext, + _notifier, + _telegramNotifier, + _currentUser + ); - var result = await handler.Handle(new RequestActivationCommand(null), CancellationToken.None); + var result = await handler.Handle( + new RequestActivationCommand(null), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(ActivationErrors.AlreadyPending, result.Error); - await _notifier.DidNotReceive().NotifyActivationRequestedAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _notifier + .DidNotReceive() + .NotifyActivationRequestedAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } [Fact] @@ -63,9 +101,17 @@ public class RequestActivationCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); _currentUser.UserId.Returns((Guid?)null); - var handler = new RequestActivationCommandHandler(dbContext, _notifier, _telegramNotifier, _currentUser); + var handler = new RequestActivationCommandHandler( + dbContext, + _notifier, + _telegramNotifier, + _currentUser + ); - var result = await handler.Handle(new RequestActivationCommand(null), CancellationToken.None); + var result = await handler.Handle( + new RequestActivationCommand(null), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(AuthErrors.Unauthorized, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Admin/Configs/ListAllConfigsQueryHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Admin/Configs/ListAllConfigsQueryHandlerTests.cs index 2030103..d9c071e 100644 --- a/backend/tests/PnvPanel.Application.Tests/Admin/Configs/ListAllConfigsQueryHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Admin/Configs/ListAllConfigsQueryHandlerTests.cs @@ -19,7 +19,12 @@ public class ListAllConfigsQueryHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); - var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); + 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); inbound.Publish("Germany", [], null); var config = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "my-config"); @@ -29,12 +34,16 @@ public class ListAllConfigsQueryHandlerTests dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.GetUserNamesAsync(Arg.Any>(), Arg.Any()) + _identityService + .GetUserNamesAsync(Arg.Any>(), Arg.Any()) .Returns(new Dictionary { [userId] = "alice" }); var handler = new ListAllConfigsQueryHandler(dbContext, _identityService); - var result = await handler.Handle(new ListAllConfigsQuery(1, 20, Search: null, Status: null), CancellationToken.None); + var result = await handler.Handle( + new ListAllConfigsQuery(1, 20, Search: null, Status: null), + CancellationToken.None + ); Assert.True(result.IsSuccess); var item = Assert.Single(result.Value.Items); @@ -52,7 +61,12 @@ public class ListAllConfigsQueryHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); - var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); + 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 matching = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "phone-config"); var other = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "laptop-config"); @@ -62,12 +76,16 @@ public class ListAllConfigsQueryHandlerTests dbContext.VpnConfigs.AddRange(matching, other); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.GetUserNamesAsync(Arg.Any>(), Arg.Any()) + _identityService + .GetUserNamesAsync(Arg.Any>(), Arg.Any()) .Returns(new Dictionary { [userId] = "alice" }); var handler = new ListAllConfigsQueryHandler(dbContext, _identityService); - var result = await handler.Handle(new ListAllConfigsQuery(1, 20, Search: "phone", Status: null), CancellationToken.None); + var result = await handler.Handle( + new ListAllConfigsQuery(1, 20, Search: "phone", Status: null), + CancellationToken.None + ); Assert.True(result.IsSuccess); var item = Assert.Single(result.Value.Items); @@ -80,7 +98,12 @@ public class ListAllConfigsQueryHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); - var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); + 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 active = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "active-config"); var revoked = VpnConfig.Create(userId, inbound.Id, VpnProtocol.Vless, "revoked-config"); @@ -91,12 +114,16 @@ public class ListAllConfigsQueryHandlerTests dbContext.VpnConfigs.AddRange(active, revoked); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.GetUserNamesAsync(Arg.Any>(), Arg.Any()) + _identityService + .GetUserNamesAsync(Arg.Any>(), Arg.Any()) .Returns(new Dictionary { [userId] = "alice" }); var handler = new ListAllConfigsQueryHandler(dbContext, _identityService); - var result = await handler.Handle(new ListAllConfigsQuery(1, 20, Search: null, Status: ConfigStatus.Revoked), CancellationToken.None); + var result = await handler.Handle( + new ListAllConfigsQuery(1, 20, Search: null, Status: ConfigStatus.Revoked), + CancellationToken.None + ); Assert.True(result.IsSuccess); var item = Assert.Single(result.Value.Items); diff --git a/backend/tests/PnvPanel.Application.Tests/Admin/Nodes/RegisterNodeCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Admin/Nodes/RegisterNodeCommandHandlerTests.cs index 2147eff..3b540c6 100644 --- a/backend/tests/PnvPanel.Application.Tests/Admin/Nodes/RegisterNodeCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Admin/Nodes/RegisterNodeCommandHandlerTests.cs @@ -21,9 +21,20 @@ public class RegisterNodeCommandHandlerTests _gateway.ValidateBaseAddress(Arg.Any()).Returns(Result.Success()); _secretProtector.Protect("secret-password").Returns("protected-secret-password"); - var handler = new RegisterNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser); + var handler = new RegisterNodeCommandHandler( + dbContext, + _gateway, + _secretProtector, + _currentUser + ); - var command = new RegisterNodeCommand("node-1", "https://node1.example.com", "admin", "secret-password", "eu-west"); + var command = new RegisterNodeCommand( + "node-1", + "https://node1.example.com", + "admin", + "secret-password", + "eu-west" + ); var result = await handler.Handle(command, CancellationToken.None); @@ -31,7 +42,10 @@ public class RegisterNodeCommandHandlerTests Assert.Equal("node-1", result.Value.Name); Assert.Equal("admin", result.Value.Username); Assert.Single(dbContext.Nodes.Local); - Assert.Equal("protected-secret-password", dbContext.Nodes.Local.Single().Credentials.ProtectedPassword); + Assert.Equal( + "protected-secret-password", + dbContext.Nodes.Local.Single().Credentials.ProtectedPassword + ); } [Fact] @@ -39,9 +53,20 @@ public class RegisterNodeCommandHandlerTests { using var dbContext = InMemoryDbContextFactory.Create(); - var handler = new RegisterNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser); + var handler = new RegisterNodeCommandHandler( + dbContext, + _gateway, + _secretProtector, + _currentUser + ); - var command = new RegisterNodeCommand("node-1", "not-a-uri", "admin", "secret-password", null); + var command = new RegisterNodeCommand( + "node-1", + "not-a-uri", + "admin", + "secret-password", + null + ); var result = await handler.Handle(command, CancellationToken.None); @@ -59,9 +84,20 @@ public class RegisterNodeCommandHandlerTests var error = Error.Validation("Nodes.SchemeNotAllowed", "Разрешён только HTTPS."); _gateway.ValidateBaseAddress(Arg.Any()).Returns(Result.Failure(error)); - var handler = new RegisterNodeCommandHandler(dbContext, _gateway, _secretProtector, _currentUser); + var handler = new RegisterNodeCommandHandler( + dbContext, + _gateway, + _secretProtector, + _currentUser + ); - var command = new RegisterNodeCommand("node-1", "http://node1.example.com", "admin", "secret-password", null); + var command = new RegisterNodeCommand( + "node-1", + "http://node1.example.com", + "admin", + "secret-password", + null + ); var result = await handler.Handle(command, CancellationToken.None); diff --git a/backend/tests/PnvPanel.Application.Tests/Admin/Support/ApproveRoleRequestCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Admin/Support/ApproveRoleRequestCommandHandlerTests.cs index 5408aa3..2b3e47b 100644 --- a/backend/tests/PnvPanel.Application.Tests/Admin/Support/ApproveRoleRequestCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Admin/Support/ApproveRoleRequestCommandHandlerTests.cs @@ -25,20 +25,38 @@ public class ApproveRoleRequestCommandHandlerTests await dbContext.SaveChangesAsync(CancellationToken.None); var newRoleId = Guid.NewGuid(); - _roleService.CreateRoleAsync("premium", 10, 5, Arg.Any()) + _roleService + .CreateRoleAsync("premium", 10, 5, Arg.Any()) .Returns(Result.Success(new RoleDto(newRoleId, "premium", 10, 5, false))); - _roleService.ChangeUserRoleAsync(userId, newRoleId, Arg.Any()).Returns(Result.Success()); + _roleService + .ChangeUserRoleAsync(userId, newRoleId, Arg.Any()) + .Returns(Result.Success()); var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid(), "admin"); - var handler = new ApproveRoleRequestCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser); + var handler = new ApproveRoleRequestCommandHandler( + dbContext, + _roleService, + _notifier, + _telegramNotifier, + currentUser + ); - var result = await handler.Handle(new ApproveRoleRequestCommand(ticket.Id), CancellationToken.None); + var result = await handler.Handle( + new ApproveRoleRequestCommand(ticket.Id), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal(TicketStatus.Resolved, ticket.Status); - await _roleService.Received(1).CreateRoleAsync("premium", 10, 5, Arg.Any()); - await _roleService.Received(1).ChangeUserRoleAsync(userId, newRoleId, Arg.Any()); - await _telegramNotifier.Received(1).NotifyUserAsync(userId, Arg.Any(), Arg.Any()); + await _roleService + .Received(1) + .CreateRoleAsync("premium", 10, 5, Arg.Any()); + await _roleService + .Received(1) + .ChangeUserRoleAsync(userId, newRoleId, Arg.Any()); + await _telegramNotifier + .Received(1) + .NotifyUserAsync(userId, Arg.Any(), Arg.Any()); } [Fact] @@ -51,16 +69,33 @@ public class ApproveRoleRequestCommandHandlerTests dbContext.SupportTickets.Add(ticket); await dbContext.SaveChangesAsync(CancellationToken.None); - _roleService.ChangeUserRoleAsync(userId, roleId, Arg.Any()).Returns(Result.Success()); + _roleService + .ChangeUserRoleAsync(userId, roleId, Arg.Any()) + .Returns(Result.Success()); var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid()); - var handler = new ApproveRoleRequestCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser); + var handler = new ApproveRoleRequestCommandHandler( + dbContext, + _roleService, + _notifier, + _telegramNotifier, + currentUser + ); - var result = await handler.Handle(new ApproveRoleRequestCommand(ticket.Id), CancellationToken.None); + var result = await handler.Handle( + new ApproveRoleRequestCommand(ticket.Id), + CancellationToken.None + ); Assert.True(result.IsSuccess); - await _roleService.DidNotReceive().CreateRoleAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _roleService + .DidNotReceive() + .CreateRoleAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } [Fact] @@ -72,9 +107,18 @@ public class ApproveRoleRequestCommandHandlerTests await dbContext.SaveChangesAsync(CancellationToken.None); var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid()); - var handler = new ApproveRoleRequestCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser); + var handler = new ApproveRoleRequestCommandHandler( + dbContext, + _roleService, + _notifier, + _telegramNotifier, + currentUser + ); - var result = await handler.Handle(new ApproveRoleRequestCommand(ticket.Id), CancellationToken.None); + var result = await handler.Handle( + new ApproveRoleRequestCommand(ticket.Id), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(SupportErrors.NotRoleRequest, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Admin/Users/BlockUserCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Admin/Users/BlockUserCommandHandlerTests.cs index cc64ba5..0fd8b96 100644 --- a/backend/tests/PnvPanel.Application.Tests/Admin/Users/BlockUserCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Admin/Users/BlockUserCommandHandlerTests.cs @@ -18,7 +18,9 @@ public class BlockUserCommandHandlerTests private readonly IRealtimeNotifier _notifier = Substitute.For(); private readonly ITelegramNotifier _telegramNotifier = Substitute.For(); private readonly ICurrentUser _currentUser = Substitute.For(); - private readonly ILogger _logger = Substitute.For>(); + private readonly ILogger _logger = Substitute.For< + ILogger + >(); [Fact] public async Task Handle_IdentityServiceFails_ReturnsFailureWithoutTouchingConfigs() @@ -26,17 +28,35 @@ public class BlockUserCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); var failure = UserErrors.NotFound; - _identityService.BlockUserAsync(userId, Arg.Any()).Returns(Result.Failure(failure)); + _identityService + .BlockUserAsync(userId, Arg.Any()) + .Returns(Result.Failure(failure)); - var handler = new BlockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _telegramNotifier, _currentUser, _logger); + var handler = new BlockUserCommandHandler( + dbContext, + _identityService, + _gateway, + _notifier, + _telegramNotifier, + _currentUser, + _logger + ); var result = await handler.Handle(new BlockUserCommand(userId), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(failure, result.Error); - await _gateway.DidNotReceive().UpdateClientAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); + await _gateway + .DidNotReceive() + .UpdateClientAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } [Fact] @@ -46,7 +66,12 @@ public class BlockUserCommandHandlerTests var userId = Guid.NewGuid(); var adminId = Guid.NewGuid(); - var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); + 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"); @@ -55,24 +80,56 @@ public class BlockUserCommandHandlerTests dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.BlockUserAsync(userId, Arg.Any()).Returns(Result.Success()); + _identityService + .BlockUserAsync(userId, Arg.Any()) + .Returns(Result.Success()); _currentUser.UserId.Returns(adminId); - _gateway.UpdateClientAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()) + _gateway + .UpdateClientAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ) .Returns(Result.Success()); - var handler = new BlockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _telegramNotifier, _currentUser, _logger); + var handler = new BlockUserCommandHandler( + dbContext, + _identityService, + _gateway, + _notifier, + _telegramNotifier, + _currentUser, + _logger + ); var result = await handler.Handle(new BlockUserCommand(userId), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(ConfigStatus.Disabled, config.Status); - await _gateway.Received(1).UpdateClientAsync( - Arg.Is(n => n.Id == node.Id), inbound.RemoteInboundId, config.ClientExternalId, config.Protocol, - "my-config", enable: false, Arg.Any()); - await _notifier.Received(1).NotifyConfigStatusChangedAsync(userId, config.Id, ConfigStatus.Disabled, Arg.Any()); + await _gateway + .Received(1) + .UpdateClientAsync( + Arg.Is(n => n.Id == node.Id), + inbound.RemoteInboundId, + config.ClientExternalId, + config.Protocol, + "my-config", + enable: false, + Arg.Any() + ); + await _notifier + .Received(1) + .NotifyConfigStatusChangedAsync( + userId, + config.Id, + ConfigStatus.Disabled, + Arg.Any() + ); var audit = Assert.Single(dbContext.AuditLogs.Local); Assert.Equal("UserBlocked", audit.Action); @@ -85,17 +142,35 @@ public class BlockUserCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); - _identityService.BlockUserAsync(userId, Arg.Any()).Returns(Result.Success()); + _identityService + .BlockUserAsync(userId, Arg.Any()) + .Returns(Result.Success()); _currentUser.UserId.Returns(Guid.NewGuid()); - var handler = new BlockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _telegramNotifier, _currentUser, _logger); + var handler = new BlockUserCommandHandler( + dbContext, + _identityService, + _gateway, + _notifier, + _telegramNotifier, + _currentUser, + _logger + ); var result = await handler.Handle(new BlockUserCommand(userId), CancellationToken.None); Assert.True(result.IsSuccess); - await _gateway.DidNotReceive().UpdateClientAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()); + await _gateway + .DidNotReceive() + .UpdateClientAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } [Fact] @@ -105,7 +180,12 @@ public class BlockUserCommandHandlerTests var userId = Guid.NewGuid(); var adminId = Guid.NewGuid(); - var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); + 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"); @@ -114,20 +194,43 @@ public class BlockUserCommandHandlerTests dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.BlockUserAsync(userId, Arg.Any()).Returns(Result.Success()); + _identityService + .BlockUserAsync(userId, Arg.Any()) + .Returns(Result.Success()); _currentUser.UserId.Returns(adminId); - _gateway.UpdateClientAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()) + _gateway + .UpdateClientAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ) .Returns(Result.Failure(Error.Failure("Xui.UpdateClientFailed", "Нода недоступна."))); - var handler = new BlockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _telegramNotifier, _currentUser, _logger); + var handler = new BlockUserCommandHandler( + dbContext, + _identityService, + _gateway, + _notifier, + _telegramNotifier, + _currentUser, + _logger + ); var result = await handler.Handle(new BlockUserCommand(userId), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(ConfigStatus.Active, config.Status); - await _notifier.DidNotReceive().NotifyConfigStatusChangedAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _notifier + .DidNotReceive() + .NotifyConfigStatusChangedAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } } diff --git a/backend/tests/PnvPanel.Application.Tests/Admin/Users/ChangeUserRoleCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Admin/Users/ChangeUserRoleCommandHandlerTests.cs index 3cd03a6..237c3b1 100644 --- a/backend/tests/PnvPanel.Application.Tests/Admin/Users/ChangeUserRoleCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Admin/Users/ChangeUserRoleCommandHandlerTests.cs @@ -19,14 +19,21 @@ public class ChangeUserRoleCommandHandlerTests var userId = Guid.NewGuid(); var roleId = Guid.NewGuid(); - _roleService.ChangeUserRoleAsync(userId, roleId, Arg.Any()).Returns(Result.Success()); + _roleService + .ChangeUserRoleAsync(userId, roleId, Arg.Any()) + .Returns(Result.Success()); var handler = new ChangeUserRoleCommandHandler(_roleService, dbContext, _currentUser); - var result = await handler.Handle(new ChangeUserRoleCommand(userId, roleId), CancellationToken.None); + var result = await handler.Handle( + new ChangeUserRoleCommand(userId, roleId), + CancellationToken.None + ); Assert.True(result.IsSuccess); - await _roleService.Received(1).ChangeUserRoleAsync(userId, roleId, Arg.Any()); + await _roleService + .Received(1) + .ChangeUserRoleAsync(userId, roleId, Arg.Any()); Assert.Single(dbContext.AuditLogs.Local); } @@ -38,11 +45,16 @@ public class ChangeUserRoleCommandHandlerTests var userId = Guid.NewGuid(); var roleId = Guid.NewGuid(); var error = UserErrors.NotFound; - _roleService.ChangeUserRoleAsync(userId, roleId, Arg.Any()).Returns(Result.Failure(error)); + _roleService + .ChangeUserRoleAsync(userId, roleId, Arg.Any()) + .Returns(Result.Failure(error)); var handler = new ChangeUserRoleCommandHandler(_roleService, dbContext, _currentUser); - var result = await handler.Handle(new ChangeUserRoleCommand(userId, roleId), CancellationToken.None); + var result = await handler.Handle( + new ChangeUserRoleCommand(userId, roleId), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(error, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Admin/Users/DeleteUserCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Admin/Users/DeleteUserCommandHandlerTests.cs index d50b8c7..3386382 100644 --- a/backend/tests/PnvPanel.Application.Tests/Admin/Users/DeleteUserCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Admin/Users/DeleteUserCommandHandlerTests.cs @@ -24,13 +24,21 @@ public class DeleteUserCommandHandlerTests var adminId = Guid.NewGuid(); _currentUser.UserId.Returns(adminId); - var handler = new DeleteUserCommandHandler(dbContext, _identityService, _gateway, _telegramNotifier, _currentUser); + var handler = new DeleteUserCommandHandler( + dbContext, + _identityService, + _gateway, + _telegramNotifier, + _currentUser + ); var result = await handler.Handle(new DeleteUserCommand(adminId), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(UserErrors.CannotDeleteSelf, result.Error); - await _identityService.DidNotReceive().DeleteUserAsync(Arg.Any(), Arg.Any()); + await _identityService + .DidNotReceive() + .DeleteUserAsync(Arg.Any(), Arg.Any()); } [Fact] @@ -40,7 +48,12 @@ public class DeleteUserCommandHandlerTests var userId = Guid.NewGuid(); var adminId = Guid.NewGuid(); - var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); + 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"); config.AssignRemoteClient("external-id"); @@ -51,20 +64,44 @@ public class DeleteUserCommandHandlerTests await dbContext.SaveChangesAsync(CancellationToken.None); _currentUser.UserId.Returns(adminId); - _gateway.RemoveClientAsync(Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()) + _gateway + .RemoveClientAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ) + .Returns(Result.Success()); + _identityService + .DeleteUserAsync(userId, Arg.Any()) .Returns(Result.Success()); - _identityService.DeleteUserAsync(userId, Arg.Any()).Returns(Result.Success()); - var handler = new DeleteUserCommandHandler(dbContext, _identityService, _gateway, _telegramNotifier, _currentUser); + var handler = new DeleteUserCommandHandler( + dbContext, + _identityService, + _gateway, + _telegramNotifier, + _currentUser + ); var result = await handler.Handle(new DeleteUserCommand(userId), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(ConfigStatus.Revoked, config.Status); - await _gateway.Received(1).RemoveClientAsync( - Arg.Is(n => n.Id == node.Id), inbound.RemoteInboundId, "external-id", config.Protocol, Arg.Any()); - await _telegramNotifier.Received(1).NotifyUserAsync(userId, Arg.Any(), Arg.Any()); + await _gateway + .Received(1) + .RemoveClientAsync( + Arg.Is(n => n.Id == node.Id), + inbound.RemoteInboundId, + "external-id", + config.Protocol, + Arg.Any() + ); + await _telegramNotifier + .Received(1) + .NotifyUserAsync(userId, Arg.Any(), Arg.Any()); await _identityService.Received(1).DeleteUserAsync(userId, Arg.Any()); var audit = Assert.Single(dbContext.AuditLogs.Local); @@ -79,15 +116,30 @@ public class DeleteUserCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); _currentUser.UserId.Returns(Guid.NewGuid()); - _identityService.DeleteUserAsync(userId, Arg.Any()).Returns(Result.Success()); + _identityService + .DeleteUserAsync(userId, Arg.Any()) + .Returns(Result.Success()); - var handler = new DeleteUserCommandHandler(dbContext, _identityService, _gateway, _telegramNotifier, _currentUser); + var handler = new DeleteUserCommandHandler( + dbContext, + _identityService, + _gateway, + _telegramNotifier, + _currentUser + ); var result = await handler.Handle(new DeleteUserCommand(userId), CancellationToken.None); Assert.True(result.IsSuccess); - await _gateway.DidNotReceive().RemoveClientAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _gateway + .DidNotReceive() + .RemoveClientAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } [Fact] @@ -96,9 +148,17 @@ public class DeleteUserCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); _currentUser.UserId.Returns(Guid.NewGuid()); - _identityService.DeleteUserAsync(userId, Arg.Any()).Returns(Result.Failure(UserErrors.NotFound)); + _identityService + .DeleteUserAsync(userId, Arg.Any()) + .Returns(Result.Failure(UserErrors.NotFound)); - var handler = new DeleteUserCommandHandler(dbContext, _identityService, _gateway, _telegramNotifier, _currentUser); + var handler = new DeleteUserCommandHandler( + dbContext, + _identityService, + _gateway, + _telegramNotifier, + _currentUser + ); var result = await handler.Handle(new DeleteUserCommand(userId), CancellationToken.None); diff --git a/backend/tests/PnvPanel.Application.Tests/Admin/Users/UnblockUserCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Admin/Users/UnblockUserCommandHandlerTests.cs index 5a56b01..c9bb10c 100644 --- a/backend/tests/PnvPanel.Application.Tests/Admin/Users/UnblockUserCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Admin/Users/UnblockUserCommandHandlerTests.cs @@ -17,7 +17,9 @@ public class UnblockUserCommandHandlerTests private readonly IXuiPanelGateway _gateway = Substitute.For(); private readonly IRealtimeNotifier _notifier = Substitute.For(); private readonly ICurrentUser _currentUser = Substitute.For(); - private readonly ILogger _logger = Substitute.For>(); + private readonly ILogger _logger = Substitute.For< + ILogger + >(); [Fact] public async Task Handle_WhenUnblockSucceeds_ReEnablesDisabledConfigsAndWritesAudit() @@ -26,7 +28,12 @@ public class UnblockUserCommandHandlerTests var userId = Guid.NewGuid(); var adminId = Guid.NewGuid(); - var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); + 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"); config.Disable(); @@ -37,22 +44,53 @@ public class UnblockUserCommandHandlerTests await dbContext.SaveChangesAsync(CancellationToken.None); _currentUser.UserId.Returns(adminId); - _identityService.UnblockUserAsync(userId, Arg.Any()).Returns(Result.Success()); - _gateway.UpdateClientAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()) + _identityService + .UnblockUserAsync(userId, Arg.Any()) + .Returns(Result.Success()); + _gateway + .UpdateClientAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ) .Returns(Result.Success()); - var handler = new UnblockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _currentUser, _logger); + var handler = new UnblockUserCommandHandler( + dbContext, + _identityService, + _gateway, + _notifier, + _currentUser, + _logger + ); var result = await handler.Handle(new UnblockUserCommand(userId), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(ConfigStatus.Active, config.Status); - await _gateway.Received(1).UpdateClientAsync( - node, inbound.RemoteInboundId, config.ClientExternalId, config.Protocol, - Arg.Any(), true, Arg.Any()); - await _notifier.Received(1).NotifyConfigStatusChangedAsync(userId, config.Id, ConfigStatus.Active, Arg.Any()); + await _gateway + .Received(1) + .UpdateClientAsync( + node, + inbound.RemoteInboundId, + config.ClientExternalId, + config.Protocol, + Arg.Any(), + true, + Arg.Any() + ); + await _notifier + .Received(1) + .NotifyConfigStatusChangedAsync( + userId, + config.Id, + ConfigStatus.Active, + Arg.Any() + ); Assert.Single(dbContext.AuditLogs.Local); Assert.Equal("UserUnblocked", dbContext.AuditLogs.Local.Single().Action); } @@ -64,9 +102,18 @@ public class UnblockUserCommandHandlerTests var userId = Guid.NewGuid(); var error = UserErrors.NotFound; - _identityService.UnblockUserAsync(userId, Arg.Any()).Returns(Result.Failure(error)); + _identityService + .UnblockUserAsync(userId, Arg.Any()) + .Returns(Result.Failure(error)); - var handler = new UnblockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _currentUser, _logger); + var handler = new UnblockUserCommandHandler( + dbContext, + _identityService, + _gateway, + _notifier, + _currentUser, + _logger + ); var result = await handler.Handle(new UnblockUserCommand(userId), CancellationToken.None); @@ -82,7 +129,12 @@ public class UnblockUserCommandHandlerTests var userId = Guid.NewGuid(); var adminId = Guid.NewGuid(); - var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); + 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"); config.Disable(); @@ -93,19 +145,41 @@ public class UnblockUserCommandHandlerTests await dbContext.SaveChangesAsync(CancellationToken.None); _currentUser.UserId.Returns(adminId); - _identityService.UnblockUserAsync(userId, Arg.Any()).Returns(Result.Success()); - _gateway.UpdateClientAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()) + _identityService + .UnblockUserAsync(userId, Arg.Any()) + .Returns(Result.Success()); + _gateway + .UpdateClientAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ) .Returns(Result.Failure(Error.Failure("Xui.UpdateClientFailed", "Нода недоступна."))); - var handler = new UnblockUserCommandHandler(dbContext, _identityService, _gateway, _notifier, _currentUser, _logger); + var handler = new UnblockUserCommandHandler( + dbContext, + _identityService, + _gateway, + _notifier, + _currentUser, + _logger + ); var result = await handler.Handle(new UnblockUserCommand(userId), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(ConfigStatus.Disabled, config.Status); - await _notifier.DidNotReceive().NotifyConfigStatusChangedAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _notifier + .DidNotReceive() + .NotifyConfigStatusChangedAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } } diff --git a/backend/tests/PnvPanel.Application.Tests/Auth/ChangePasswordCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Auth/ChangePasswordCommandHandlerTests.cs index 0dfa32d..9d2e648 100644 --- a/backend/tests/PnvPanel.Application.Tests/Auth/ChangePasswordCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Auth/ChangePasswordCommandHandlerTests.cs @@ -15,29 +15,50 @@ public class ChangePasswordCommandHandlerTests [Fact] public async Task Handle_Unauthenticated_ReturnsUnauthorized() { - var handler = new ChangePasswordCommandHandler(_identityService, FakeCurrentUser.Anonymous()); + var handler = new ChangePasswordCommandHandler( + _identityService, + FakeCurrentUser.Anonymous() + ); - var result = await handler.Handle(new ChangePasswordCommand("old", "new"), CancellationToken.None); + var result = await handler.Handle( + new ChangePasswordCommand("old", "new"), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(AuthErrors.Unauthorized, result.Error); - await _identityService.DidNotReceive().ChangePasswordAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _identityService + .DidNotReceive() + .ChangePasswordAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } [Fact] public async Task Handle_Authenticated_DelegatesToIdentityService() { var userId = Guid.NewGuid(); - _identityService.ChangePasswordAsync(userId, "old-pass", "new-pass", Arg.Any()) + _identityService + .ChangePasswordAsync(userId, "old-pass", "new-pass", Arg.Any()) .Returns(Result.Success()); - var handler = new ChangePasswordCommandHandler(_identityService, FakeCurrentUser.Authenticated(userId)); + var handler = new ChangePasswordCommandHandler( + _identityService, + FakeCurrentUser.Authenticated(userId) + ); - var result = await handler.Handle(new ChangePasswordCommand("old-pass", "new-pass"), CancellationToken.None); + var result = await handler.Handle( + new ChangePasswordCommand("old-pass", "new-pass"), + CancellationToken.None + ); Assert.True(result.IsSuccess); - await _identityService.Received(1).ChangePasswordAsync(userId, "old-pass", "new-pass", Arg.Any()); + await _identityService + .Received(1) + .ChangePasswordAsync(userId, "old-pass", "new-pass", Arg.Any()); } [Fact] @@ -45,12 +66,19 @@ public class ChangePasswordCommandHandlerTests { var userId = Guid.NewGuid(); var error = Error.Validation("Auth.WrongCurrentPassword", "Текущий пароль неверен."); - _identityService.ChangePasswordAsync(userId, "wrong", "new-pass", Arg.Any()) + _identityService + .ChangePasswordAsync(userId, "wrong", "new-pass", Arg.Any()) .Returns(Result.Failure(error)); - var handler = new ChangePasswordCommandHandler(_identityService, FakeCurrentUser.Authenticated(userId)); + var handler = new ChangePasswordCommandHandler( + _identityService, + FakeCurrentUser.Authenticated(userId) + ); - var result = await handler.Handle(new ChangePasswordCommand("wrong", "new-pass"), CancellationToken.None); + var result = await handler.Handle( + new ChangePasswordCommand("wrong", "new-pass"), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(error, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Auth/GetCurrentUserQueryHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Auth/GetCurrentUserQueryHandlerTests.cs index 84bd19f..70a81dc 100644 --- a/backend/tests/PnvPanel.Application.Tests/Auth/GetCurrentUserQueryHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Auth/GetCurrentUserQueryHandlerTests.cs @@ -27,9 +27,14 @@ public class GetCurrentUserQueryHandlerTests public async Task Handle_ProfileMissing_ReturnsUnauthorized() { var userId = Guid.NewGuid(); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns((CurrentUserProfile?)null); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns((CurrentUserProfile?)null); - var handler = new GetCurrentUserQueryHandler(_identityService, FakeCurrentUser.Authenticated(userId)); + var handler = new GetCurrentUserQueryHandler( + _identityService, + FakeCurrentUser.Authenticated(userId) + ); var result = await handler.Handle(new GetCurrentUserQuery(), CancellationToken.None); @@ -41,12 +46,26 @@ public class GetCurrentUserQueryHandlerTests public async Task Handle_AuthenticatedWithProfile_ReturnsCurrentUserDto() { var userId = Guid.NewGuid(); - var profile = new CurrentUserProfile(userId, "alice", Guid.NewGuid(), "user", true, false, 3, RoleQuota.Unlimited, "sub-token"); + var profile = new CurrentUserProfile( + userId, + "alice", + Guid.NewGuid(), + "user", + true, + false, + 3, + RoleQuota.Unlimited, + "sub-token" + ); _identityService.GetProfileAsync(userId, Arg.Any()).Returns(profile); - _identityService.GetTelegramLinkInfoAsync(userId, Arg.Any()) + _identityService + .GetTelegramLinkInfoAsync(userId, Arg.Any()) .Returns(new TelegramLinkInfo(true, 42, "alice_tg")); - var handler = new GetCurrentUserQueryHandler(_identityService, FakeCurrentUser.Authenticated(userId, "alice")); + var handler = new GetCurrentUserQueryHandler( + _identityService, + FakeCurrentUser.Authenticated(userId, "alice") + ); var result = await handler.Handle(new GetCurrentUserQuery(), CancellationToken.None); diff --git a/backend/tests/PnvPanel.Application.Tests/Auth/LoginCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Auth/LoginCommandHandlerTests.cs index 712574e..7b19506 100644 --- a/backend/tests/PnvPanel.Application.Tests/Auth/LoginCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Auth/LoginCommandHandlerTests.cs @@ -11,9 +11,11 @@ public class LoginCommandHandlerTests { private readonly IIdentityService _identityService = Substitute.For(); private readonly IJwtTokenService _jwtTokenService = Substitute.For(); - private readonly IRefreshTokenService _refreshTokenService = Substitute.For(); + private readonly IRefreshTokenService _refreshTokenService = + Substitute.For(); - private LoginCommandHandler CreateHandler() => new(_identityService, _jwtTokenService, _refreshTokenService); + private LoginCommandHandler CreateHandler() => + new(_identityService, _jwtTokenService, _refreshTokenService); [Fact] public async Task Handle_WithValidCredentials_ReturnsAuthResult() @@ -21,20 +23,33 @@ public class LoginCommandHandlerTests var userId = Guid.NewGuid(); var authUser = new AuthenticatedUser(userId, "alice", "user"); var profile = new CurrentUserProfile( - userId, "alice", Guid.NewGuid(), "user", IsActivated: true, IsBlocked: false, MaxConfigs: 3, - MaxIpLimit: RoleQuota.Unlimited, SubscriptionToken: "sub-token"); + userId, + "alice", + Guid.NewGuid(), + "user", + IsActivated: true, + IsBlocked: false, + MaxConfigs: 3, + MaxIpLimit: RoleQuota.Unlimited, + SubscriptionToken: "sub-token" + ); - _identityService.ValidateCredentialsAsync("alice", "P@ssw0rd", Arg.Any()) + _identityService + .ValidateCredentialsAsync("alice", "P@ssw0rd", Arg.Any()) .Returns(Result.Success(authUser)); _identityService.GetProfileAsync(userId, Arg.Any()).Returns(profile); - _identityService.GetTelegramLinkInfoAsync(userId, Arg.Any()) + _identityService + .GetTelegramLinkInfoAsync(userId, Arg.Any()) .Returns(new TelegramLinkInfo(true, 123456, "alice_tg")); - _jwtTokenService.GenerateAccessToken(Arg.Any()) + _jwtTokenService + .GenerateAccessToken(Arg.Any()) .Returns(("access-token", DateTimeOffset.UtcNow.AddMinutes(15))); - _refreshTokenService.IssueAsync(userId, Arg.Any()) + _refreshTokenService + .IssueAsync(userId, Arg.Any()) .Returns(new IssuedRefreshToken("refresh-token", DateTimeOffset.UtcNow.AddDays(30))); - var result = await CreateHandler().Handle(new LoginCommand("alice", "P@ssw0rd"), CancellationToken.None); + var result = await CreateHandler() + .Handle(new LoginCommand("alice", "P@ssw0rd"), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal("access-token", result.Value.AccessToken); @@ -46,14 +61,18 @@ public class LoginCommandHandlerTests [Fact] public async Task Handle_WithInvalidCredentials_ReturnsFailureWithoutIssuingTokens() { - _identityService.ValidateCredentialsAsync("alice", "wrong", Arg.Any()) + _identityService + .ValidateCredentialsAsync("alice", "wrong", Arg.Any()) .Returns(Result.Failure(AuthErrors.InvalidCredentials)); - var result = await CreateHandler().Handle(new LoginCommand("alice", "wrong"), CancellationToken.None); + var result = await CreateHandler() + .Handle(new LoginCommand("alice", "wrong"), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(AuthErrors.InvalidCredentials, result.Error); - await _refreshTokenService.DidNotReceive().IssueAsync(Arg.Any(), Arg.Any()); + await _refreshTokenService + .DidNotReceive() + .IssueAsync(Arg.Any(), Arg.Any()); } [Fact] @@ -62,11 +81,15 @@ public class LoginCommandHandlerTests var userId = Guid.NewGuid(); var authUser = new AuthenticatedUser(userId, "alice", "user"); - _identityService.ValidateCredentialsAsync("alice", "P@ssw0rd", Arg.Any()) + _identityService + .ValidateCredentialsAsync("alice", "P@ssw0rd", Arg.Any()) .Returns(Result.Success(authUser)); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns((CurrentUserProfile?)null); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns((CurrentUserProfile?)null); - var result = await CreateHandler().Handle(new LoginCommand("alice", "P@ssw0rd"), CancellationToken.None); + var result = await CreateHandler() + .Handle(new LoginCommand("alice", "P@ssw0rd"), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(AuthErrors.InvalidCredentials, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Auth/RefreshCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Auth/RefreshCommandHandlerTests.cs index 93441cf..a69f304 100644 --- a/backend/tests/PnvPanel.Application.Tests/Auth/RefreshCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Auth/RefreshCommandHandlerTests.cs @@ -11,27 +11,46 @@ public class RefreshCommandHandlerTests { private readonly IIdentityService _identityService = Substitute.For(); private readonly IJwtTokenService _jwtTokenService = Substitute.For(); - private readonly IRefreshTokenService _refreshTokenService = Substitute.For(); + private readonly IRefreshTokenService _refreshTokenService = + Substitute.For(); - private RefreshCommandHandler CreateHandler() => new(_identityService, _jwtTokenService, _refreshTokenService); + private RefreshCommandHandler CreateHandler() => + new(_identityService, _jwtTokenService, _refreshTokenService); [Fact] public async Task Handle_WithValidToken_RotatesAndReturnsNewAuthResult() { var userId = Guid.NewGuid(); var profile = new CurrentUserProfile( - userId, "alice", Guid.NewGuid(), "user", IsActivated: true, IsBlocked: false, MaxConfigs: 3, - MaxIpLimit: RoleQuota.Unlimited, SubscriptionToken: "sub-token"); - var rotated = new RotatedRefreshToken(userId, "new-refresh-token", DateTimeOffset.UtcNow.AddDays(30)); + userId, + "alice", + Guid.NewGuid(), + "user", + IsActivated: true, + IsBlocked: false, + MaxConfigs: 3, + MaxIpLimit: RoleQuota.Unlimited, + SubscriptionToken: "sub-token" + ); + var rotated = new RotatedRefreshToken( + userId, + "new-refresh-token", + DateTimeOffset.UtcNow.AddDays(30) + ); - _refreshTokenService.RotateAsync("old-token", Arg.Any()).Returns(Result.Success(rotated)); + _refreshTokenService + .RotateAsync("old-token", Arg.Any()) + .Returns(Result.Success(rotated)); _identityService.GetProfileAsync(userId, Arg.Any()).Returns(profile); - _identityService.GetTelegramLinkInfoAsync(userId, Arg.Any()) + _identityService + .GetTelegramLinkInfoAsync(userId, Arg.Any()) .Returns(new TelegramLinkInfo(false, null, null)); - _jwtTokenService.GenerateAccessToken(Arg.Any()) + _jwtTokenService + .GenerateAccessToken(Arg.Any()) .Returns(("new-access-token", DateTimeOffset.UtcNow.AddMinutes(15))); - var result = await CreateHandler().Handle(new RefreshCommand("old-token"), CancellationToken.None); + var result = await CreateHandler() + .Handle(new RefreshCommand("old-token"), CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal("new-access-token", result.Value.AccessToken); @@ -41,26 +60,39 @@ public class RefreshCommandHandlerTests [Fact] public async Task Handle_WithInvalidOrReusedToken_ReturnsFailure() { - _refreshTokenService.RotateAsync("stolen-token", Arg.Any()) + _refreshTokenService + .RotateAsync("stolen-token", Arg.Any()) .Returns(Result.Failure(AuthErrors.InvalidRefreshToken)); - var result = await CreateHandler().Handle(new RefreshCommand("stolen-token"), CancellationToken.None); + var result = await CreateHandler() + .Handle(new RefreshCommand("stolen-token"), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(AuthErrors.InvalidRefreshToken, result.Error); - await _identityService.DidNotReceive().GetProfileAsync(Arg.Any(), Arg.Any()); + await _identityService + .DidNotReceive() + .GetProfileAsync(Arg.Any(), Arg.Any()); } [Fact] public async Task Handle_WhenProfileNoLongerExists_ReturnsInvalidRefreshToken() { var userId = Guid.NewGuid(); - var rotated = new RotatedRefreshToken(userId, "new-refresh-token", DateTimeOffset.UtcNow.AddDays(30)); + var rotated = new RotatedRefreshToken( + userId, + "new-refresh-token", + DateTimeOffset.UtcNow.AddDays(30) + ); - _refreshTokenService.RotateAsync("old-token", Arg.Any()).Returns(Result.Success(rotated)); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns((CurrentUserProfile?)null); + _refreshTokenService + .RotateAsync("old-token", Arg.Any()) + .Returns(Result.Success(rotated)); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns((CurrentUserProfile?)null); - var result = await CreateHandler().Handle(new RefreshCommand("old-token"), CancellationToken.None); + var result = await CreateHandler() + .Handle(new RefreshCommand("old-token"), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(AuthErrors.InvalidRefreshToken, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Common/Behaviors/RequireActivationBehaviorTests.cs b/backend/tests/PnvPanel.Application.Tests/Common/Behaviors/RequireActivationBehaviorTests.cs index f2b2b81..303ecf2 100644 --- a/backend/tests/PnvPanel.Application.Tests/Common/Behaviors/RequireActivationBehaviorTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Common/Behaviors/RequireActivationBehaviorTests.cs @@ -15,21 +15,37 @@ public class RequireActivationBehaviorTests private readonly ICurrentUser _currentUser = Substitute.For(); private readonly IIdentityService _identityService = Substitute.For(); - private RequireActivationBehavior> CreateBehavior() => new(_currentUser, _identityService); + private RequireActivationBehavior> CreateBehavior() => + new(_currentUser, _identityService); private static CurrentUserProfile CreateProfile(Guid userId, bool isActivated) => - new(userId, "alice", Guid.NewGuid(), "user", IsActivated: isActivated, IsBlocked: false, MaxConfigs: 3, - MaxIpLimit: RoleQuota.Unlimited, SubscriptionToken: "sub-token"); + new( + userId, + "alice", + Guid.NewGuid(), + "user", + IsActivated: isActivated, + IsBlocked: false, + MaxConfigs: 3, + MaxIpLimit: RoleQuota.Unlimited, + SubscriptionToken: "sub-token" + ); [Fact] public async Task Handle_WhenActivated_CallsNext() { var userId = Guid.NewGuid(); _currentUser.UserId.Returns(userId); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns(CreateProfile(userId, isActivated: true)); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns(CreateProfile(userId, isActivated: true)); - var result = await CreateBehavior().Handle( - new DummyRequest(), () => Task.FromResult(Result.Success("ok")), CancellationToken.None); + var result = await CreateBehavior() + .Handle( + new DummyRequest(), + () => Task.FromResult(Result.Success("ok")), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal("ok", result.Value); @@ -40,17 +56,21 @@ public class RequireActivationBehaviorTests { var userId = Guid.NewGuid(); _currentUser.UserId.Returns(userId); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns(CreateProfile(userId, isActivated: false)); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns(CreateProfile(userId, isActivated: false)); var nextCalled = false; - var result = await CreateBehavior().Handle( - new DummyRequest(), - () => - { - nextCalled = true; - return Task.FromResult(Result.Success("ok")); - }, - CancellationToken.None); + var result = await CreateBehavior() + .Handle( + new DummyRequest(), + () => + { + nextCalled = true; + return Task.FromResult(Result.Success("ok")); + }, + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(AuthErrors.NotActivated, result.Error); @@ -62,8 +82,12 @@ public class RequireActivationBehaviorTests { _currentUser.UserId.Returns((Guid?)null); - var result = await CreateBehavior().Handle( - new DummyRequest(), () => Task.FromResult(Result.Success("ok")), CancellationToken.None); + var result = await CreateBehavior() + .Handle( + new DummyRequest(), + () => Task.FromResult(Result.Success("ok")), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(AuthErrors.Unauthorized, result.Error); @@ -74,10 +98,16 @@ public class RequireActivationBehaviorTests { var userId = Guid.NewGuid(); _currentUser.UserId.Returns(userId); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns((CurrentUserProfile?)null); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns((CurrentUserProfile?)null); - var result = await CreateBehavior().Handle( - new DummyRequest(), () => Task.FromResult(Result.Success("ok")), CancellationToken.None); + var result = await CreateBehavior() + .Handle( + new DummyRequest(), + () => Task.FromResult(Result.Success("ok")), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(AuthErrors.Unauthorized, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Configs/GetMyConfigs/GetMyConfigsQueryHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Configs/GetMyConfigs/GetMyConfigsQueryHandlerTests.cs index cb80e53..bfabb4a 100644 --- a/backend/tests/PnvPanel.Application.Tests/Configs/GetMyConfigs/GetMyConfigsQueryHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Configs/GetMyConfigs/GetMyConfigsQueryHandlerTests.cs @@ -27,16 +27,35 @@ public class GetMyConfigsQueryHandlerTests 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"); + var otherUsersConfig = VpnConfig.Create( + otherUserId, + inbound.Id, + VpnProtocol.Vless, + "Other" + ); dbContext.Inbounds.Add(inbound); dbContext.VpnConfigs.AddRange(activeConfig, revokedConfig, otherUsersConfig); await dbContext.SaveChangesAsync(CancellationToken.None); - var profile = new CurrentUserProfile(userId, "alice", Guid.NewGuid(), "user", true, false, 5, RoleQuota.Unlimited, "sub-token"); + var profile = new CurrentUserProfile( + userId, + "alice", + Guid.NewGuid(), + "user", + true, + false, + 5, + RoleQuota.Unlimited, + "sub-token" + ); _identityService.GetProfileAsync(userId, Arg.Any()).Returns(profile); - var handler = new GetMyConfigsQueryHandler(dbContext, _identityService, FakeCurrentUser.Authenticated(userId)); + var handler = new GetMyConfigsQueryHandler( + dbContext, + _identityService, + FakeCurrentUser.Authenticated(userId) + ); var result = await handler.Handle(new GetMyConfigsQuery(), CancellationToken.None); @@ -51,7 +70,11 @@ public class GetMyConfigsQueryHandlerTests { using var dbContext = InMemoryDbContextFactory.Create(); - var handler = new GetMyConfigsQueryHandler(dbContext, _identityService, FakeCurrentUser.Anonymous()); + var handler = new GetMyConfigsQueryHandler( + dbContext, + _identityService, + FakeCurrentUser.Anonymous() + ); var result = await handler.Handle(new GetMyConfigsQuery(), CancellationToken.None); @@ -64,9 +87,15 @@ public class GetMyConfigsQueryHandlerTests { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns((CurrentUserProfile?)null); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns((CurrentUserProfile?)null); - var handler = new GetMyConfigsQueryHandler(dbContext, _identityService, FakeCurrentUser.Authenticated(userId)); + var handler = new GetMyConfigsQueryHandler( + dbContext, + _identityService, + FakeCurrentUser.Authenticated(userId) + ); var result = await handler.Handle(new GetMyConfigsQuery(), CancellationToken.None); diff --git a/backend/tests/PnvPanel.Application.Tests/Configs/Revoke/RevokeVpnConfigCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Configs/Revoke/RevokeVpnConfigCommandHandlerTests.cs index bff99bb..69f9cfa 100644 --- a/backend/tests/PnvPanel.Application.Tests/Configs/Revoke/RevokeVpnConfigCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Configs/Revoke/RevokeVpnConfigCommandHandlerTests.cs @@ -21,7 +21,12 @@ public class RevokeVpnConfigCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); - var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); + 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); config.AssignRemoteClient("external-id"); @@ -31,15 +36,37 @@ public class RevokeVpnConfigCommandHandlerTests dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); - var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Authenticated(userId)); + var handler = new RevokeVpnConfigCommandHandler( + dbContext, + _gateway, + _notifier, + FakeCurrentUser.Authenticated(userId) + ); - var result = await handler.Handle(new RevokeVpnConfigCommand(config.Id), CancellationToken.None); + var result = await handler.Handle( + new RevokeVpnConfigCommand(config.Id), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal(ConfigStatus.Revoked, config.Status); - await _gateway.Received(1).RemoveClientAsync( - Arg.Any(), inbound.RemoteInboundId, "external-id", config.Protocol, Arg.Any()); - await _notifier.Received(1).NotifyConfigStatusChangedAsync(userId, config.Id, ConfigStatus.Revoked, Arg.Any()); + await _gateway + .Received(1) + .RemoveClientAsync( + Arg.Any(), + inbound.RemoteInboundId, + "external-id", + config.Protocol, + Arg.Any() + ); + await _notifier + .Received(1) + .NotifyConfigStatusChangedAsync( + userId, + config.Id, + ConfigStatus.Revoked, + Arg.Any() + ); } [Fact] @@ -56,13 +83,28 @@ public class RevokeVpnConfigCommandHandlerTests dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); - var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Authenticated(userId)); + var handler = new RevokeVpnConfigCommandHandler( + dbContext, + _gateway, + _notifier, + FakeCurrentUser.Authenticated(userId) + ); - var result = await handler.Handle(new RevokeVpnConfigCommand(config.Id), CancellationToken.None); + var result = await handler.Handle( + new RevokeVpnConfigCommand(config.Id), + CancellationToken.None + ); Assert.True(result.IsSuccess); - await _gateway.DidNotReceive().RemoveClientAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _gateway + .DidNotReceive() + .RemoveClientAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } [Fact] @@ -71,9 +113,17 @@ public class RevokeVpnConfigCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); - var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Authenticated(userId)); + var handler = new RevokeVpnConfigCommandHandler( + dbContext, + _gateway, + _notifier, + FakeCurrentUser.Authenticated(userId) + ); - var result = await handler.Handle(new RevokeVpnConfigCommand(Guid.NewGuid()), CancellationToken.None); + var result = await handler.Handle( + new RevokeVpnConfigCommand(Guid.NewGuid()), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(ConfigErrors.NotFound, result.Error); @@ -84,9 +134,17 @@ public class RevokeVpnConfigCommandHandlerTests { using var dbContext = InMemoryDbContextFactory.Create(); - var handler = new RevokeVpnConfigCommandHandler(dbContext, _gateway, _notifier, FakeCurrentUser.Anonymous()); + var handler = new RevokeVpnConfigCommandHandler( + dbContext, + _gateway, + _notifier, + FakeCurrentUser.Anonymous() + ); - var result = await handler.Handle(new RevokeVpnConfigCommand(Guid.NewGuid()), CancellationToken.None); + var result = await handler.Handle( + new RevokeVpnConfigCommand(Guid.NewGuid()), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(PnvPanel.Application.Auth.AuthErrors.Unauthorized, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Configs/Rotate/RotateVpnConfigCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Configs/Rotate/RotateVpnConfigCommandHandlerTests.cs index 6bb36bb..3c0d4cf 100644 --- a/backend/tests/PnvPanel.Application.Tests/Configs/Rotate/RotateVpnConfigCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Configs/Rotate/RotateVpnConfigCommandHandlerTests.cs @@ -18,16 +18,33 @@ public class RotateVpnConfigCommandHandlerTests private readonly IIdentityService _identityService = Substitute.For(); private static CurrentUserProfile MakeProfile(Guid userId) => - new(userId, "alice", Guid.NewGuid(), "user", true, false, 3, RoleQuota.Unlimited, "sub-token"); + new( + userId, + "alice", + Guid.NewGuid(), + "user", + true, + false, + 3, + RoleQuota.Unlimited, + "sub-token" + ); [Fact] public async Task Handle_WhenActiveConfigOwnedByUser_RotatesAndAddsNewClient() { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns(MakeProfile(userId)); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns(MakeProfile(userId)); - var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); + 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"); config.AssignRemoteClient("old-external-id"); @@ -37,19 +54,41 @@ public class RotateVpnConfigCommandHandlerTests dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); - _gateway.AddClientAsync( - Arg.Any(), inbound.RemoteInboundId, config.Protocol, Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()) + _gateway + .AddClientAsync( + Arg.Any(), + inbound.RemoteInboundId, + config.Protocol, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ) .Returns(Result.Success("new-external-id")); - var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, _identityService, FakeCurrentUser.Authenticated(userId)); + var handler = new RotateVpnConfigCommandHandler( + dbContext, + _gateway, + _identityService, + FakeCurrentUser.Authenticated(userId) + ); - var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None); + var result = await handler.Handle( + new RotateVpnConfigCommand(config.Id), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal("new-external-id", config.ClientExternalId); - await _gateway.Received(1).RemoveClientAsync( - Arg.Any(), inbound.RemoteInboundId, "old-external-id", config.Protocol, Arg.Any()); + await _gateway + .Received(1) + .RemoveClientAsync( + Arg.Any(), + inbound.RemoteInboundId, + "old-external-id", + config.Protocol, + Arg.Any() + ); } [Fact] @@ -58,9 +97,17 @@ public class RotateVpnConfigCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); - var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, _identityService, FakeCurrentUser.Authenticated(userId)); + var handler = new RotateVpnConfigCommandHandler( + dbContext, + _gateway, + _identityService, + FakeCurrentUser.Authenticated(userId) + ); - var result = await handler.Handle(new RotateVpnConfigCommand(Guid.NewGuid()), CancellationToken.None); + var result = await handler.Handle( + new RotateVpnConfigCommand(Guid.NewGuid()), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(ConfigErrors.NotFound, result.Error); @@ -80,9 +127,17 @@ public class RotateVpnConfigCommandHandlerTests dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); - var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, _identityService, FakeCurrentUser.Authenticated(otherUserId)); + var handler = new RotateVpnConfigCommandHandler( + dbContext, + _gateway, + _identityService, + FakeCurrentUser.Authenticated(otherUserId) + ); - var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None); + var result = await handler.Handle( + new RotateVpnConfigCommand(config.Id), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(ConfigErrors.NotFound, result.Error); @@ -102,9 +157,17 @@ public class RotateVpnConfigCommandHandlerTests dbContext.VpnConfigs.Add(config); await dbContext.SaveChangesAsync(CancellationToken.None); - var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, _identityService, FakeCurrentUser.Authenticated(userId)); + var handler = new RotateVpnConfigCommandHandler( + dbContext, + _gateway, + _identityService, + FakeCurrentUser.Authenticated(userId) + ); - var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None); + var result = await handler.Handle( + new RotateVpnConfigCommand(config.Id), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(ConfigErrors.NotFound, result.Error); @@ -115,9 +178,16 @@ public class RotateVpnConfigCommandHandlerTests { using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns(MakeProfile(userId)); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns(MakeProfile(userId)); - var node = Node.Register("node-1", new Uri("https://node1.example.com"), new NodeCredentials("admin", "protected"), null); + 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); config.AssignRemoteClient("old-external-id"); @@ -128,14 +198,29 @@ public class RotateVpnConfigCommandHandlerTests await dbContext.SaveChangesAsync(CancellationToken.None); var gatewayError = Error.Failure("Xui.Unreachable", "Панель недоступна."); - _gateway.AddClientAsync( - Arg.Any(), inbound.RemoteInboundId, config.Protocol, Arg.Any(), - Arg.Any(), Arg.Any(), Arg.Any()) + _gateway + .AddClientAsync( + Arg.Any(), + inbound.RemoteInboundId, + config.Protocol, + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ) .Returns(Result.Failure(gatewayError)); - var handler = new RotateVpnConfigCommandHandler(dbContext, _gateway, _identityService, FakeCurrentUser.Authenticated(userId)); + var handler = new RotateVpnConfigCommandHandler( + dbContext, + _gateway, + _identityService, + FakeCurrentUser.Authenticated(userId) + ); - var result = await handler.Handle(new RotateVpnConfigCommand(config.Id), CancellationToken.None); + var result = await handler.Handle( + new RotateVpnConfigCommand(config.Id), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(gatewayError, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/PnvPanel.Application.Tests.csproj b/backend/tests/PnvPanel.Application.Tests/PnvPanel.Application.Tests.csproj index 9703d6d..c30cbd8 100644 --- a/backend/tests/PnvPanel.Application.Tests/PnvPanel.Application.Tests.csproj +++ b/backend/tests/PnvPanel.Application.Tests/PnvPanel.Application.Tests.csproj @@ -1,5 +1,4 @@ - net10.0 enable @@ -24,5 +23,4 @@ - diff --git a/backend/tests/PnvPanel.Application.Tests/Support/AddTicketCommentCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Support/AddTicketCommentCommandHandlerTests.cs index 89b4a98..bb8adc6 100644 --- a/backend/tests/PnvPanel.Application.Tests/Support/AddTicketCommentCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Support/AddTicketCommentCommandHandlerTests.cs @@ -16,8 +16,17 @@ public class AddTicketCommentCommandHandlerTests private readonly IRealtimeNotifier _notifier = Substitute.For(); private static CurrentUserProfile Profile(Guid userId, string role) => - new(userId, "user", Guid.NewGuid(), role, IsActivated: true, IsBlocked: false, MaxConfigs: 3, - MaxIpLimit: RoleQuota.Unlimited, SubscriptionToken: "token"); + new( + userId, + "user", + Guid.NewGuid(), + role, + IsActivated: true, + IsBlocked: false, + MaxConfigs: 3, + MaxIpLimit: RoleQuota.Unlimited, + SubscriptionToken: "token" + ); [Fact] public async Task Handle_WhenOwnerComments_AddsCommentAndDoesNotNotifySelf() @@ -28,16 +37,33 @@ public class AddTicketCommentCommandHandlerTests dbContext.SupportTickets.Add(ticket); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns(Profile(userId, "user")); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns(Profile(userId, "user")); var currentUser = FakeCurrentUser.Authenticated(userId, "alice"); - var handler = new AddTicketCommentCommandHandler(dbContext, _identityService, _fileStorage, _notifier, currentUser); + var handler = new AddTicketCommentCommandHandler( + dbContext, + _identityService, + _fileStorage, + _notifier, + currentUser + ); - var result = await handler.Handle(new AddTicketCommentCommand(ticket.Id, "апдейт", []), CancellationToken.None); + var result = await handler.Handle( + new AddTicketCommentCommand(ticket.Id, "апдейт", []), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal("апдейт", result.Value.Body); - await _notifier.DidNotReceive().NotifyTicketUpdatedAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await _notifier + .DidNotReceive() + .NotifyTicketUpdatedAsync( + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } [Fact] @@ -50,15 +76,28 @@ public class AddTicketCommentCommandHandlerTests dbContext.SupportTickets.Add(ticket); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.GetProfileAsync(adminId, Arg.Any()).Returns(Profile(adminId, "admin")); + _identityService + .GetProfileAsync(adminId, Arg.Any()) + .Returns(Profile(adminId, "admin")); var currentUser = FakeCurrentUser.Authenticated(adminId, "admin"); - var handler = new AddTicketCommentCommandHandler(dbContext, _identityService, _fileStorage, _notifier, currentUser); + var handler = new AddTicketCommentCommandHandler( + dbContext, + _identityService, + _fileStorage, + _notifier, + currentUser + ); - var result = await handler.Handle(new AddTicketCommentCommand(ticket.Id, "ответ админа", []), CancellationToken.None); + var result = await handler.Handle( + new AddTicketCommentCommand(ticket.Id, "ответ админа", []), + CancellationToken.None + ); Assert.True(result.IsSuccess); - await _notifier.Received(1).NotifyTicketUpdatedAsync(ticket.Id, ownerId, Arg.Any()); + await _notifier + .Received(1) + .NotifyTicketUpdatedAsync(ticket.Id, ownerId, Arg.Any()); } [Fact] @@ -71,12 +110,23 @@ public class AddTicketCommentCommandHandlerTests dbContext.SupportTickets.Add(ticket); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.GetProfileAsync(strangerId, Arg.Any()).Returns(Profile(strangerId, "user")); + _identityService + .GetProfileAsync(strangerId, Arg.Any()) + .Returns(Profile(strangerId, "user")); var currentUser = FakeCurrentUser.Authenticated(strangerId); - var handler = new AddTicketCommentCommandHandler(dbContext, _identityService, _fileStorage, _notifier, currentUser); + var handler = new AddTicketCommentCommandHandler( + dbContext, + _identityService, + _fileStorage, + _notifier, + currentUser + ); - var result = await handler.Handle(new AddTicketCommentCommand(ticket.Id, "текст", []), CancellationToken.None); + var result = await handler.Handle( + new AddTicketCommentCommand(ticket.Id, "текст", []), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(SupportErrors.NotFound, result.Error); @@ -92,12 +142,23 @@ public class AddTicketCommentCommandHandlerTests dbContext.SupportTickets.Add(ticket); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns(Profile(userId, "user")); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns(Profile(userId, "user")); var currentUser = FakeCurrentUser.Authenticated(userId); - var handler = new AddTicketCommentCommandHandler(dbContext, _identityService, _fileStorage, _notifier, currentUser); + var handler = new AddTicketCommentCommandHandler( + dbContext, + _identityService, + _fileStorage, + _notifier, + currentUser + ); - var result = await handler.Handle(new AddTicketCommentCommand(ticket.Id, "текст", []), CancellationToken.None); + var result = await handler.Handle( + new AddTicketCommentCommand(ticket.Id, "текст", []), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(SupportErrors.TicketClosed, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Support/CreateBugReportTicketCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Support/CreateBugReportTicketCommandHandlerTests.cs index fb0d301..06c9415 100644 --- a/backend/tests/PnvPanel.Application.Tests/Support/CreateBugReportTicketCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Support/CreateBugReportTicketCommandHandlerTests.cs @@ -21,9 +21,18 @@ public class CreateBugReportTicketCommandHandlerTests var userId = Guid.NewGuid(); var currentUser = FakeCurrentUser.Authenticated(userId, "alice"); - var handler = new CreateBugReportTicketCommandHandler(dbContext, _fileStorage, _notifier, _telegramNotifier, currentUser); + var handler = new CreateBugReportTicketCommandHandler( + dbContext, + _fileStorage, + _notifier, + _telegramNotifier, + currentUser + ); - var result = await handler.Handle(new CreateBugReportTicketCommand("Что-то сломалось", []), CancellationToken.None); + var result = await handler.Handle( + new CreateBugReportTicketCommand("Что-то сломалось", []), + CancellationToken.None + ); await dbContext.SaveChangesAsync(CancellationToken.None); Assert.True(result.IsSuccess); @@ -33,8 +42,14 @@ public class CreateBugReportTicketCommandHandlerTests Assert.Equal("Что-то сломалось", result.Value.Comments[0].Body); Assert.Single(dbContext.SupportTickets); Assert.Single(dbContext.TicketComments); - await _telegramNotifier.Received(1).NotifyAdminsBugReportCreatedAsync( - Arg.Any(), "alice", "Что-то сломалось", Arg.Any()); + await _telegramNotifier + .Received(1) + .NotifyAdminsBugReportCreatedAsync( + Arg.Any(), + "alice", + "Что-то сломалось", + Arg.Any() + ); } [Fact] @@ -42,13 +57,23 @@ public class CreateBugReportTicketCommandHandlerTests { using var dbContext = InMemoryDbContextFactory.Create(); var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid()); - var attachments = Enumerable.Range(0, 6) + var attachments = Enumerable + .Range(0, 6) .Select(_ => new TicketAttachmentUpload(new MemoryStream(), "a.png", "image/png", 10)) .ToList(); - var handler = new CreateBugReportTicketCommandHandler(dbContext, _fileStorage, _notifier, _telegramNotifier, currentUser); + var handler = new CreateBugReportTicketCommandHandler( + dbContext, + _fileStorage, + _notifier, + _telegramNotifier, + currentUser + ); - var result = await handler.Handle(new CreateBugReportTicketCommand("текст", attachments), CancellationToken.None); + var result = await handler.Handle( + new CreateBugReportTicketCommand("текст", attachments), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(SupportErrors.TooManyAttachments, result.Error); @@ -59,11 +84,23 @@ public class CreateBugReportTicketCommandHandlerTests { using var dbContext = InMemoryDbContextFactory.Create(); var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid()); - var attachments = new[] { new TicketAttachmentUpload(new MemoryStream(), "a.exe", "application/x-msdownload", 10) }; + var attachments = new[] + { + new TicketAttachmentUpload(new MemoryStream(), "a.exe", "application/x-msdownload", 10), + }; - var handler = new CreateBugReportTicketCommandHandler(dbContext, _fileStorage, _notifier, _telegramNotifier, currentUser); + var handler = new CreateBugReportTicketCommandHandler( + dbContext, + _fileStorage, + _notifier, + _telegramNotifier, + currentUser + ); - var result = await handler.Handle(new CreateBugReportTicketCommand("текст", attachments), CancellationToken.None); + var result = await handler.Handle( + new CreateBugReportTicketCommand("текст", attachments), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(SupportErrors.UnsupportedAttachmentType, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Support/CreateRoleRequestTicketCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Support/CreateRoleRequestTicketCommandHandlerTests.cs index ce87d03..0aa8ab8 100644 --- a/backend/tests/PnvPanel.Application.Tests/Support/CreateRoleRequestTicketCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Support/CreateRoleRequestTicketCommandHandlerTests.cs @@ -21,20 +21,36 @@ public class CreateRoleRequestTicketCommandHandlerTests var userId = Guid.NewGuid(); var roleId = Guid.NewGuid(); var currentUser = FakeCurrentUser.Authenticated(userId, "alice"); - _roleService.ListRolesAsync(Arg.Any()) + _roleService + .ListRolesAsync(Arg.Any()) .Returns(new List { new(roleId, "premium", 5, 2, false) }); - var handler = new CreateRoleRequestTicketCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser); + var handler = new CreateRoleRequestTicketCommandHandler( + dbContext, + _roleService, + _notifier, + _telegramNotifier, + currentUser + ); var result = await handler.Handle( - new CreateRoleRequestTicketCommand(roleId, null, null, null, "нужно больше конфигов"), CancellationToken.None); + new CreateRoleRequestTicketCommand(roleId, null, null, null, "нужно больше конфигов"), + CancellationToken.None + ); await dbContext.SaveChangesAsync(CancellationToken.None); Assert.True(result.IsSuccess); Assert.Equal(roleId, result.Value.RequestedRoleId); Assert.Equal("premium", result.Value.RequestedRoleName); - await _telegramNotifier.Received(1).NotifyAdminsRoleRequestCreatedAsync( - Arg.Any(), "alice", "premium", "нужно больше конфигов", Arg.Any()); + await _telegramNotifier + .Received(1) + .NotifyAdminsRoleRequestCreatedAsync( + Arg.Any(), + "alice", + "premium", + "нужно больше конфигов", + Arg.Any() + ); } [Fact] @@ -43,13 +59,22 @@ public class CreateRoleRequestTicketCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var roleId = Guid.NewGuid(); var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid()); - _roleService.ListRolesAsync(Arg.Any()) + _roleService + .ListRolesAsync(Arg.Any()) .Returns(new List { new(roleId, "admin", -1, -1, true) }); - var handler = new CreateRoleRequestTicketCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser); + var handler = new CreateRoleRequestTicketCommandHandler( + dbContext, + _roleService, + _notifier, + _telegramNotifier, + currentUser + ); var result = await handler.Handle( - new CreateRoleRequestTicketCommand(roleId, null, null, null, "хочу быть админом"), CancellationToken.None); + new CreateRoleRequestTicketCommand(roleId, null, null, null, "хочу быть админом"), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(SupportErrors.CannotRequestAdminRole, result.Error); @@ -61,10 +86,18 @@ public class CreateRoleRequestTicketCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid(), "bob"); - var handler = new CreateRoleRequestTicketCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser); + var handler = new CreateRoleRequestTicketCommandHandler( + dbContext, + _roleService, + _notifier, + _telegramNotifier, + currentUser + ); var result = await handler.Handle( - new CreateRoleRequestTicketCommand(null, "custom", 10, 4, "нужна кастомная роль"), CancellationToken.None); + new CreateRoleRequestTicketCommand(null, "custom", 10, 4, "нужна кастомная роль"), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal("custom", result.Value.ProposedRoleName); @@ -81,10 +114,18 @@ public class CreateRoleRequestTicketCommandHandlerTests await dbContext.SaveChangesAsync(CancellationToken.None); var currentUser = FakeCurrentUser.Authenticated(userId); - var handler = new CreateRoleRequestTicketCommandHandler(dbContext, _roleService, _notifier, _telegramNotifier, currentUser); + var handler = new CreateRoleRequestTicketCommandHandler( + dbContext, + _roleService, + _notifier, + _telegramNotifier, + currentUser + ); var result = await handler.Handle( - new CreateRoleRequestTicketCommand(null, "y", 2, 2, "ещё заявка"), CancellationToken.None); + new CreateRoleRequestTicketCommand(null, "y", 2, 2, "ещё заявка"), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(SupportErrors.RoleRequestAlreadyPending, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Support/ReopenTicketCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Support/ReopenTicketCommandHandlerTests.cs index fbe28fe..58e31ec 100644 --- a/backend/tests/PnvPanel.Application.Tests/Support/ReopenTicketCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Support/ReopenTicketCommandHandlerTests.cs @@ -25,12 +25,21 @@ public class ReopenTicketCommandHandlerTests var currentUser = FakeCurrentUser.Authenticated(userId); var handler = new ReopenTicketCommandHandler(dbContext, _telegramNotifier, currentUser); - var result = await handler.Handle(new ReopenTicketCommand(ticket.Id), CancellationToken.None); + var result = await handler.Handle( + new ReopenTicketCommand(ticket.Id), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal(TicketStatus.Open, ticket.Status); - await _telegramNotifier.Received(1).NotifyAdminsTicketReopenedAsync( - ticket.Id, Arg.Any(), TicketType.BugReport, Arg.Any()); + await _telegramNotifier + .Received(1) + .NotifyAdminsTicketReopenedAsync( + ticket.Id, + Arg.Any(), + TicketType.BugReport, + Arg.Any() + ); } [Fact] @@ -45,7 +54,10 @@ public class ReopenTicketCommandHandlerTests var currentUser = FakeCurrentUser.Authenticated(userId); var handler = new ReopenTicketCommandHandler(dbContext, _telegramNotifier, currentUser); - var result = await handler.Handle(new ReopenTicketCommand(ticket.Id), CancellationToken.None); + var result = await handler.Handle( + new ReopenTicketCommand(ticket.Id), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(SupportErrors.NotResolved, result.Error); @@ -63,7 +75,10 @@ public class ReopenTicketCommandHandlerTests var currentUser = FakeCurrentUser.Authenticated(Guid.NewGuid()); var handler = new ReopenTicketCommandHandler(dbContext, _telegramNotifier, currentUser); - var result = await handler.Handle(new ReopenTicketCommand(ticket.Id), CancellationToken.None); + var result = await handler.Handle( + new ReopenTicketCommand(ticket.Id), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(SupportErrors.NotFound, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/ApproveTelegramLoginCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/ApproveTelegramLoginCommandHandlerTests.cs index 51bc93a..7014b57 100644 --- a/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/ApproveTelegramLoginCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/ApproveTelegramLoginCommandHandlerTests.cs @@ -23,11 +23,16 @@ public class ApproveTelegramLoginCommandHandlerTests dbContext.TelegramLoginRequests.Add(request); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()).Returns(userId); + _identityService + .FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()) + .Returns(userId); var handler = new ApproveTelegramLoginCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new ApproveTelegramLoginCommand(request.Id, telegramUserId), CancellationToken.None); + var result = await handler.Handle( + new ApproveTelegramLoginCommand(request.Id, telegramUserId), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal(TelegramLoginStatus.Approved, request.Status); @@ -40,12 +45,16 @@ public class ApproveTelegramLoginCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); const long telegramUserId = 123456L; - _identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()) + _identityService + .FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()) .Returns((Guid?)null); var handler = new ApproveTelegramLoginCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new ApproveTelegramLoginCommand(Guid.NewGuid(), telegramUserId), CancellationToken.None); + var result = await handler.Handle( + new ApproveTelegramLoginCommand(Guid.NewGuid(), telegramUserId), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(TelegramErrors.NotLinked, result.Error); @@ -58,11 +67,16 @@ public class ApproveTelegramLoginCommandHandlerTests var userId = Guid.NewGuid(); const long telegramUserId = 123456L; - _identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()).Returns(userId); + _identityService + .FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()) + .Returns(userId); var handler = new ApproveTelegramLoginCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new ApproveTelegramLoginCommand(Guid.NewGuid(), telegramUserId), CancellationToken.None); + var result = await handler.Handle( + new ApproveTelegramLoginCommand(Guid.NewGuid(), telegramUserId), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(TelegramErrors.LoginRequestNotFound, result.Error); @@ -80,11 +94,16 @@ public class ApproveTelegramLoginCommandHandlerTests dbContext.TelegramLoginRequests.Add(request); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()).Returns(userId); + _identityService + .FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()) + .Returns(userId); var handler = new ApproveTelegramLoginCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new ApproveTelegramLoginCommand(request.Id, telegramUserId), CancellationToken.None); + var result = await handler.Handle( + new ApproveTelegramLoginCommand(request.Id, telegramUserId), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal("Telegram.LoginRequestInvalid", result.Error.Code); @@ -101,11 +120,16 @@ public class ApproveTelegramLoginCommandHandlerTests dbContext.TelegramLoginRequests.Add(request); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()).Returns(userId); + _identityService + .FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()) + .Returns(userId); var handler = new ApproveTelegramLoginCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new ApproveTelegramLoginCommand(request.Id, telegramUserId), CancellationToken.None); + var result = await handler.Handle( + new ApproveTelegramLoginCommand(request.Id, telegramUserId), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal("Telegram.LoginRequestInvalid", result.Error.Code); diff --git a/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/LinkTelegramCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/LinkTelegramCommandHandlerTests.cs index 358631b..6517dbd 100644 --- a/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/LinkTelegramCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/LinkTelegramCommandHandlerTests.cs @@ -22,12 +22,16 @@ public class LinkTelegramCommandHandlerTests dbContext.TelegramLinkTokens.Add(token); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.LinkTelegramAsync(userId, 123456L, "alice_tg", Arg.Any()) + _identityService + .LinkTelegramAsync(userId, 123456L, "alice_tg", Arg.Any()) .Returns(Result.Success()); var handler = new LinkTelegramCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new LinkTelegramCommand(token.Token, 123456L, "alice_tg"), CancellationToken.None); + var result = await handler.Handle( + new LinkTelegramCommand(token.Token, 123456L, "alice_tg"), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal(userId, result.Value); @@ -41,7 +45,10 @@ public class LinkTelegramCommandHandlerTests var handler = new LinkTelegramCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new LinkTelegramCommand("missing-token", 123456L, null), CancellationToken.None); + var result = await handler.Handle( + new LinkTelegramCommand("missing-token", 123456L, null), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(TelegramErrors.LinkTokenNotFound, result.Error); @@ -59,12 +66,21 @@ public class LinkTelegramCommandHandlerTests var handler = new LinkTelegramCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new LinkTelegramCommand(token.Token, 123456L, null), CancellationToken.None); + var result = await handler.Handle( + new LinkTelegramCommand(token.Token, 123456L, null), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(TelegramErrors.LinkTokenNotFound, result.Error); - await _identityService.DidNotReceive().LinkTelegramAsync( - Arg.Any(), Arg.Any(), Arg.Any(), Arg.Any()); + await _identityService + .DidNotReceive() + .LinkTelegramAsync( + Arg.Any(), + Arg.Any(), + Arg.Any(), + Arg.Any() + ); } [Fact] @@ -78,7 +94,10 @@ public class LinkTelegramCommandHandlerTests var handler = new LinkTelegramCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new LinkTelegramCommand(token.Token, 123456L, null), CancellationToken.None); + var result = await handler.Handle( + new LinkTelegramCommand(token.Token, 123456L, null), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(TelegramErrors.LinkTokenNotFound, result.Error); @@ -93,13 +112,20 @@ public class LinkTelegramCommandHandlerTests dbContext.TelegramLinkTokens.Add(token); await dbContext.SaveChangesAsync(CancellationToken.None); - var error = Error.Conflict("Telegram.AlreadyLinked", "Этот Telegram уже привязан к другому аккаунту."); - _identityService.LinkTelegramAsync(userId, 123456L, null, Arg.Any()) + var error = Error.Conflict( + "Telegram.AlreadyLinked", + "Этот Telegram уже привязан к другому аккаунту." + ); + _identityService + .LinkTelegramAsync(userId, 123456L, null, Arg.Any()) .Returns(Result.Failure(error)); var handler = new LinkTelegramCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new LinkTelegramCommand(token.Token, 123456L, null), CancellationToken.None); + var result = await handler.Handle( + new LinkTelegramCommand(token.Token, 123456L, null), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(error, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/RejectTelegramLoginCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/RejectTelegramLoginCommandHandlerTests.cs index b1b632b..3a3d984 100644 --- a/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/RejectTelegramLoginCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Telegram/Bot/RejectTelegramLoginCommandHandlerTests.cs @@ -23,11 +23,16 @@ public class RejectTelegramLoginCommandHandlerTests dbContext.TelegramLoginRequests.Add(request); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()).Returns(userId); + _identityService + .FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()) + .Returns(userId); var handler = new RejectTelegramLoginCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new RejectTelegramLoginCommand(request.Id, telegramUserId), CancellationToken.None); + var result = await handler.Handle( + new RejectTelegramLoginCommand(request.Id, telegramUserId), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal(TelegramLoginStatus.Rejected, request.Status); @@ -39,12 +44,16 @@ public class RejectTelegramLoginCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); const long telegramUserId = 123456L; - _identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()) + _identityService + .FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()) .Returns((Guid?)null); var handler = new RejectTelegramLoginCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new RejectTelegramLoginCommand(Guid.NewGuid(), telegramUserId), CancellationToken.None); + var result = await handler.Handle( + new RejectTelegramLoginCommand(Guid.NewGuid(), telegramUserId), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(TelegramErrors.NotLinked, result.Error); @@ -57,11 +66,16 @@ public class RejectTelegramLoginCommandHandlerTests var userId = Guid.NewGuid(); const long telegramUserId = 123456L; - _identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()).Returns(userId); + _identityService + .FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()) + .Returns(userId); var handler = new RejectTelegramLoginCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new RejectTelegramLoginCommand(Guid.NewGuid(), telegramUserId), CancellationToken.None); + var result = await handler.Handle( + new RejectTelegramLoginCommand(Guid.NewGuid(), telegramUserId), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(TelegramErrors.LoginRequestNotFound, result.Error); @@ -79,11 +93,16 @@ public class RejectTelegramLoginCommandHandlerTests dbContext.TelegramLoginRequests.Add(request); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()).Returns(userId); + _identityService + .FindUserIdByTelegramUserIdAsync(telegramUserId, Arg.Any()) + .Returns(userId); var handler = new RejectTelegramLoginCommandHandler(dbContext, _identityService); - var result = await handler.Handle(new RejectTelegramLoginCommand(request.Id, telegramUserId), CancellationToken.None); + var result = await handler.Handle( + new RejectTelegramLoginCommand(request.Id, telegramUserId), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal("Telegram.LoginRequestInvalid", result.Error.Code); diff --git a/backend/tests/PnvPanel.Application.Tests/Telegram/CreateLinkTokenCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Telegram/CreateLinkTokenCommandHandlerTests.cs index 7c492dc..29e17f1 100644 --- a/backend/tests/PnvPanel.Application.Tests/Telegram/CreateLinkTokenCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Telegram/CreateLinkTokenCommandHandlerTests.cs @@ -13,7 +13,10 @@ public class CreateLinkTokenCommandHandlerTests using var dbContext = InMemoryDbContextFactory.Create(); var userId = Guid.NewGuid(); - var handler = new CreateLinkTokenCommandHandler(dbContext, FakeCurrentUser.Authenticated(userId)); + var handler = new CreateLinkTokenCommandHandler( + dbContext, + FakeCurrentUser.Authenticated(userId) + ); var result = await handler.Handle(new CreateLinkTokenCommand(), CancellationToken.None); diff --git a/backend/tests/PnvPanel.Application.Tests/Telegram/GetLoginRequestStatusQueryHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Telegram/GetLoginRequestStatusQueryHandlerTests.cs index e1b6f81..339f9e1 100644 --- a/backend/tests/PnvPanel.Application.Tests/Telegram/GetLoginRequestStatusQueryHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Telegram/GetLoginRequestStatusQueryHandlerTests.cs @@ -13,10 +13,12 @@ public class GetLoginRequestStatusQueryHandlerTests { private readonly IIdentityService _identityService = Substitute.For(); private readonly IJwtTokenService _jwtTokenService = Substitute.For(); - private readonly IRefreshTokenService _refreshTokenService = Substitute.For(); + private readonly IRefreshTokenService _refreshTokenService = + Substitute.For(); - private TelegramNs.GetLoginRequestStatusQueryHandler CreateHandler(PnvPanel.Infrastructure.Persistence.AppDbContext dbContext) - => new(dbContext, _identityService, _jwtTokenService, _refreshTokenService); + private TelegramNs.GetLoginRequestStatusQueryHandler CreateHandler( + PnvPanel.Infrastructure.Persistence.AppDbContext dbContext + ) => new(dbContext, _identityService, _jwtTokenService, _refreshTokenService); [Fact] public async Task Handle_WhenRequestNotFound_ReturnsNotFound() @@ -25,7 +27,10 @@ public class GetLoginRequestStatusQueryHandlerTests var handler = CreateHandler(dbContext); - var result = await handler.Handle(new TelegramNs.GetLoginRequestStatusQuery(Guid.NewGuid()), CancellationToken.None); + var result = await handler.Handle( + new TelegramNs.GetLoginRequestStatusQuery(Guid.NewGuid()), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(TelegramNs.TelegramErrors.LoginRequestNotFound, result.Error); @@ -41,12 +46,17 @@ public class GetLoginRequestStatusQueryHandlerTests var handler = CreateHandler(dbContext); - var result = await handler.Handle(new TelegramNs.GetLoginRequestStatusQuery(request.Id), CancellationToken.None); + var result = await handler.Handle( + new TelegramNs.GetLoginRequestStatusQuery(request.Id), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal(TelegramLoginStatus.Expired, result.Value.Status); Assert.Null(result.Value.Auth); - await _refreshTokenService.DidNotReceive().IssueAsync(Arg.Any(), Arg.Any()); + await _refreshTokenService + .DidNotReceive() + .IssueAsync(Arg.Any(), Arg.Any()); } [Fact] @@ -59,7 +69,10 @@ public class GetLoginRequestStatusQueryHandlerTests var handler = CreateHandler(dbContext); - var result = await handler.Handle(new TelegramNs.GetLoginRequestStatusQuery(request.Id), CancellationToken.None); + var result = await handler.Handle( + new TelegramNs.GetLoginRequestStatusQuery(request.Id), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal(TelegramLoginStatus.Pending, result.Value.Status); @@ -76,16 +89,31 @@ public class GetLoginRequestStatusQueryHandlerTests dbContext.TelegramLoginRequests.Add(request); await dbContext.SaveChangesAsync(CancellationToken.None); - var profile = new CurrentUserProfile(userId, "alice", Guid.NewGuid(), "user", true, false, 3, RoleQuota.Unlimited, "sub-token"); + var profile = new CurrentUserProfile( + userId, + "alice", + Guid.NewGuid(), + "user", + true, + false, + 3, + RoleQuota.Unlimited, + "sub-token" + ); _identityService.GetProfileAsync(userId, Arg.Any()).Returns(profile); - _jwtTokenService.GenerateAccessToken(Arg.Any()) + _jwtTokenService + .GenerateAccessToken(Arg.Any()) .Returns(("access-token", DateTimeOffset.UtcNow.AddMinutes(15))); - _refreshTokenService.IssueAsync(userId, Arg.Any()) + _refreshTokenService + .IssueAsync(userId, Arg.Any()) .Returns(new IssuedRefreshToken("refresh-token", DateTimeOffset.UtcNow.AddDays(30))); var handler = CreateHandler(dbContext); - var result = await handler.Handle(new TelegramNs.GetLoginRequestStatusQuery(request.Id), CancellationToken.None); + var result = await handler.Handle( + new TelegramNs.GetLoginRequestStatusQuery(request.Id), + CancellationToken.None + ); Assert.True(result.IsSuccess); Assert.Equal(TelegramLoginStatus.Approved, result.Value.Status); @@ -105,11 +133,16 @@ public class GetLoginRequestStatusQueryHandlerTests dbContext.TelegramLoginRequests.Add(request); await dbContext.SaveChangesAsync(CancellationToken.None); - _identityService.GetProfileAsync(userId, Arg.Any()).Returns((CurrentUserProfile?)null); + _identityService + .GetProfileAsync(userId, Arg.Any()) + .Returns((CurrentUserProfile?)null); var handler = CreateHandler(dbContext); - var result = await handler.Handle(new TelegramNs.GetLoginRequestStatusQuery(request.Id), CancellationToken.None); + var result = await handler.Handle( + new TelegramNs.GetLoginRequestStatusQuery(request.Id), + CancellationToken.None + ); Assert.False(result.IsSuccess); Assert.Equal(AuthErrors.Unauthorized, result.Error); diff --git a/backend/tests/PnvPanel.Application.Tests/Telegram/UnlinkTelegramCommandHandlerTests.cs b/backend/tests/PnvPanel.Application.Tests/Telegram/UnlinkTelegramCommandHandlerTests.cs index 7b440f3..66b7c51 100644 --- a/backend/tests/PnvPanel.Application.Tests/Telegram/UnlinkTelegramCommandHandlerTests.cs +++ b/backend/tests/PnvPanel.Application.Tests/Telegram/UnlinkTelegramCommandHandlerTests.cs @@ -16,26 +16,38 @@ public class UnlinkTelegramCommandHandlerTests public async Task Handle_WhenAuthenticated_DelegatesToIdentityService() { var userId = Guid.NewGuid(); - _identityService.UnlinkTelegramAsync(userId, Arg.Any()).Returns(Result.Success()); + _identityService + .UnlinkTelegramAsync(userId, Arg.Any()) + .Returns(Result.Success()); - var handler = new UnlinkTelegramCommandHandler(_identityService, FakeCurrentUser.Authenticated(userId)); + var handler = new UnlinkTelegramCommandHandler( + _identityService, + FakeCurrentUser.Authenticated(userId) + ); var result = await handler.Handle(new UnlinkTelegramCommand(), CancellationToken.None); Assert.True(result.IsSuccess); - await _identityService.Received(1).UnlinkTelegramAsync(userId, Arg.Any()); + await _identityService + .Received(1) + .UnlinkTelegramAsync(userId, Arg.Any()); } [Fact] public async Task Handle_WhenNotAuthenticated_ReturnsUnauthorized() { - var handler = new UnlinkTelegramCommandHandler(_identityService, FakeCurrentUser.Anonymous()); + var handler = new UnlinkTelegramCommandHandler( + _identityService, + FakeCurrentUser.Anonymous() + ); var result = await handler.Handle(new UnlinkTelegramCommand(), CancellationToken.None); Assert.False(result.IsSuccess); Assert.Equal(AuthErrors.Unauthorized, result.Error); - await _identityService.DidNotReceive().UnlinkTelegramAsync(Arg.Any(), Arg.Any()); + await _identityService + .DidNotReceive() + .UnlinkTelegramAsync(Arg.Any(), Arg.Any()); } [Fact] @@ -43,9 +55,14 @@ public class UnlinkTelegramCommandHandlerTests { var userId = Guid.NewGuid(); var error = TelegramErrors.NotLinked; - _identityService.UnlinkTelegramAsync(userId, Arg.Any()).Returns(Result.Failure(error)); + _identityService + .UnlinkTelegramAsync(userId, Arg.Any()) + .Returns(Result.Failure(error)); - var handler = new UnlinkTelegramCommandHandler(_identityService, FakeCurrentUser.Authenticated(userId)); + var handler = new UnlinkTelegramCommandHandler( + _identityService, + FakeCurrentUser.Authenticated(userId) + ); var result = await handler.Handle(new UnlinkTelegramCommand(), CancellationToken.None); diff --git a/backend/tests/PnvPanel.Application.Tests/TestSupport/FakeCurrentUser.cs b/backend/tests/PnvPanel.Application.Tests/TestSupport/FakeCurrentUser.cs index c912143..97df04a 100644 --- a/backend/tests/PnvPanel.Application.Tests/TestSupport/FakeCurrentUser.cs +++ b/backend/tests/PnvPanel.Application.Tests/TestSupport/FakeCurrentUser.cs @@ -10,8 +10,8 @@ public sealed class FakeCurrentUser : ICurrentUser public bool IsAuthenticated => UserId is not null; - public static FakeCurrentUser Authenticated(Guid userId, string userName = "testuser") - => new() { UserId = userId, UserName = userName }; + public static FakeCurrentUser Authenticated(Guid userId, string userName = "testuser") => + new() { UserId = userId, UserName = userName }; public static FakeCurrentUser Anonymous() => new(); } diff --git a/backend/tests/PnvPanel.Domain.Tests/Apps/ClientAppTests.cs b/backend/tests/PnvPanel.Domain.Tests/Apps/ClientAppTests.cs index 6f37162..a1e104f 100644 --- a/backend/tests/PnvPanel.Domain.Tests/Apps/ClientAppTests.cs +++ b/backend/tests/PnvPanel.Domain.Tests/Apps/ClientAppTests.cs @@ -8,7 +8,14 @@ public class ClientAppTests [Fact] public void Create_SetsEnabledByDefault() { - var app = ClientApp.Create("v2rayNG", new Uri("https://play.google.com/store/apps/details?id=x"), OsPlatform.Android, "desc", null, 1); + var app = ClientApp.Create( + "v2rayNG", + new Uri("https://play.google.com/store/apps/details?id=x"), + OsPlatform.Android, + "desc", + null, + 1 + ); Assert.Equal("v2rayNG", app.Name); Assert.Equal(OsPlatform.Android, app.OperatingSystem); @@ -19,9 +26,24 @@ public class ClientAppTests [Fact] public void Update_ReplacesAllMutableFields() { - var app = ClientApp.Create("Old", new Uri("https://old.example.com"), OsPlatform.IOS, "old", "old-icon", 1); + var app = ClientApp.Create( + "Old", + new Uri("https://old.example.com"), + OsPlatform.IOS, + "old", + "old-icon", + 1 + ); - app.Update("New", new Uri("https://new.example.com"), OsPlatform.MacOS, "new", "new-icon", 2, isEnabled: false); + app.Update( + "New", + new Uri("https://new.example.com"), + OsPlatform.MacOS, + "new", + "new-icon", + 2, + isEnabled: false + ); Assert.Equal("New", app.Name); Assert.Equal(new Uri("https://new.example.com"), app.DownloadUrl); diff --git a/backend/tests/PnvPanel.Domain.Tests/Audit/AuditLogTests.cs b/backend/tests/PnvPanel.Domain.Tests/Audit/AuditLogTests.cs index 4c48541..c011445 100644 --- a/backend/tests/PnvPanel.Domain.Tests/Audit/AuditLogTests.cs +++ b/backend/tests/PnvPanel.Domain.Tests/Audit/AuditLogTests.cs @@ -10,7 +10,14 @@ public class AuditLogTests { var actorId = Guid.NewGuid(); - var log = AuditLog.Create(actorId, "user.blocked", "AppUser", actorId.ToString(), "{\"reason\":\"abuse\"}", AuditSource.Web); + var log = AuditLog.Create( + actorId, + "user.blocked", + "AppUser", + actorId.ToString(), + "{\"reason\":\"abuse\"}", + AuditSource.Web + ); Assert.Equal(actorId, log.ActorId); Assert.Equal("user.blocked", log.Action); @@ -24,7 +31,14 @@ public class AuditLogTests [Fact] public void Create_AllowsNullActorForSystemActions() { - var log = AuditLog.Create(null, "node.healthcheck", "Node", Guid.NewGuid().ToString(), null, AuditSource.System); + var log = AuditLog.Create( + null, + "node.healthcheck", + "Node", + Guid.NewGuid().ToString(), + null, + AuditSource.System + ); Assert.Null(log.ActorId); Assert.Equal(AuditSource.System, log.Source); diff --git a/backend/tests/PnvPanel.Domain.Tests/Nodes/NodeTests.cs b/backend/tests/PnvPanel.Domain.Tests/Nodes/NodeTests.cs index b44a147..d7a2eab 100644 --- a/backend/tests/PnvPanel.Domain.Tests/Nodes/NodeTests.cs +++ b/backend/tests/PnvPanel.Domain.Tests/Nodes/NodeTests.cs @@ -11,7 +11,12 @@ public class NodeTests [Fact] public void Register_WithAbsoluteUri_CreatesEnabledUnknownStatusNode() { - var node = Node.Register("Germany-1", new Uri("https://de1.example.com:2053"), Credentials, "Germany"); + var node = Node.Register( + "Germany-1", + new Uri("https://de1.example.com:2053"), + Credentials, + "Germany" + ); Assert.Equal("Germany-1", node.Name); Assert.Equal("Germany", node.Location); @@ -25,13 +30,20 @@ public class NodeTests { var relativeUri = new Uri("de1.example.com", UriKind.Relative); - Assert.Throws(() => Node.Register("Germany-1", relativeUri, Credentials, null)); + Assert.Throws(() => + Node.Register("Germany-1", relativeUri, Credentials, null) + ); } [Fact] public void UpdateDetails_ChangesNameAndLocation() { - var node = Node.Register("Old", new Uri("https://example.com"), Credentials, "Old location"); + var node = Node.Register( + "Old", + new Uri("https://example.com"), + Credentials, + "Old location" + ); node.UpdateDetails("New", "New location"); diff --git a/backend/tests/PnvPanel.Domain.Tests/PnvPanel.Domain.Tests.csproj b/backend/tests/PnvPanel.Domain.Tests/PnvPanel.Domain.Tests.csproj index ba43054..d24fcf1 100644 --- a/backend/tests/PnvPanel.Domain.Tests/PnvPanel.Domain.Tests.csproj +++ b/backend/tests/PnvPanel.Domain.Tests/PnvPanel.Domain.Tests.csproj @@ -1,5 +1,4 @@ - net10.0 enable @@ -23,5 +22,4 @@ - diff --git a/backend/tests/PnvPanel.IntegrationTests/Activation/ActivationFlowTests.cs b/backend/tests/PnvPanel.IntegrationTests/Activation/ActivationFlowTests.cs index 79cda65..31aefb0 100644 --- a/backend/tests/PnvPanel.IntegrationTests/Activation/ActivationFlowTests.cs +++ b/backend/tests/PnvPanel.IntegrationTests/Activation/ActivationFlowTests.cs @@ -8,7 +8,11 @@ namespace PnvPanel.IntegrationTests.Activation; [Collection(IntegrationTestCollection.Name)] public class ActivationFlowTests(PnvPanelWebApplicationFactory factory) { - private sealed record ActivationRequestResponse(Guid Id, string? Comment, DateTimeOffset CreatedAt); + private sealed record ActivationRequestResponse( + Guid Id, + string? Comment, + DateTimeOffset CreatedAt + ); [Fact] public async Task RequestThenAdminApprove_ActivatesUser() @@ -18,7 +22,10 @@ public class ActivationFlowTests(PnvPanelWebApplicationFactory factory) var (userId, userToken) = await RegisterAndLoginAsync(userClient, userName, "P@ssw0rd123"); userClient.UseBearerToken(userToken); - var requestResponse = await userClient.PostJsonAsync("/api/activation/request", new { comment = "Please activate me" }); + var requestResponse = await userClient.PostJsonAsync( + "/api/activation/request", + new { comment = "Please activate me" } + ); Assert.Equal(HttpStatusCode.OK, requestResponse.StatusCode); var request = await requestResponse.ReadAsAsync(); Assert.NotNull(request); @@ -27,7 +34,10 @@ public class ActivationFlowTests(PnvPanelWebApplicationFactory factory) var adminToken = await LoginAsAdminAsync(adminClient); adminClient.UseBearerToken(adminToken); - var approveResponse = await adminClient.PostAsync($"/api/admin/activation-requests/{request!.Id}/approve", content: null); + var approveResponse = await adminClient.PostAsync( + $"/api/admin/activation-requests/{request!.Id}/approve", + content: null + ); Assert.Equal(HttpStatusCode.NoContent, approveResponse.StatusCode); var meResponse = await userClient.GetAsync("/api/auth/me"); @@ -44,14 +54,20 @@ public class ActivationFlowTests(PnvPanelWebApplicationFactory factory) var (_, userToken) = await RegisterAndLoginAsync(userClient, userName, "P@ssw0rd123"); userClient.UseBearerToken(userToken); - var requestResponse = await userClient.PostJsonAsync("/api/activation/request", new { comment = (string?)null }); + var requestResponse = await userClient.PostJsonAsync( + "/api/activation/request", + new { comment = (string?)null } + ); var request = await requestResponse.ReadAsAsync(); using var adminClient = factory.CreateClient(); var adminToken = await LoginAsAdminAsync(adminClient); adminClient.UseBearerToken(adminToken); - var rejectResponse = await adminClient.PostJsonAsync($"/api/admin/activation-requests/{request!.Id}/reject", new { reason = "not enough info" }); + var rejectResponse = await adminClient.PostJsonAsync( + $"/api/admin/activation-requests/{request!.Id}/reject", + new { reason = "not enough info" } + ); Assert.Equal(HttpStatusCode.NoContent, rejectResponse.StatusCode); var meResponse = await userClient.GetAsync("/api/auth/me"); @@ -67,10 +83,16 @@ public class ActivationFlowTests(PnvPanelWebApplicationFactory factory) var (_, userToken) = await RegisterAndLoginAsync(userClient, userName, "P@ssw0rd123"); userClient.UseBearerToken(userToken); - var first = await userClient.PostJsonAsync("/api/activation/request", new { comment = (string?)null }); + var first = await userClient.PostJsonAsync( + "/api/activation/request", + new { comment = (string?)null } + ); Assert.Equal(HttpStatusCode.OK, first.StatusCode); - var second = await userClient.PostJsonAsync("/api/activation/request", new { comment = (string?)null }); + var second = await userClient.PostJsonAsync( + "/api/activation/request", + new { comment = (string?)null } + ); Assert.Equal(HttpStatusCode.Conflict, second.StatusCode); } } diff --git a/backend/tests/PnvPanel.IntegrationTests/Admin/NodeInboundCrudTests.cs b/backend/tests/PnvPanel.IntegrationTests/Admin/NodeInboundCrudTests.cs index 597c17f..0b03250 100644 --- a/backend/tests/PnvPanel.IntegrationTests/Admin/NodeInboundCrudTests.cs +++ b/backend/tests/PnvPanel.IntegrationTests/Admin/NodeInboundCrudTests.cs @@ -8,11 +8,28 @@ namespace PnvPanel.IntegrationTests.Admin; [Collection(IntegrationTestCollection.Name)] public class NodeInboundCrudTests(PnvPanelWebApplicationFactory factory) { - private sealed record NodeResponse(Guid Id, string Name, string BaseAddress, string Username, string? Location, string Status, bool IsEnabled); + private sealed record NodeResponse( + Guid Id, + string Name, + string BaseAddress, + string Username, + string? Location, + string Status, + bool IsEnabled + ); private sealed record InboundResponse( - Guid Id, Guid NodeId, string RemoteInboundId, string Protocol, string Remark, int Port, - bool IsPublished, string? DisplayName, int? MaxClients, IReadOnlyList AllowedRoleIds); + Guid Id, + Guid NodeId, + string RemoteInboundId, + string Protocol, + string Remark, + int Port, + bool IsPublished, + string? DisplayName, + int? MaxClients, + IReadOnlyList AllowedRoleIds + ); private sealed record SyncNodeResponse(int InboundsSynced, string Status); @@ -23,14 +40,17 @@ public class NodeInboundCrudTests(PnvPanelWebApplicationFactory factory) var adminToken = await LoginAsAdminAsync(adminClient); adminClient.UseBearerToken(adminToken); - var registerResponse = await adminClient.PostJsonAsync("/api/admin/nodes", new - { - name = $"Node-{Guid.NewGuid():N}"[..20], - baseAddress = "https://node.example.com:2053", - username = "admin", - password = "node-panel-password", - location = "Germany", - }); + var registerResponse = await adminClient.PostJsonAsync( + "/api/admin/nodes", + new + { + name = $"Node-{Guid.NewGuid():N}"[..20], + baseAddress = "https://node.example.com:2053", + username = "admin", + password = "node-panel-password", + location = "Germany", + } + ); Assert.Equal(HttpStatusCode.OK, registerResponse.StatusCode); var node = await registerResponse.ReadAsAsync(); Assert.NotNull(node); @@ -40,24 +60,32 @@ public class NodeInboundCrudTests(PnvPanelWebApplicationFactory factory) var nodes = await listNodesResponse.ReadAsAsync>(); Assert.Contains(nodes!, n => n.Id == node!.Id); - var syncResponse = await adminClient.PostAsync($"/api/admin/nodes/{node!.Id}/sync", content: null); + var syncResponse = await adminClient.PostAsync( + $"/api/admin/nodes/{node!.Id}/sync", + content: null + ); Assert.Equal(HttpStatusCode.OK, syncResponse.StatusCode); var sync = await syncResponse.ReadAsAsync(); Assert.Equal(1, sync!.InboundsSynced); - var listInboundsResponse = await adminClient.GetAsync($"/api/admin/inbounds?nodeId={node.Id}"); + var listInboundsResponse = await adminClient.GetAsync( + $"/api/admin/inbounds?nodeId={node.Id}" + ); Assert.Equal(HttpStatusCode.OK, listInboundsResponse.StatusCode); var inbounds = await listInboundsResponse.ReadAsAsync>(); var inbound = Assert.Single(inbounds!); Assert.False(inbound.IsPublished); - var publishResponse = await adminClient.SendPutJsonAsync($"/api/admin/inbounds/{inbound.Id}/publish", new - { - isPublished = true, - displayName = "Germany (VLESS)", - allowedRoleIds = Array.Empty(), - maxClients = (int?)null, - }); + var publishResponse = await adminClient.SendPutJsonAsync( + $"/api/admin/inbounds/{inbound.Id}/publish", + new + { + isPublished = true, + displayName = "Germany (VLESS)", + allowedRoleIds = Array.Empty(), + maxClients = (int?)null, + } + ); Assert.Equal(HttpStatusCode.OK, publishResponse.StatusCode); var published = await publishResponse.ReadAsAsync(); Assert.True(published!.IsPublished); @@ -72,10 +100,17 @@ public class NodeInboundCrudTests(PnvPanelWebApplicationFactory factory) var (_, userToken) = await RegisterAndLoginAsync(userClient, userName, "P@ssw0rd123"); userClient.UseBearerToken(userToken); - var response = await userClient.PostJsonAsync("/api/admin/nodes", new - { - name = "Node", baseAddress = "https://node.example.com", username = "admin", password = "pw", location = (string?)null, - }); + var response = await userClient.PostJsonAsync( + "/api/admin/nodes", + new + { + name = "Node", + baseAddress = "https://node.example.com", + username = "admin", + password = "pw", + location = (string?)null, + } + ); Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); } diff --git a/backend/tests/PnvPanel.IntegrationTests/Auth/AuthFlowTests.cs b/backend/tests/PnvPanel.IntegrationTests/Auth/AuthFlowTests.cs index 640dfe7..975b3ff 100644 --- a/backend/tests/PnvPanel.IntegrationTests/Auth/AuthFlowTests.cs +++ b/backend/tests/PnvPanel.IntegrationTests/Auth/AuthFlowTests.cs @@ -10,9 +10,19 @@ public class AuthFlowTests(PnvPanelWebApplicationFactory factory) { private sealed record RegisterResponse(Guid Id, string UserName); - private sealed record CurrentUserResponse(Guid Id, string UserName, string Role, bool IsActivated, bool TelegramLinked); + private sealed record CurrentUserResponse( + Guid Id, + string UserName, + string Role, + bool IsActivated, + bool TelegramLinked + ); - private sealed record LoginResponse(string AccessToken, DateTimeOffset ExpiresAt, CurrentUserResponse User); + private sealed record LoginResponse( + string AccessToken, + DateTimeOffset ExpiresAt, + CurrentUserResponse User + ); [Fact] public async Task RegisterLoginMeRefreshLogout_FullFlow_Succeeds() @@ -21,13 +31,19 @@ public class AuthFlowTests(PnvPanelWebApplicationFactory factory) var userName = $"alice_{Guid.NewGuid():N}"[..20]; const string password = "P@ssw0rd123"; - var registerResponse = await client.PostJsonAsync("/api/auth/register", new { userName, password }); + var registerResponse = await client.PostJsonAsync( + "/api/auth/register", + new { userName, password } + ); Assert.Equal(HttpStatusCode.OK, registerResponse.StatusCode); var registered = await registerResponse.ReadAsAsync(); Assert.NotNull(registered); Assert.Equal(userName, registered!.UserName); - var loginResponse = await client.PostJsonAsync("/api/auth/login", new { userName, password }); + var loginResponse = await client.PostJsonAsync( + "/api/auth/login", + new { userName, password } + ); Assert.Equal(HttpStatusCode.OK, loginResponse.StatusCode); var login = await loginResponse.ReadAsAsync(); Assert.NotNull(login); @@ -61,9 +77,15 @@ public class AuthFlowTests(PnvPanelWebApplicationFactory factory) using var client = factory.CreateClient(); var userName = $"bob_{Guid.NewGuid():N}"[..20]; - await client.PostJsonAsync("/api/auth/register", new { userName, password = "CorrectPassword123" }); + await client.PostJsonAsync( + "/api/auth/register", + new { userName, password = "CorrectPassword123" } + ); - var loginResponse = await client.PostJsonAsync("/api/auth/login", new { userName, password = "WrongPassword123" }); + var loginResponse = await client.PostJsonAsync( + "/api/auth/login", + new { userName, password = "WrongPassword123" } + ); Assert.Equal(HttpStatusCode.Unauthorized, loginResponse.StatusCode); } @@ -74,10 +96,16 @@ public class AuthFlowTests(PnvPanelWebApplicationFactory factory) using var client = factory.CreateClient(); var userName = $"carol_{Guid.NewGuid():N}"[..20]; - var first = await client.PostJsonAsync("/api/auth/register", new { userName, password = "P@ssw0rd123" }); + var first = await client.PostJsonAsync( + "/api/auth/register", + new { userName, password = "P@ssw0rd123" } + ); Assert.Equal(HttpStatusCode.OK, first.StatusCode); - var second = await client.PostJsonAsync("/api/auth/register", new { userName, password = "AnotherPass123!" }); + var second = await client.PostJsonAsync( + "/api/auth/register", + new { userName, password = "AnotherPass123!" } + ); Assert.Equal(HttpStatusCode.Conflict, second.StatusCode); } diff --git a/backend/tests/PnvPanel.IntegrationTests/Configs/ConfigQuotaTests.cs b/backend/tests/PnvPanel.IntegrationTests/Configs/ConfigQuotaTests.cs index ccfbe85..25a6397 100644 --- a/backend/tests/PnvPanel.IntegrationTests/Configs/ConfigQuotaTests.cs +++ b/backend/tests/PnvPanel.IntegrationTests/Configs/ConfigQuotaTests.cs @@ -17,9 +17,21 @@ public class ConfigQuotaTests(PnvPanelWebApplicationFactory factory) private sealed record SyncNodeResponse(int InboundsSynced, string Status); - private sealed record InboundResponse(Guid Id, Guid NodeId, string RemoteInboundId, string Protocol, string Remark, int Port, bool IsPublished); + private sealed record InboundResponse( + Guid Id, + Guid NodeId, + string RemoteInboundId, + string Protocol, + string Remark, + int Port, + bool IsPublished + ); - private sealed record ActivationRequestResponse(Guid Id, string? Comment, DateTimeOffset CreatedAt); + private sealed record ActivationRequestResponse( + Guid Id, + string? Comment, + DateTimeOffset CreatedAt + ); private sealed record MyConfigsResponse(List Configs, int MaxConfigs); @@ -41,33 +53,44 @@ public class ConfigQuotaTests(PnvPanelWebApplicationFactory factory) var userRole = roles!.Single(r => r.Name == "user"); var updateRoleResponse = await adminClient.SendPutJsonAsync( - $"/api/admin/roles/{userRole.Id}", new { maxConfigs = Quota, maxIpLimit = -1 }); + $"/api/admin/roles/{userRole.Id}", + new { maxConfigs = Quota, maxIpLimit = -1 } + ); Assert.Equal(HttpStatusCode.OK, updateRoleResponse.StatusCode); - var registerNodeResponse = await adminClient.PostJsonAsync("/api/admin/nodes", new - { - name = $"QuotaNode-{Guid.NewGuid():N}"[..24], - baseAddress = "https://quota-node.example.com", - username = "admin", - password = "node-panel-password", - location = (string?)null, - }); + var registerNodeResponse = await adminClient.PostJsonAsync( + "/api/admin/nodes", + new + { + name = $"QuotaNode-{Guid.NewGuid():N}"[..24], + baseAddress = "https://quota-node.example.com", + username = "admin", + password = "node-panel-password", + location = (string?)null, + } + ); var node = await registerNodeResponse.ReadAsAsync(); - var syncResponse = await adminClient.PostAsync($"/api/admin/nodes/{node!.Id}/sync", content: null); + var syncResponse = await adminClient.PostAsync( + $"/api/admin/nodes/{node!.Id}/sync", + content: null + ); var sync = await syncResponse.ReadAsAsync(); Assert.Equal(1, sync!.InboundsSynced); var inboundsResponse = await adminClient.GetAsync($"/api/admin/inbounds?nodeId={node.Id}"); var inbound = (await inboundsResponse.ReadAsAsync>())!.Single(); - var publishResponse = await adminClient.SendPutJsonAsync($"/api/admin/inbounds/{inbound.Id}/publish", new - { - isPublished = true, - displayName = "Quota inbound", - allowedRoleIds = new[] { userRole.Id }, - maxClients = (int?)null, - }); + var publishResponse = await adminClient.SendPutJsonAsync( + $"/api/admin/inbounds/{inbound.Id}/publish", + new + { + isPublished = true, + displayName = "Quota inbound", + allowedRoleIds = new[] { userRole.Id }, + maxClients = (int?)null, + } + ); Assert.Equal(HttpStatusCode.OK, publishResponse.StatusCode); using var userClient = factory.CreateClient(); @@ -75,21 +98,29 @@ public class ConfigQuotaTests(PnvPanelWebApplicationFactory factory) var (_, userToken) = await RegisterAndLoginAsync(userClient, userName, "P@ssw0rd123"); userClient.UseBearerToken(userToken); - var activationRequestResponse = await userClient.PostJsonAsync("/api/activation/request", new { comment = (string?)null }); - var activationRequest = await activationRequestResponse.ReadAsAsync(); - var approveResponse = await adminClient.PostAsync($"/api/admin/activation-requests/{activationRequest!.Id}/approve", content: null); + var activationRequestResponse = await userClient.PostJsonAsync( + "/api/activation/request", + new { comment = (string?)null } + ); + var activationRequest = + await activationRequestResponse.ReadAsAsync(); + var approveResponse = await adminClient.PostAsync( + $"/api/admin/activation-requests/{activationRequest!.Id}/approve", + content: null + ); Assert.Equal(HttpStatusCode.NoContent, approveResponse.StatusCode); - var tasks = Enumerable.Range(0, ConcurrentAttempts).Select(async i => - { - using var attemptClient = factory.CreateClient(); - attemptClient.UseBearerToken(userToken); - return await attemptClient.PostJsonAsync("/api/configs", new + var tasks = Enumerable + .Range(0, ConcurrentAttempts) + .Select(async i => { - inboundId = inbound.Id, - label = $"device-{i}", + using var attemptClient = factory.CreateClient(); + attemptClient.UseBearerToken(userToken); + return await attemptClient.PostJsonAsync( + "/api/configs", + new { inboundId = inbound.Id, label = $"device-{i}" } + ); }); - }); var responses = await Task.WhenAll(tasks); diff --git a/backend/tests/PnvPanel.IntegrationTests/PnvPanel.IntegrationTests.csproj b/backend/tests/PnvPanel.IntegrationTests/PnvPanel.IntegrationTests.csproj index fd4edd9..94e66b6 100644 --- a/backend/tests/PnvPanel.IntegrationTests/PnvPanel.IntegrationTests.csproj +++ b/backend/tests/PnvPanel.IntegrationTests/PnvPanel.IntegrationTests.csproj @@ -1,5 +1,4 @@ - net10.0 enable @@ -24,5 +23,4 @@ - diff --git a/backend/tests/PnvPanel.IntegrationTests/TestSupport/AuthTestHelper.cs b/backend/tests/PnvPanel.IntegrationTests/TestSupport/AuthTestHelper.cs index 41a1822..ec3426c 100644 --- a/backend/tests/PnvPanel.IntegrationTests/TestSupport/AuthTestHelper.cs +++ b/backend/tests/PnvPanel.IntegrationTests/TestSupport/AuthTestHelper.cs @@ -4,22 +4,46 @@ namespace PnvPanel.IntegrationTests.TestSupport; public static class AuthTestHelper { - public sealed record CurrentUserResponse(Guid Id, string UserName, string Role, bool IsActivated, bool TelegramLinked); + public sealed record CurrentUserResponse( + Guid Id, + string UserName, + string Role, + bool IsActivated, + bool TelegramLinked + ); - public sealed record LoginResponse(string AccessToken, DateTimeOffset ExpiresAt, CurrentUserResponse User); + public sealed record LoginResponse( + string AccessToken, + DateTimeOffset ExpiresAt, + CurrentUserResponse User + ); - public static async Task<(Guid Id, string AccessToken)> RegisterAndLoginAsync(HttpClient client, string userName, string password) + public static async Task<(Guid Id, string AccessToken)> RegisterAndLoginAsync( + HttpClient client, + string userName, + string password + ) { - var registerResponse = await client.PostJsonAsync("/api/auth/register", new { userName, password }); + var registerResponse = await client.PostJsonAsync( + "/api/auth/register", + new { userName, password } + ); registerResponse.EnsureSuccessStatusCode(); var (id, accessToken) = await LoginAsync(client, userName, password); return (id, accessToken); } - public static async Task<(Guid Id, string AccessToken)> LoginAsync(HttpClient client, string userName, string password) + public static async Task<(Guid Id, string AccessToken)> LoginAsync( + HttpClient client, + string userName, + string password + ) { - var loginResponse = await client.PostJsonAsync("/api/auth/login", new { userName, password }); + var loginResponse = await client.PostJsonAsync( + "/api/auth/login", + new { userName, password } + ); loginResponse.EnsureSuccessStatusCode(); var login = await loginResponse.ReadAsAsync(); return (login!.User.Id, login.AccessToken); @@ -27,7 +51,11 @@ public static class AuthTestHelper public static async Task LoginAsAdminAsync(HttpClient client) { - var (_, accessToken) = await LoginAsync(client, PnvPanelWebApplicationFactory.AdminUserName, PnvPanelWebApplicationFactory.AdminPassword); + var (_, accessToken) = await LoginAsync( + client, + PnvPanelWebApplicationFactory.AdminUserName, + PnvPanelWebApplicationFactory.AdminPassword + ); return accessToken; } } diff --git a/backend/tests/PnvPanel.IntegrationTests/TestSupport/FakeXuiPanelGateway.cs b/backend/tests/PnvPanel.IntegrationTests/TestSupport/FakeXuiPanelGateway.cs index b9ac544..d02a605 100644 --- a/backend/tests/PnvPanel.IntegrationTests/TestSupport/FakeXuiPanelGateway.cs +++ b/backend/tests/PnvPanel.IntegrationTests/TestSupport/FakeXuiPanelGateway.cs @@ -13,10 +13,13 @@ public sealed class FakeXuiPanelGateway : IXuiPanelGateway { public Result ValidateBaseAddress(Uri baseAddress) => Result.Success(); - public Task ProbeAsync(Node node, CancellationToken cancellationToken) - => Task.FromResult(new NodeProbeResult(true, null)); + public Task ProbeAsync(Node node, CancellationToken cancellationToken) => + Task.FromResult(new NodeProbeResult(true, null)); - public Task>> ListInboundsAsync(Node node, CancellationToken cancellationToken) + public Task>> ListInboundsAsync( + Node node, + CancellationToken cancellationToken + ) { IReadOnlyList inbounds = [ @@ -26,32 +29,53 @@ public sealed class FakeXuiPanelGateway : IXuiPanelGateway return Task.FromResult(Result.Success(inbounds)); } - public void InvalidateClient(Guid nodeId) - { - } + public void InvalidateClient(Guid nodeId) { } public Task> AddClientAsync( - Node node, string inboundRemoteId, VpnProtocol protocol, string clientEmail, string clientName, int limitIp, - CancellationToken cancellationToken) - => Task.FromResult(Result.Success(Guid.NewGuid().ToString())); + Node node, + string inboundRemoteId, + VpnProtocol protocol, + string clientEmail, + string clientName, + int limitIp, + CancellationToken cancellationToken + ) => Task.FromResult(Result.Success(Guid.NewGuid().ToString())); public Task RemoveClientAsync( - Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol, CancellationToken cancellationToken) - => Task.FromResult(Result.Success()); + Node node, + string inboundRemoteId, + string clientExternalId, + VpnProtocol protocol, + CancellationToken cancellationToken + ) => Task.FromResult(Result.Success()); public Task UpdateClientAsync( - Node node, string inboundRemoteId, string clientExternalId, VpnProtocol protocol, - string name, bool enable, CancellationToken cancellationToken) - => Task.FromResult(Result.Success()); + Node node, + string inboundRemoteId, + string clientExternalId, + VpnProtocol protocol, + string name, + bool enable, + CancellationToken cancellationToken + ) => Task.FromResult(Result.Success()); public Task> BuildConnectionStringAsync( - Node node, Inbound inbound, string clientExternalId, string clientName, string publicHost, CancellationToken cancellationToken) - => Task.FromResult(Result.Success("vless://fake-connection-string")); + Node node, + Inbound inbound, + string clientExternalId, + string clientName, + string publicHost, + CancellationToken cancellationToken + ) => Task.FromResult(Result.Success("vless://fake-connection-string")); public Task>> GetClientTrafficAsync( - Node node, string inboundRemoteId, CancellationToken cancellationToken) + Node node, + string inboundRemoteId, + CancellationToken cancellationToken + ) { - IReadOnlyDictionary traffic = new Dictionary(); + IReadOnlyDictionary traffic = + new Dictionary(); return Task.FromResult(Result.Success(traffic)); } } diff --git a/backend/tests/PnvPanel.IntegrationTests/TestSupport/HttpClientJsonExtensions.cs b/backend/tests/PnvPanel.IntegrationTests/TestSupport/HttpClientJsonExtensions.cs index f46c919..b7817bf 100644 --- a/backend/tests/PnvPanel.IntegrationTests/TestSupport/HttpClientJsonExtensions.cs +++ b/backend/tests/PnvPanel.IntegrationTests/TestSupport/HttpClientJsonExtensions.cs @@ -8,15 +8,24 @@ public static class HttpClientJsonExtensions { public static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); - public static void UseBearerToken(this HttpClient client, string accessToken) - => client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); + public static void UseBearerToken(this HttpClient client, string accessToken) => + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue( + "Bearer", + accessToken + ); - public static async Task ReadAsAsync(this HttpResponseMessage response) - => await response.Content.ReadFromJsonAsync(JsonOptions); + public static async Task ReadAsAsync(this HttpResponseMessage response) => + await response.Content.ReadFromJsonAsync(JsonOptions); - public static Task PostJsonAsync(this HttpClient client, string url, object body) - => client.PostAsJsonAsync(url, body, JsonOptions); + public static Task PostJsonAsync( + this HttpClient client, + string url, + object body + ) => client.PostAsJsonAsync(url, body, JsonOptions); - public static Task SendPutJsonAsync(this HttpClient client, string url, object body) - => client.PutAsJsonAsync(url, body, JsonOptions); + public static Task SendPutJsonAsync( + this HttpClient client, + string url, + object body + ) => client.PutAsJsonAsync(url, body, JsonOptions); } diff --git a/backend/tests/PnvPanel.IntegrationTests/TestSupport/PnvPanelWebApplicationFactory.cs b/backend/tests/PnvPanel.IntegrationTests/TestSupport/PnvPanelWebApplicationFactory.cs index 77d886e..13185f5 100644 --- a/backend/tests/PnvPanel.IntegrationTests/TestSupport/PnvPanelWebApplicationFactory.cs +++ b/backend/tests/PnvPanel.IntegrationTests/TestSupport/PnvPanelWebApplicationFactory.cs @@ -42,20 +42,24 @@ public sealed class PnvPanelWebApplicationFactory : WebApplicationFactory - { - config.AddInMemoryCollection(new Dictionary + builder.ConfigureAppConfiguration( + (_, config) => { - ["ConnectionStrings:Default"] = _postgres.GetConnectionString(), - ["AdminSeed:Username"] = AdminUserName, - ["AdminSeed:Password"] = AdminPassword, - // Пусто — TelegramBotHostedService при пустом токене не стартует (см. Api/Telegram/TelegramBotHostedService.cs). - ["Telegram:BotToken"] = "", - // Весь collection делит один TestServer/host — все запросы идут от одного "клиента", - // дефолтный лимит 20/мин быстро исчерпывается. Поднимаем для тестового окружения. - ["RateLimiting:AuthPermitLimit"] = "10000", - }); - }); + config.AddInMemoryCollection( + new Dictionary + { + ["ConnectionStrings:Default"] = _postgres.GetConnectionString(), + ["AdminSeed:Username"] = AdminUserName, + ["AdminSeed:Password"] = AdminPassword, + // Пусто — TelegramBotHostedService при пустом токене не стартует (см. Api/Telegram/TelegramBotHostedService.cs). + ["Telegram:BotToken"] = "", + // Весь collection делит один TestServer/host — все запросы идут от одного "клиента", + // дефолтный лимит 20/мин быстро исчерпывается. Поднимаем для тестового окружения. + ["RateLimiting:AuthPermitLimit"] = "10000", + } + ); + } + ); builder.ConfigureServices(services => {