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