Enhance API endpoints with response type annotations
- Updated various API endpoints to include response type annotations using .Produces<T>() for better documentation and type safety. - Enhanced activation, admin, user, config, and other endpoints to specify response types, improving clarity for frontend integration. - Added new DTOs for structured responses in authentication and Telegram-related endpoints. - Improved overall API schema generation to reflect these changes, ensuring consistency between backend and frontend types.
This commit is contained in:
@@ -2,6 +2,7 @@ using PnvPanel.Api.Common;
|
|||||||
using PnvPanel.Application.Activation;
|
using PnvPanel.Application.Activation;
|
||||||
using PnvPanel.Application.Admin.Activation;
|
using PnvPanel.Application.Admin.Activation;
|
||||||
using PnvPanel.Application.Common.Messaging;
|
using PnvPanel.Application.Common.Messaging;
|
||||||
|
using PnvPanel.Application.Common.Models;
|
||||||
using PnvPanel.Domain.Activation;
|
using PnvPanel.Domain.Activation;
|
||||||
using PnvPanel.Infrastructure.Identity;
|
using PnvPanel.Infrastructure.Identity;
|
||||||
|
|
||||||
@@ -12,16 +13,16 @@ public static class ActivationEndpoints
|
|||||||
public static IEndpointRouteBuilder MapActivationEndpoints(this IEndpointRouteBuilder app)
|
public static IEndpointRouteBuilder MapActivationEndpoints(this IEndpointRouteBuilder app)
|
||||||
{
|
{
|
||||||
var user = app.MapGroup("/api/activation").WithTags("Activation").RequireAuthorization();
|
var user = app.MapGroup("/api/activation").WithTags("Activation").RequireAuthorization();
|
||||||
user.MapGet("/status", GetStatus);
|
user.MapGet("/status", GetStatus).Produces<ActivationStatusDto>();
|
||||||
user.MapPost("/request", RequestActivation);
|
user.MapPost("/request", RequestActivation).Produces<ActivationRequestDto>();
|
||||||
|
|
||||||
var admin = app.MapGroup("/api/admin/activation-requests")
|
var admin = app.MapGroup("/api/admin/activation-requests")
|
||||||
.WithTags("Admin.Activation")
|
.WithTags("Admin.Activation")
|
||||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||||
|
|
||||||
admin.MapGet("", ListRequests);
|
admin.MapGet("", ListRequests).Produces<PagedList<ActivationRequestAdminDto>>();
|
||||||
admin.MapPost("/{id:guid}/approve", Approve);
|
admin.MapPost("/{id:guid}/approve", Approve).Produces(StatusCodes.Status204NoContent);
|
||||||
admin.MapPost("/{id:guid}/reject", Reject);
|
admin.MapPost("/{id:guid}/reject", Reject).Produces(StatusCodes.Status204NoContent);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ public static class AdminAppEndpoints
|
|||||||
.WithTags("Admin.Apps")
|
.WithTags("Admin.Apps")
|
||||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||||
|
|
||||||
admin.MapGet("", ListApps);
|
admin.MapGet("", ListApps).Produces<IReadOnlyList<AdminAppDto>>();
|
||||||
admin.MapPost("", CreateApp);
|
admin.MapPost("", CreateApp).Produces<AdminAppDto>();
|
||||||
admin.MapPut("/{id:guid}", UpdateApp);
|
admin.MapPut("/{id:guid}", UpdateApp).Produces<AdminAppDto>();
|
||||||
admin.MapDelete("/{id:guid}", DeleteApp);
|
admin.MapDelete("/{id:guid}", DeleteApp).Produces(StatusCodes.Status204NoContent);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using PnvPanel.Api.Common;
|
|||||||
using PnvPanel.Application.Admin.Audit;
|
using PnvPanel.Application.Admin.Audit;
|
||||||
using PnvPanel.Application.Admin.Stats;
|
using PnvPanel.Application.Admin.Stats;
|
||||||
using PnvPanel.Application.Common.Messaging;
|
using PnvPanel.Application.Common.Messaging;
|
||||||
|
using PnvPanel.Application.Common.Models;
|
||||||
using PnvPanel.Infrastructure.Identity;
|
using PnvPanel.Infrastructure.Identity;
|
||||||
|
|
||||||
namespace PnvPanel.Api.Endpoints;
|
namespace PnvPanel.Api.Endpoints;
|
||||||
@@ -14,8 +15,8 @@ public static class AdminStatsEndpoints
|
|||||||
.WithTags("Admin.Stats")
|
.WithTags("Admin.Stats")
|
||||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||||
|
|
||||||
admin.MapGet("/stats", GetStats);
|
admin.MapGet("/stats", GetStats).Produces<StatsDto>();
|
||||||
admin.MapGet("/audit", GetAudit);
|
admin.MapGet("/audit", GetAudit).Produces<PagedList<AuditLogDto>>();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using PnvPanel.Api.Common;
|
using PnvPanel.Api.Common;
|
||||||
using PnvPanel.Application.Admin.Users;
|
using PnvPanel.Application.Admin.Users;
|
||||||
|
using PnvPanel.Application.Common.Interfaces;
|
||||||
using PnvPanel.Application.Common.Messaging;
|
using PnvPanel.Application.Common.Messaging;
|
||||||
|
using PnvPanel.Application.Common.Models;
|
||||||
|
using PnvPanel.Application.Configs;
|
||||||
using PnvPanel.Infrastructure.Identity;
|
using PnvPanel.Infrastructure.Identity;
|
||||||
|
|
||||||
namespace PnvPanel.Api.Endpoints;
|
namespace PnvPanel.Api.Endpoints;
|
||||||
@@ -13,12 +16,12 @@ public static class AdminUserEndpoints
|
|||||||
.WithTags("Admin.Users")
|
.WithTags("Admin.Users")
|
||||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||||
|
|
||||||
admin.MapGet("/users", ListUsers);
|
admin.MapGet("/users", ListUsers).Produces<PagedList<UserSummaryDto>>();
|
||||||
admin.MapPatch("/users/{id:guid}/block", BlockUser);
|
admin.MapPatch("/users/{id:guid}/block", BlockUser).Produces(StatusCodes.Status204NoContent);
|
||||||
admin.MapPatch("/users/{id:guid}/unblock", UnblockUser);
|
admin.MapPatch("/users/{id:guid}/unblock", UnblockUser).Produces(StatusCodes.Status204NoContent);
|
||||||
admin.MapPost("/users/{id:guid}/reset-password", ResetPassword);
|
admin.MapPost("/users/{id:guid}/reset-password", ResetPassword).Produces(StatusCodes.Status204NoContent);
|
||||||
admin.MapGet("/users/{id:guid}/configs", GetUserConfigs);
|
admin.MapGet("/users/{id:guid}/configs", GetUserConfigs).Produces<IReadOnlyList<VpnConfigDto>>();
|
||||||
admin.MapDelete("/configs/{id:guid}", ForceRevokeConfig);
|
admin.MapDelete("/configs/{id:guid}", ForceRevokeConfig).Produces(StatusCodes.Status204NoContent);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using PnvPanel.Api.Common;
|
using PnvPanel.Api.Common;
|
||||||
using PnvPanel.Application.Apps;
|
using PnvPanel.Application.Apps;
|
||||||
using PnvPanel.Application.Common.Messaging;
|
using PnvPanel.Application.Common.Messaging;
|
||||||
|
using PnvPanel.Domain.Apps;
|
||||||
|
|
||||||
namespace PnvPanel.Api.Endpoints;
|
namespace PnvPanel.Api.Endpoints;
|
||||||
|
|
||||||
@@ -8,7 +9,10 @@ public static class AppEndpoints
|
|||||||
{
|
{
|
||||||
public static IEndpointRouteBuilder MapAppEndpoints(this IEndpointRouteBuilder app)
|
public static IEndpointRouteBuilder MapAppEndpoints(this IEndpointRouteBuilder app)
|
||||||
{
|
{
|
||||||
app.MapGet("/api/apps", ListApps).WithTags("Apps").RequireAuthorization();
|
app.MapGet("/api/apps", ListApps)
|
||||||
|
.WithTags("Apps")
|
||||||
|
.RequireAuthorization()
|
||||||
|
.Produces<IReadOnlyDictionary<OsPlatform, IReadOnlyList<ClientAppDto>>>();
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,13 +21,13 @@ public static class AuthEndpoints
|
|||||||
.WithTags("Auth")
|
.WithTags("Auth")
|
||||||
.RequireRateLimiting(RateLimiting.AuthPolicy);
|
.RequireRateLimiting(RateLimiting.AuthPolicy);
|
||||||
|
|
||||||
group.MapPost("/register", Register);
|
group.MapPost("/register", Register).Produces<RegisterResult>();
|
||||||
group.MapPost("/login", Login);
|
group.MapPost("/login", Login).Produces<AuthResponseDto>();
|
||||||
group.MapPost("/refresh", Refresh);
|
group.MapPost("/refresh", Refresh).Produces<AuthResponseDto>();
|
||||||
group.MapPost("/logout", Logout).RequireAuthorization();
|
group.MapPost("/logout", Logout).RequireAuthorization().Produces(StatusCodes.Status204NoContent);
|
||||||
group.MapPost("/change-password", ChangePassword).RequireAuthorization();
|
group.MapPost("/change-password", ChangePassword).RequireAuthorization().Produces(StatusCodes.Status204NoContent);
|
||||||
group.MapGet("/me", Me).RequireAuthorization();
|
group.MapGet("/me", Me).RequireAuthorization().Produces<CurrentUserDto>();
|
||||||
group.MapDelete("/me", DeleteMe).RequireAuthorization();
|
group.MapDelete("/me", DeleteMe).RequireAuthorization().Produces(StatusCodes.Status204NoContent);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
@@ -64,6 +64,9 @@ public static class AuthEndpoints
|
|||||||
return Results.Ok(ToLoginResponse(result.Value));
|
return Results.Ok(ToLoginResponse(result.Value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static AuthResponseDto ToLoginResponse(AuthResult auth) =>
|
||||||
|
new(auth.AccessToken, auth.AccessTokenExpiresAt, auth.User);
|
||||||
|
|
||||||
private static async Task<IResult> Logout(HttpRequest request, HttpResponse response, ISender sender, CancellationToken cancellationToken)
|
private static async Task<IResult> 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))
|
||||||
@@ -92,13 +95,6 @@ public static class AuthEndpoints
|
|||||||
return result.ToHttpResult();
|
return result.ToHttpResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static object ToLoginResponse(AuthResult auth) => new
|
|
||||||
{
|
|
||||||
accessToken = auth.AccessToken,
|
|
||||||
expiresAt = auth.AccessTokenExpiresAt,
|
|
||||||
user = auth.User,
|
|
||||||
};
|
|
||||||
|
|
||||||
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);
|
var options = BuildCookieOptions(request);
|
||||||
@@ -116,3 +112,5 @@ public static class AuthEndpoints
|
|||||||
Path = "/api/auth",
|
Path = "/api/auth",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record AuthResponseDto(string AccessToken, DateTimeOffset ExpiresAt, CurrentUserDto User);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using PnvPanel.Api.Common;
|
using PnvPanel.Api.Common;
|
||||||
using PnvPanel.Application.Common.Messaging;
|
using PnvPanel.Application.Common.Messaging;
|
||||||
|
using PnvPanel.Application.Configs;
|
||||||
using PnvPanel.Application.Configs.Create;
|
using PnvPanel.Application.Configs.Create;
|
||||||
using PnvPanel.Application.Configs.Edit;
|
using PnvPanel.Application.Configs.Edit;
|
||||||
using PnvPanel.Application.Configs.GetConfigLink;
|
using PnvPanel.Application.Configs.GetConfigLink;
|
||||||
@@ -17,14 +18,14 @@ public static class ConfigEndpoints
|
|||||||
{
|
{
|
||||||
var group = app.MapGroup("/api").WithTags("Configs").RequireAuthorization();
|
var group = app.MapGroup("/api").WithTags("Configs").RequireAuthorization();
|
||||||
|
|
||||||
group.MapGet("/inbounds/available", ListAvailableInbounds);
|
group.MapGet("/inbounds/available", ListAvailableInbounds).Produces<IReadOnlyList<AvailableInboundDto>>();
|
||||||
group.MapGet("/configs", GetMyConfigs);
|
group.MapGet("/configs", GetMyConfigs).Produces<GetMyConfigsResult>();
|
||||||
group.MapPost("/configs", CreateConfig);
|
group.MapPost("/configs", CreateConfig).Produces<VpnConfigDto>();
|
||||||
group.MapPatch("/configs/{id:guid}", EditConfig);
|
group.MapPatch("/configs/{id:guid}", EditConfig).Produces<VpnConfigDto>();
|
||||||
group.MapPost("/configs/{id:guid}/rotate", RotateConfig);
|
group.MapPost("/configs/{id:guid}/rotate", RotateConfig).Produces<VpnConfigDto>();
|
||||||
group.MapDelete("/configs/{id:guid}", RevokeConfig);
|
group.MapDelete("/configs/{id:guid}", RevokeConfig).Produces(StatusCodes.Status204NoContent);
|
||||||
group.MapGet("/configs/{id:guid}/link", GetConfigLink);
|
group.MapGet("/configs/{id:guid}/link", GetConfigLink).Produces<ConfigLinkResponseDto>();
|
||||||
group.MapGet("/subscription", GetMySubscription);
|
group.MapGet("/subscription", GetMySubscription).Produces<MySubscriptionResponseDto>();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
@@ -74,7 +75,7 @@ public static class ConfigEndpoints
|
|||||||
return result.ToHttpResult();
|
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 { connectionString = result.Value.ConnectionString, subscriptionUrl });
|
return Results.Ok(new ConfigLinkResponseDto(result.Value.ConnectionString, subscriptionUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> GetMySubscription(HttpRequest request, ISender sender, CancellationToken cancellationToken)
|
private static async Task<IResult> GetMySubscription(HttpRequest request, ISender sender, CancellationToken cancellationToken)
|
||||||
@@ -84,10 +85,14 @@ public static class ConfigEndpoints
|
|||||||
return result.ToHttpResult();
|
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 { subscriptionUrl });
|
return Results.Ok(new MySubscriptionResponseDto(subscriptionUrl));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed record CreateConfigBody(Guid InboundId, string? Label, int? DeviceLimit);
|
public sealed record CreateConfigBody(Guid InboundId, string? Label, int? DeviceLimit);
|
||||||
|
|
||||||
public sealed record EditConfigBody(string? Label, int? DeviceLimit);
|
public sealed record EditConfigBody(string? Label, int? DeviceLimit);
|
||||||
|
|
||||||
|
public sealed record ConfigLinkResponseDto(string ConnectionString, string SubscriptionUrl);
|
||||||
|
|
||||||
|
public sealed record MySubscriptionResponseDto(string SubscriptionUrl);
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ public static class InboundEndpoints
|
|||||||
.WithTags("Admin.Inbounds")
|
.WithTags("Admin.Inbounds")
|
||||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||||
|
|
||||||
admin.MapGet("", ListInbounds);
|
admin.MapGet("", ListInbounds).Produces<IReadOnlyList<InboundDto>>();
|
||||||
admin.MapPut("/{id:guid}/publish", PublishInbound);
|
admin.MapPut("/{id:guid}/publish", PublishInbound).Produces<InboundDto>();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ public static class NodeEndpoints
|
|||||||
.WithTags("Admin.Nodes")
|
.WithTags("Admin.Nodes")
|
||||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||||
|
|
||||||
admin.MapGet("", ListNodes);
|
admin.MapGet("", ListNodes).Produces<IReadOnlyList<NodeDto>>();
|
||||||
admin.MapPost("", RegisterNode);
|
admin.MapPost("", RegisterNode).Produces<NodeDto>();
|
||||||
admin.MapPut("/{id:guid}", UpdateNode);
|
admin.MapPut("/{id:guid}", UpdateNode).Produces<NodeDto>();
|
||||||
admin.MapDelete("/{id:guid}", DeleteNode);
|
admin.MapDelete("/{id:guid}", DeleteNode).Produces(StatusCodes.Status204NoContent);
|
||||||
admin.MapPost("/{id:guid}/sync", SyncNode);
|
admin.MapPost("/{id:guid}/sync", SyncNode).Produces<SyncNodeResultDto>();
|
||||||
admin.MapPost("/{id:guid}/probe", ProbeNode);
|
admin.MapPost("/{id:guid}/probe", ProbeNode).Produces<NodeProbeResultDto>();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using PnvPanel.Api.Common;
|
using PnvPanel.Api.Common;
|
||||||
using PnvPanel.Application.Admin.Roles;
|
using PnvPanel.Application.Admin.Roles;
|
||||||
using PnvPanel.Application.Admin.Users;
|
using PnvPanel.Application.Admin.Users;
|
||||||
|
using PnvPanel.Application.Common.Interfaces;
|
||||||
using PnvPanel.Application.Common.Messaging;
|
using PnvPanel.Application.Common.Messaging;
|
||||||
using PnvPanel.Infrastructure.Identity;
|
using PnvPanel.Infrastructure.Identity;
|
||||||
|
|
||||||
@@ -14,11 +15,11 @@ public static class RoleEndpoints
|
|||||||
.WithTags("Admin.Roles")
|
.WithTags("Admin.Roles")
|
||||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||||
|
|
||||||
admin.MapGet("/roles", ListRoles);
|
admin.MapGet("/roles", ListRoles).Produces<IReadOnlyList<RoleDto>>();
|
||||||
admin.MapPost("/roles", CreateRole);
|
admin.MapPost("/roles", CreateRole).Produces<RoleDto>();
|
||||||
admin.MapPut("/roles/{id:guid}", UpdateRole);
|
admin.MapPut("/roles/{id:guid}", UpdateRole).Produces<RoleDto>();
|
||||||
admin.MapDelete("/roles/{id:guid}", DeleteRole);
|
admin.MapDelete("/roles/{id:guid}", DeleteRole).Produces(StatusCodes.Status204NoContent);
|
||||||
admin.MapPatch("/users/{id:guid}/role", ChangeUserRole);
|
admin.MapPatch("/users/{id:guid}/role", ChangeUserRole).Produces(StatusCodes.Status204NoContent);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ public static class SubscriptionEndpoints
|
|||||||
// Вне /api по дизайну (api-design.md) — публичный эндпоинт для VPN-клиентов.
|
// Вне /api по дизайну (api-design.md) — публичный эндпоинт для VPN-клиентов.
|
||||||
app.MapGet("/sub/{token}", GetSubscription)
|
app.MapGet("/sub/{token}", GetSubscription)
|
||||||
.WithTags("Subscription")
|
.WithTags("Subscription")
|
||||||
.RequireRateLimiting(RateLimiting.AuthPolicy);
|
.RequireRateLimiting(RateLimiting.AuthPolicy)
|
||||||
|
.Produces<string>(StatusCodes.Status200OK, "text/plain")
|
||||||
|
.Produces(StatusCodes.Status404NotFound);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using PnvPanel.Api.Common;
|
using PnvPanel.Api.Common;
|
||||||
|
using PnvPanel.Application.Auth;
|
||||||
using PnvPanel.Application.Common.Messaging;
|
using PnvPanel.Application.Common.Messaging;
|
||||||
using PnvPanel.Application.Telegram;
|
using PnvPanel.Application.Telegram;
|
||||||
|
using PnvPanel.Domain.Telegram;
|
||||||
using PnvPanel.Infrastructure.Telegram;
|
using PnvPanel.Infrastructure.Telegram;
|
||||||
|
|
||||||
namespace PnvPanel.Api.Endpoints;
|
namespace PnvPanel.Api.Endpoints;
|
||||||
@@ -14,10 +16,10 @@ public static class TelegramEndpoints
|
|||||||
.WithTags("Auth.Telegram")
|
.WithTags("Auth.Telegram")
|
||||||
.RequireRateLimiting(RateLimiting.AuthPolicy);
|
.RequireRateLimiting(RateLimiting.AuthPolicy);
|
||||||
|
|
||||||
group.MapPost("/link-token", CreateLinkToken).RequireAuthorization();
|
group.MapPost("/link-token", CreateLinkToken).RequireAuthorization().Produces<LinkTokenResponseDto>();
|
||||||
group.MapPost("/unlink", Unlink).RequireAuthorization();
|
group.MapPost("/unlink", Unlink).RequireAuthorization().Produces(StatusCodes.Status204NoContent);
|
||||||
group.MapPost("/login-request", CreateLoginRequest);
|
group.MapPost("/login-request", CreateLoginRequest).Produces<TelegramLoginRequestResponseDto>();
|
||||||
group.MapGet("/login-request/{id:guid}", GetLoginRequestStatus);
|
group.MapGet("/login-request/{id:guid}", GetLoginRequestStatus).Produces<TelegramLoginStatusResponseDto>();
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
@@ -34,7 +36,7 @@ public static class TelegramEndpoints
|
|||||||
? null
|
? null
|
||||||
: $"https://t.me/{botUsername}?start=link_{result.Value.Token}";
|
: $"https://t.me/{botUsername}?start=link_{result.Value.Token}";
|
||||||
|
|
||||||
return Results.Ok(new { deepLink, expiresAt = result.Value.ExpiresAt });
|
return Results.Ok(new LinkTokenResponseDto(deepLink, result.Value.ExpiresAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> Unlink(ISender sender, CancellationToken cancellationToken)
|
private static async Task<IResult> Unlink(ISender sender, CancellationToken cancellationToken)
|
||||||
@@ -56,10 +58,11 @@ public static class TelegramEndpoints
|
|||||||
? null
|
? null
|
||||||
: $"https://t.me/{botUsername}?start=login_{result.Value.RequestId}";
|
: $"https://t.me/{botUsername}?start=login_{result.Value.RequestId}";
|
||||||
|
|
||||||
return Results.Ok(new { requestId = result.Value.RequestId, deepLink, expiresAt = result.Value.ExpiresAt });
|
return Results.Ok(new TelegramLoginRequestResponseDto(result.Value.RequestId, deepLink, result.Value.ExpiresAt));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task<IResult> GetLoginRequestStatus(Guid id, HttpResponse response, ISender sender, CancellationToken cancellationToken)
|
private static async Task<IResult> GetLoginRequestStatus(
|
||||||
|
Guid id, HttpRequest request, HttpResponse response, ISender sender, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var result = await sender.Send(new GetLoginRequestStatusQuery(id), cancellationToken);
|
var result = await sender.Send(new GetLoginRequestStatusQuery(id), cancellationToken);
|
||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
@@ -67,25 +70,29 @@ public static class TelegramEndpoints
|
|||||||
|
|
||||||
if (result.Value.Auth is { } auth)
|
if (result.Value.Auth is { } auth)
|
||||||
{
|
{
|
||||||
|
// Secure = IsHttps запроса (учитывает ForwardedHeaders за внешним TLS-прокси) —
|
||||||
|
// иначе браузер/HttpClient не пришлёт cookie обратно на plain-http (см. AuthEndpoints).
|
||||||
var cookieOptions = new CookieOptions
|
var cookieOptions = new CookieOptions
|
||||||
{
|
{
|
||||||
HttpOnly = true,
|
HttpOnly = true,
|
||||||
Secure = true,
|
Secure = request.IsHttps,
|
||||||
SameSite = SameSiteMode.Strict,
|
SameSite = SameSiteMode.Strict,
|
||||||
Path = "/api/auth",
|
Path = "/api/auth",
|
||||||
Expires = auth.RefreshTokenExpiresAt,
|
Expires = auth.RefreshTokenExpiresAt,
|
||||||
};
|
};
|
||||||
response.Cookies.Append("pnv_refresh_token", auth.RefreshToken, cookieOptions);
|
response.Cookies.Append("pnv_refresh_token", auth.RefreshToken, cookieOptions);
|
||||||
|
|
||||||
return Results.Ok(new
|
return Results.Ok(new TelegramLoginStatusResponseDto(
|
||||||
{
|
result.Value.Status, auth.AccessToken, auth.AccessTokenExpiresAt, auth.User));
|
||||||
status = result.Value.Status.ToString(),
|
|
||||||
accessToken = auth.AccessToken,
|
|
||||||
expiresAt = auth.AccessTokenExpiresAt,
|
|
||||||
user = auth.User,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Results.Ok(new { status = result.Value.Status.ToString() });
|
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 TelegramLoginStatusResponseDto(
|
||||||
|
TelegramLoginStatus Status, string? AccessToken, DateTimeOffset? ExpiresAt, CurrentUserDto? User);
|
||||||
|
|||||||
+5
-3
@@ -73,7 +73,8 @@
|
|||||||
- **Админ в боте**: уведомления о запросах активации + inline «Активировать/Отклонить», `/requests` (по Telegram id из env).
|
- **Админ в боте**: уведомления о запросах активации + inline «Активировать/Отклонить», `/requests` (по Telegram id из env).
|
||||||
- **DM-уведомления юзеру**: активация (`ApproveActivationCommandHandler`), блокировка (`BlockUserCommandHandler`), принудительный отзыв конфига админом (`ForceRevokeConfigCommandHandler`) — если Telegram привязан. Бот — read-only по конфигам.
|
- **DM-уведомления юзеру**: активация (`ApproveActivationCommandHandler`), блокировка (`BlockUserCommandHandler`), принудительный отзыв конфига админом (`ForceRevokeConfigCommandHandler`) — если Telegram привязан. Бот — read-only по конфигам.
|
||||||
- **Готово, когда**: юзер привязывает Telegram, входит без пароля, видит конфиги; админ активирует запросы прямо в боте. ✅ Достигнуто.
|
- **Готово, когда**: юзер привязывает Telegram, входит без пароля, видит конфиги; админ активирует запросы прямо в боте. ✅ Достигнуто.
|
||||||
- **Перенесено в backlog** (не реализовано в MVP): восстановление пароля через бота (`/resetpassword` с одноразовой ссылкой) — сейчас сброс пароля только через админа (`ResetUserPasswordCommand`); QR прямо в сообщениях бота; фронтовые кнопки «Войти через Telegram»/«Привязать Telegram» (бэкенд-контракт готов, фронт не реализовывался в эту итерацию).
|
- **Перенесено в backlog** (не реализовано в MVP): восстановление пароля через бота (`/resetpassword` с одноразовой ссылкой) — сейчас сброс пароля только через админа (`ResetUserPasswordCommand`); QR прямо в сообщениях бота.
|
||||||
|
Фронтовые кнопки «Войти через Telegram»/«Привязать Telegram» реализованы в отдельной итерации (см. M0 фронт).
|
||||||
|
|
||||||
## M8 — Закалка (hardening) ✅
|
## M8 — Закалка (hardening) ✅
|
||||||
- Тесты: `PnvPanel.Domain.Tests` (54, чистые unit-тесты инвариантов сущностей), `PnvPanel.Application.Tests`
|
- Тесты: `PnvPanel.Domain.Tests` (54, чистые unit-тесты инвариантов сущностей), `PnvPanel.Application.Tests`
|
||||||
@@ -87,8 +88,9 @@
|
|||||||
`dp_keys` для key-ring Data Protection (переживает пересоздание контейнера), healthcheck `app`
|
`dp_keys` для key-ring Data Protection (переживает пересоздание контейнера), healthcheck `app`
|
||||||
через `GET /health` (curl добавлен в runtime-образ). TLS — внешним прокси (без изменений).
|
через `GET /health` (curl добавлен в runtime-образ). TLS — внешним прокси (без изменений).
|
||||||
- **Готово, когда**: зелёный CI, покрытие ключевых сценариев, готовность к деплою. ✅ Достигнуто
|
- **Готово, когда**: зелёный CI, покрытие ключевых сценариев, готовность к деплою. ✅ Достигнуто
|
||||||
(интеграционные тесты не запускались локально — Docker Desktop недоступен на машине разработки;
|
(интеграционные тесты прогнаны локально через Testcontainers после появления Docker на машине
|
||||||
зависят от Docker в CI для первого реального прогона).
|
разработки — 134/134 зелёных; там же впервые собран и проверен единый Docker-образ и
|
||||||
|
docker-compose стек end-to-end).
|
||||||
|
|
||||||
## Backlog (после MVP)
|
## Backlog (после MVP)
|
||||||
- Полное самообслуживание в боте (создание/ротация/отзыв конфигов) — в MVP бот read-only.
|
- Полное самообслуживание в боте (создание/ротация/отзыв конфигов) — в MVP бот read-only.
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ export interface paths {
|
|||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description OK */
|
||||||
200: {
|
200: {
|
||||||
|
headers: {
|
||||||
|
[name: string]: unknown;
|
||||||
|
};
|
||||||
|
content: {
|
||||||
|
"text/plain": string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/** @description Not Found */
|
||||||
|
404: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -60,7 +69,11 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
[key: string]: components["schemas"]["ClientAppDto"][];
|
||||||
|
};
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -99,7 +112,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["RegisterResult"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -136,7 +151,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["AuthResponseDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -169,7 +186,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["AuthResponseDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -197,8 +216,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -234,8 +253,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -270,7 +289,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["CurrentUserDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -285,8 +306,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -320,7 +341,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["ActivationStatusDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -359,7 +382,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["ActivationRequestDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -394,7 +419,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["PagedListOfActivationRequestAdminDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -426,8 +453,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -465,8 +492,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -501,7 +528,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["RoleDto"][];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -524,7 +553,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["RoleDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -562,7 +593,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["RoleDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -578,8 +611,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -620,8 +653,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -652,7 +685,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["NodeDto"][];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -675,7 +710,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["NodeDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -713,7 +750,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["NodeDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -729,8 +768,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -768,7 +807,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["SyncNodeResultDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -803,7 +844,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["NodeProbeResultDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -836,7 +879,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["InboundDto"][];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -876,7 +921,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["InboundDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -908,7 +955,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["AvailableInboundDto"][];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -941,7 +990,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["GetMyConfigsResult"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -964,7 +1015,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["VpnConfigDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -995,8 +1048,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -1026,7 +1079,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["VpnConfigDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1057,7 +1112,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["VpnConfigDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1090,7 +1147,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["ConfigLinkResponseDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1123,7 +1182,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["MySubscriptionResponseDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1160,7 +1221,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["PagedListOfUserSummaryDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1196,8 +1259,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -1231,8 +1294,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -1266,8 +1329,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -1304,7 +1367,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["VpnConfigDto"][];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1337,8 +1402,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -1372,7 +1437,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["StatsDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1408,7 +1475,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["PagedListOfAuditLogDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1441,7 +1510,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["AdminAppDto"][];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1464,7 +1535,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["AdminAppDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1502,7 +1575,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["AdminAppDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1518,8 +1593,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -1555,7 +1630,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["LinkTokenResponseDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1583,8 +1660,8 @@ export interface paths {
|
|||||||
};
|
};
|
||||||
requestBody?: never;
|
requestBody?: never;
|
||||||
responses: {
|
responses: {
|
||||||
/** @description OK */
|
/** @description No Content */
|
||||||
200: {
|
204: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
@@ -1621,7 +1698,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["TelegramLoginRequestResponseDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1654,7 +1733,9 @@ export interface paths {
|
|||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content?: never;
|
content: {
|
||||||
|
"application/json": components["schemas"]["TelegramLoginStatusResponseDto"];
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -1670,8 +1751,69 @@ export interface paths {
|
|||||||
export type webhooks = Record<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
ActivationRequestAdminDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
/** Format: uuid */
|
||||||
|
userId: string;
|
||||||
|
userName: string;
|
||||||
|
comment: null | string;
|
||||||
|
status: components["schemas"]["ActivationStatus"];
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
ActivationRequestDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
comment: null | string;
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
/** @enum {unknown} */
|
/** @enum {unknown} */
|
||||||
ActivationStatus: "Pending" | "Approved" | "Rejected" | null;
|
ActivationStatus: "Pending" | "Approved" | "Rejected";
|
||||||
|
ActivationStatusDto: {
|
||||||
|
isActivated: boolean;
|
||||||
|
pendingRequest: null | components["schemas"]["ActivationRequestDto"];
|
||||||
|
};
|
||||||
|
AdminAppDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
operatingSystem: components["schemas"]["OsPlatform"];
|
||||||
|
description: null | string;
|
||||||
|
iconUrl: null | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
sortOrder: number | string;
|
||||||
|
isEnabled: boolean;
|
||||||
|
};
|
||||||
|
AuditLogDto: {
|
||||||
|
/** Format: int64 */
|
||||||
|
id: number | string;
|
||||||
|
/** Format: uuid */
|
||||||
|
actorId: null | string;
|
||||||
|
action: string;
|
||||||
|
targetType: string;
|
||||||
|
targetId: string;
|
||||||
|
metadata: null | string;
|
||||||
|
source: components["schemas"]["AuditSource"];
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
/** @enum {unknown} */
|
||||||
|
AuditSource: "Web" | "Telegram" | "System";
|
||||||
|
AuthResponseDto: {
|
||||||
|
accessToken: string;
|
||||||
|
/** Format: date-time */
|
||||||
|
expiresAt: string;
|
||||||
|
user: components["schemas"]["CurrentUserDto"];
|
||||||
|
};
|
||||||
|
AvailableInboundDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
inboundId: string;
|
||||||
|
displayName: string;
|
||||||
|
protocol: components["schemas"]["VpnProtocol"];
|
||||||
|
};
|
||||||
ChangePasswordCommand: {
|
ChangePasswordCommand: {
|
||||||
currentPassword: string;
|
currentPassword: string;
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
@@ -1680,6 +1822,20 @@ export interface components {
|
|||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
roleId: string;
|
roleId: string;
|
||||||
};
|
};
|
||||||
|
ClientAppDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
downloadUrl: string;
|
||||||
|
description: null | string;
|
||||||
|
iconUrl: null | string;
|
||||||
|
};
|
||||||
|
ConfigLinkResponseDto: {
|
||||||
|
connectionString: string;
|
||||||
|
subscriptionUrl: string;
|
||||||
|
};
|
||||||
|
/** @enum {unknown} */
|
||||||
|
ConfigStatus: "Active" | "Disabled" | "Expired" | "LimitReached" | "Revoked";
|
||||||
CreateAppCommand: {
|
CreateAppCommand: {
|
||||||
name: string;
|
name: string;
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
@@ -1701,17 +1857,102 @@ export interface components {
|
|||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
maxConfigs: number | string;
|
maxConfigs: number | string;
|
||||||
};
|
};
|
||||||
|
CurrentUserDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
userName: string;
|
||||||
|
role: string;
|
||||||
|
isActivated: boolean;
|
||||||
|
telegramLinked: boolean;
|
||||||
|
};
|
||||||
EditConfigBody: {
|
EditConfigBody: {
|
||||||
label: null | string;
|
label: null | string;
|
||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
deviceLimit: null | number | string;
|
deviceLimit: null | number | string;
|
||||||
};
|
};
|
||||||
|
GetMyConfigsResult: {
|
||||||
|
configs: components["schemas"]["VpnConfigDto"][];
|
||||||
|
/** Format: int32 */
|
||||||
|
maxConfigs: number | string;
|
||||||
|
};
|
||||||
|
InboundDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
/** Format: uuid */
|
||||||
|
nodeId: string;
|
||||||
|
remoteInboundId: string;
|
||||||
|
protocol: components["schemas"]["VpnProtocol"];
|
||||||
|
remark: string;
|
||||||
|
/** Format: int32 */
|
||||||
|
port: number | string;
|
||||||
|
isPublished: boolean;
|
||||||
|
displayName: null | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
maxClients: null | number | string;
|
||||||
|
allowedRoleIds: string[];
|
||||||
|
/** Format: date-time */
|
||||||
|
lastSyncAt: null | string;
|
||||||
|
};
|
||||||
|
LinkTokenResponseDto: {
|
||||||
|
deepLink: null | string;
|
||||||
|
/** Format: date-time */
|
||||||
|
expiresAt: string;
|
||||||
|
};
|
||||||
LoginCommand: {
|
LoginCommand: {
|
||||||
userName: string;
|
userName: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
MySubscriptionResponseDto: {
|
||||||
|
subscriptionUrl: string;
|
||||||
|
};
|
||||||
|
NodeDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
baseAddress: string;
|
||||||
|
username: string;
|
||||||
|
location: null | string;
|
||||||
|
status: components["schemas"]["NodeStatus"];
|
||||||
|
isEnabled: boolean;
|
||||||
|
/** Format: date-time */
|
||||||
|
lastSyncAt: null | string;
|
||||||
|
};
|
||||||
|
NodeProbeResultDto: {
|
||||||
|
isReachable: boolean;
|
||||||
|
errorMessage: null | string;
|
||||||
|
status: components["schemas"]["NodeStatus"];
|
||||||
|
};
|
||||||
|
/** @enum {unknown} */
|
||||||
|
NodeStatus: "Unknown" | "Online" | "Offline";
|
||||||
/** @enum {unknown} */
|
/** @enum {unknown} */
|
||||||
OsPlatform: "IOS" | "Android" | "Windows" | "MacOS" | "Linux";
|
OsPlatform: "IOS" | "Android" | "Windows" | "MacOS" | "Linux";
|
||||||
|
PagedListOfActivationRequestAdminDto: {
|
||||||
|
items: components["schemas"]["ActivationRequestAdminDto"][];
|
||||||
|
/** Format: int32 */
|
||||||
|
total: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
page: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
pageSize: number | string;
|
||||||
|
};
|
||||||
|
PagedListOfAuditLogDto: {
|
||||||
|
items: components["schemas"]["AuditLogDto"][];
|
||||||
|
/** Format: int32 */
|
||||||
|
total: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
page: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
pageSize: number | string;
|
||||||
|
};
|
||||||
|
PagedListOfUserSummaryDto: {
|
||||||
|
items: components["schemas"]["UserSummaryDto"][];
|
||||||
|
/** Format: int32 */
|
||||||
|
total: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
page: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
pageSize: number | string;
|
||||||
|
};
|
||||||
PublishInboundBody: {
|
PublishInboundBody: {
|
||||||
isPublished: boolean;
|
isPublished: boolean;
|
||||||
displayName: null | string;
|
displayName: null | string;
|
||||||
@@ -1730,6 +1971,11 @@ export interface components {
|
|||||||
password: string;
|
password: string;
|
||||||
location: null | string;
|
location: null | string;
|
||||||
};
|
};
|
||||||
|
RegisterResult: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
userName: string;
|
||||||
|
};
|
||||||
RejectActivationBody: {
|
RejectActivationBody: {
|
||||||
reason: null | string;
|
reason: null | string;
|
||||||
};
|
};
|
||||||
@@ -1739,6 +1985,55 @@ export interface components {
|
|||||||
ResetPasswordBody: {
|
ResetPasswordBody: {
|
||||||
newPassword: string;
|
newPassword: string;
|
||||||
};
|
};
|
||||||
|
RoleDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
/** Format: int32 */
|
||||||
|
maxConfigs: number | string;
|
||||||
|
isSystem: boolean;
|
||||||
|
};
|
||||||
|
StatsDto: {
|
||||||
|
/** Format: int32 */
|
||||||
|
totalUsers: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
activatedUsers: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
pendingActivationRequests: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
totalNodes: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
onlineNodes: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
totalConfigs: number | string;
|
||||||
|
/** Format: int32 */
|
||||||
|
activeConfigs: number | string;
|
||||||
|
/** Format: int64 */
|
||||||
|
totalUsedUpBytes: number | string;
|
||||||
|
/** Format: int64 */
|
||||||
|
totalUsedDownBytes: number | string;
|
||||||
|
};
|
||||||
|
SyncNodeResultDto: {
|
||||||
|
/** Format: int32 */
|
||||||
|
inboundsSynced: number | string;
|
||||||
|
status: components["schemas"]["NodeStatus"];
|
||||||
|
};
|
||||||
|
TelegramLoginRequestResponseDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
requestId: string;
|
||||||
|
deepLink: null | string;
|
||||||
|
/** Format: date-time */
|
||||||
|
expiresAt: string;
|
||||||
|
};
|
||||||
|
/** @enum {unknown} */
|
||||||
|
TelegramLoginStatus: "Pending" | "Approved" | "Rejected" | "Expired" | "Consumed";
|
||||||
|
TelegramLoginStatusResponseDto: {
|
||||||
|
status: components["schemas"]["TelegramLoginStatus"];
|
||||||
|
accessToken: null | string;
|
||||||
|
/** Format: date-time */
|
||||||
|
expiresAt: null | string;
|
||||||
|
user: null | components["schemas"]["CurrentUserDto"];
|
||||||
|
};
|
||||||
UpdateAppBody: {
|
UpdateAppBody: {
|
||||||
name: string;
|
name: string;
|
||||||
downloadUrl: string;
|
downloadUrl: string;
|
||||||
@@ -1760,6 +2055,36 @@ export interface components {
|
|||||||
/** Format: int32 */
|
/** Format: int32 */
|
||||||
maxConfigs: number | string;
|
maxConfigs: number | string;
|
||||||
};
|
};
|
||||||
|
UserSummaryDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
userName: string;
|
||||||
|
role: string;
|
||||||
|
isActivated: boolean;
|
||||||
|
isBlocked: boolean;
|
||||||
|
/** Format: date-time */
|
||||||
|
activatedAt: null | string;
|
||||||
|
};
|
||||||
|
VpnConfigDto: {
|
||||||
|
/** Format: uuid */
|
||||||
|
id: string;
|
||||||
|
label: null | string;
|
||||||
|
protocol: components["schemas"]["VpnProtocol"];
|
||||||
|
location: string;
|
||||||
|
/** Format: int32 */
|
||||||
|
deviceLimit: number | string;
|
||||||
|
/** Format: int64 */
|
||||||
|
usedUpBytes: number | string;
|
||||||
|
/** Format: int64 */
|
||||||
|
usedDownBytes: number | string;
|
||||||
|
/** Format: date-time */
|
||||||
|
expiresAt: null | string;
|
||||||
|
status: components["schemas"]["ConfigStatus"];
|
||||||
|
/** Format: date-time */
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
/** @enum {unknown} */
|
||||||
|
VpnProtocol: "Vless" | "Vmess" | "Trojan" | "Shadowsocks";
|
||||||
};
|
};
|
||||||
responses: never;
|
responses: never;
|
||||||
parameters: never;
|
parameters: never;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Типы вручную синхронизированы с DTO бэкенда (см. backend/src/PnvPanel.Application/**).
|
// Типы вручную синхронизированы с DTO бэкенда (см. backend/src/PnvPanel.Application/**).
|
||||||
// TODO: заменить на `pnpm gen:api` (openapi-typescript), когда бэкенд доступен по сети
|
// Держи в синхроне с `pnpm gen:api` (openapi-typescript, -> schema.gen.ts): все эндпоинты аннотированы
|
||||||
// (сейчас недоступен локально — Postgres/Docker не подняты, схему /openapi/v1.json взять негде).
|
// `.Produces<T>()`, схема полностью описывает тела ответов — сверяй при расхождении вручную (сгенерированный
|
||||||
|
// файл не используется напрямую фичами — предпочтены осмысленные имена и generic PagedList<T>, которых нет в JSON Schema).
|
||||||
|
|
||||||
export type ApiError = {
|
export type ApiError = {
|
||||||
title: string
|
title: string
|
||||||
|
|||||||
Reference in New Issue
Block a user