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.Admin.Activation;
|
||||
using PnvPanel.Application.Common.Messaging;
|
||||
using PnvPanel.Application.Common.Models;
|
||||
using PnvPanel.Domain.Activation;
|
||||
using PnvPanel.Infrastructure.Identity;
|
||||
|
||||
@@ -12,16 +13,16 @@ public static class ActivationEndpoints
|
||||
public static IEndpointRouteBuilder MapActivationEndpoints(this IEndpointRouteBuilder app)
|
||||
{
|
||||
var user = app.MapGroup("/api/activation").WithTags("Activation").RequireAuthorization();
|
||||
user.MapGet("/status", GetStatus);
|
||||
user.MapPost("/request", RequestActivation);
|
||||
user.MapGet("/status", GetStatus).Produces<ActivationStatusDto>();
|
||||
user.MapPost("/request", RequestActivation).Produces<ActivationRequestDto>();
|
||||
|
||||
var admin = app.MapGroup("/api/admin/activation-requests")
|
||||
.WithTags("Admin.Activation")
|
||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||
|
||||
admin.MapGet("", ListRequests);
|
||||
admin.MapPost("/{id:guid}/approve", Approve);
|
||||
admin.MapPost("/{id:guid}/reject", Reject);
|
||||
admin.MapGet("", ListRequests).Produces<PagedList<ActivationRequestAdminDto>>();
|
||||
admin.MapPost("/{id:guid}/approve", Approve).Produces(StatusCodes.Status204NoContent);
|
||||
admin.MapPost("/{id:guid}/reject", Reject).Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ public static class AdminAppEndpoints
|
||||
.WithTags("Admin.Apps")
|
||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||
|
||||
admin.MapGet("", ListApps);
|
||||
admin.MapPost("", CreateApp);
|
||||
admin.MapPut("/{id:guid}", UpdateApp);
|
||||
admin.MapDelete("/{id:guid}", DeleteApp);
|
||||
admin.MapGet("", ListApps).Produces<IReadOnlyList<AdminAppDto>>();
|
||||
admin.MapPost("", CreateApp).Produces<AdminAppDto>();
|
||||
admin.MapPut("/{id:guid}", UpdateApp).Produces<AdminAppDto>();
|
||||
admin.MapDelete("/{id:guid}", DeleteApp).Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using PnvPanel.Api.Common;
|
||||
using PnvPanel.Application.Admin.Audit;
|
||||
using PnvPanel.Application.Admin.Stats;
|
||||
using PnvPanel.Application.Common.Messaging;
|
||||
using PnvPanel.Application.Common.Models;
|
||||
using PnvPanel.Infrastructure.Identity;
|
||||
|
||||
namespace PnvPanel.Api.Endpoints;
|
||||
@@ -14,8 +15,8 @@ public static class AdminStatsEndpoints
|
||||
.WithTags("Admin.Stats")
|
||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||
|
||||
admin.MapGet("/stats", GetStats);
|
||||
admin.MapGet("/audit", GetAudit);
|
||||
admin.MapGet("/stats", GetStats).Produces<StatsDto>();
|
||||
admin.MapGet("/audit", GetAudit).Produces<PagedList<AuditLogDto>>();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
using PnvPanel.Api.Common;
|
||||
using PnvPanel.Application.Admin.Users;
|
||||
using PnvPanel.Application.Common.Interfaces;
|
||||
using PnvPanel.Application.Common.Messaging;
|
||||
using PnvPanel.Application.Common.Models;
|
||||
using PnvPanel.Application.Configs;
|
||||
using PnvPanel.Infrastructure.Identity;
|
||||
|
||||
namespace PnvPanel.Api.Endpoints;
|
||||
@@ -13,12 +16,12 @@ public static class AdminUserEndpoints
|
||||
.WithTags("Admin.Users")
|
||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||
|
||||
admin.MapGet("/users", ListUsers);
|
||||
admin.MapPatch("/users/{id:guid}/block", BlockUser);
|
||||
admin.MapPatch("/users/{id:guid}/unblock", UnblockUser);
|
||||
admin.MapPost("/users/{id:guid}/reset-password", ResetPassword);
|
||||
admin.MapGet("/users/{id:guid}/configs", GetUserConfigs);
|
||||
admin.MapDelete("/configs/{id:guid}", ForceRevokeConfig);
|
||||
admin.MapGet("/users", ListUsers).Produces<PagedList<UserSummaryDto>>();
|
||||
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.MapGet("/users/{id:guid}/configs", GetUserConfigs).Produces<IReadOnlyList<VpnConfigDto>>();
|
||||
admin.MapDelete("/configs/{id:guid}", ForceRevokeConfig).Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using PnvPanel.Api.Common;
|
||||
using PnvPanel.Application.Apps;
|
||||
using PnvPanel.Application.Common.Messaging;
|
||||
using PnvPanel.Domain.Apps;
|
||||
|
||||
namespace PnvPanel.Api.Endpoints;
|
||||
|
||||
@@ -8,7 +9,10 @@ public static class AppEndpoints
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,13 +21,13 @@ public static class AuthEndpoints
|
||||
.WithTags("Auth")
|
||||
.RequireRateLimiting(RateLimiting.AuthPolicy);
|
||||
|
||||
group.MapPost("/register", Register);
|
||||
group.MapPost("/login", Login);
|
||||
group.MapPost("/refresh", Refresh);
|
||||
group.MapPost("/logout", Logout).RequireAuthorization();
|
||||
group.MapPost("/change-password", ChangePassword).RequireAuthorization();
|
||||
group.MapGet("/me", Me).RequireAuthorization();
|
||||
group.MapDelete("/me", DeleteMe).RequireAuthorization();
|
||||
group.MapPost("/register", Register).Produces<RegisterResult>();
|
||||
group.MapPost("/login", Login).Produces<AuthResponseDto>();
|
||||
group.MapPost("/refresh", Refresh).Produces<AuthResponseDto>();
|
||||
group.MapPost("/logout", Logout).RequireAuthorization().Produces(StatusCodes.Status204NoContent);
|
||||
group.MapPost("/change-password", ChangePassword).RequireAuthorization().Produces(StatusCodes.Status204NoContent);
|
||||
group.MapGet("/me", Me).RequireAuthorization().Produces<CurrentUserDto>();
|
||||
group.MapDelete("/me", DeleteMe).RequireAuthorization().Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -64,6 +64,9 @@ public static class AuthEndpoints
|
||||
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)
|
||||
{
|
||||
if (request.Cookies.TryGetValue(RefreshCookieName, out var rawToken) && !string.IsNullOrEmpty(rawToken))
|
||||
@@ -92,13 +95,6 @@ public static class AuthEndpoints
|
||||
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)
|
||||
{
|
||||
var options = BuildCookieOptions(request);
|
||||
@@ -116,3 +112,5 @@ public static class AuthEndpoints
|
||||
Path = "/api/auth",
|
||||
};
|
||||
}
|
||||
|
||||
public sealed record AuthResponseDto(string AccessToken, DateTimeOffset ExpiresAt, CurrentUserDto User);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using PnvPanel.Api.Common;
|
||||
using PnvPanel.Application.Common.Messaging;
|
||||
using PnvPanel.Application.Configs;
|
||||
using PnvPanel.Application.Configs.Create;
|
||||
using PnvPanel.Application.Configs.Edit;
|
||||
using PnvPanel.Application.Configs.GetConfigLink;
|
||||
@@ -17,14 +18,14 @@ public static class ConfigEndpoints
|
||||
{
|
||||
var group = app.MapGroup("/api").WithTags("Configs").RequireAuthorization();
|
||||
|
||||
group.MapGet("/inbounds/available", ListAvailableInbounds);
|
||||
group.MapGet("/configs", GetMyConfigs);
|
||||
group.MapPost("/configs", CreateConfig);
|
||||
group.MapPatch("/configs/{id:guid}", EditConfig);
|
||||
group.MapPost("/configs/{id:guid}/rotate", RotateConfig);
|
||||
group.MapDelete("/configs/{id:guid}", RevokeConfig);
|
||||
group.MapGet("/configs/{id:guid}/link", GetConfigLink);
|
||||
group.MapGet("/subscription", GetMySubscription);
|
||||
group.MapGet("/inbounds/available", ListAvailableInbounds).Produces<IReadOnlyList<AvailableInboundDto>>();
|
||||
group.MapGet("/configs", GetMyConfigs).Produces<GetMyConfigsResult>();
|
||||
group.MapPost("/configs", CreateConfig).Produces<VpnConfigDto>();
|
||||
group.MapPatch("/configs/{id:guid}", EditConfig).Produces<VpnConfigDto>();
|
||||
group.MapPost("/configs/{id:guid}/rotate", RotateConfig).Produces<VpnConfigDto>();
|
||||
group.MapDelete("/configs/{id:guid}", RevokeConfig).Produces(StatusCodes.Status204NoContent);
|
||||
group.MapGet("/configs/{id:guid}/link", GetConfigLink).Produces<ConfigLinkResponseDto>();
|
||||
group.MapGet("/subscription", GetMySubscription).Produces<MySubscriptionResponseDto>();
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -74,7 +75,7 @@ public static class ConfigEndpoints
|
||||
return result.ToHttpResult();
|
||||
|
||||
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)
|
||||
@@ -84,10 +85,14 @@ public static class ConfigEndpoints
|
||||
return result.ToHttpResult();
|
||||
|
||||
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 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")
|
||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||
|
||||
admin.MapGet("", ListInbounds);
|
||||
admin.MapPut("/{id:guid}/publish", PublishInbound);
|
||||
admin.MapGet("", ListInbounds).Produces<IReadOnlyList<InboundDto>>();
|
||||
admin.MapPut("/{id:guid}/publish", PublishInbound).Produces<InboundDto>();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -13,12 +13,12 @@ public static class NodeEndpoints
|
||||
.WithTags("Admin.Nodes")
|
||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||
|
||||
admin.MapGet("", ListNodes);
|
||||
admin.MapPost("", RegisterNode);
|
||||
admin.MapPut("/{id:guid}", UpdateNode);
|
||||
admin.MapDelete("/{id:guid}", DeleteNode);
|
||||
admin.MapPost("/{id:guid}/sync", SyncNode);
|
||||
admin.MapPost("/{id:guid}/probe", ProbeNode);
|
||||
admin.MapGet("", ListNodes).Produces<IReadOnlyList<NodeDto>>();
|
||||
admin.MapPost("", RegisterNode).Produces<NodeDto>();
|
||||
admin.MapPut("/{id:guid}", UpdateNode).Produces<NodeDto>();
|
||||
admin.MapDelete("/{id:guid}", DeleteNode).Produces(StatusCodes.Status204NoContent);
|
||||
admin.MapPost("/{id:guid}/sync", SyncNode).Produces<SyncNodeResultDto>();
|
||||
admin.MapPost("/{id:guid}/probe", ProbeNode).Produces<NodeProbeResultDto>();
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using PnvPanel.Api.Common;
|
||||
using PnvPanel.Application.Admin.Roles;
|
||||
using PnvPanel.Application.Admin.Users;
|
||||
using PnvPanel.Application.Common.Interfaces;
|
||||
using PnvPanel.Application.Common.Messaging;
|
||||
using PnvPanel.Infrastructure.Identity;
|
||||
|
||||
@@ -14,11 +15,11 @@ public static class RoleEndpoints
|
||||
.WithTags("Admin.Roles")
|
||||
.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin));
|
||||
|
||||
admin.MapGet("/roles", ListRoles);
|
||||
admin.MapPost("/roles", CreateRole);
|
||||
admin.MapPut("/roles/{id:guid}", UpdateRole);
|
||||
admin.MapDelete("/roles/{id:guid}", DeleteRole);
|
||||
admin.MapPatch("/users/{id:guid}/role", ChangeUserRole);
|
||||
admin.MapGet("/roles", ListRoles).Produces<IReadOnlyList<RoleDto>>();
|
||||
admin.MapPost("/roles", CreateRole).Produces<RoleDto>();
|
||||
admin.MapPut("/roles/{id:guid}", UpdateRole).Produces<RoleDto>();
|
||||
admin.MapDelete("/roles/{id:guid}", DeleteRole).Produces(StatusCodes.Status204NoContent);
|
||||
admin.MapPatch("/users/{id:guid}/role", ChangeUserRole).Produces(StatusCodes.Status204NoContent);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ public static class SubscriptionEndpoints
|
||||
// Вне /api по дизайну (api-design.md) — публичный эндпоинт для VPN-клиентов.
|
||||
app.MapGet("/sub/{token}", GetSubscription)
|
||||
.WithTags("Subscription")
|
||||
.RequireRateLimiting(RateLimiting.AuthPolicy);
|
||||
.RequireRateLimiting(RateLimiting.AuthPolicy)
|
||||
.Produces<string>(StatusCodes.Status200OK, "text/plain")
|
||||
.Produces(StatusCodes.Status404NotFound);
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using PnvPanel.Api.Common;
|
||||
using PnvPanel.Application.Auth;
|
||||
using PnvPanel.Application.Common.Messaging;
|
||||
using PnvPanel.Application.Telegram;
|
||||
using PnvPanel.Domain.Telegram;
|
||||
using PnvPanel.Infrastructure.Telegram;
|
||||
|
||||
namespace PnvPanel.Api.Endpoints;
|
||||
@@ -14,10 +16,10 @@ public static class TelegramEndpoints
|
||||
.WithTags("Auth.Telegram")
|
||||
.RequireRateLimiting(RateLimiting.AuthPolicy);
|
||||
|
||||
group.MapPost("/link-token", CreateLinkToken).RequireAuthorization();
|
||||
group.MapPost("/unlink", Unlink).RequireAuthorization();
|
||||
group.MapPost("/login-request", CreateLoginRequest);
|
||||
group.MapGet("/login-request/{id:guid}", GetLoginRequestStatus);
|
||||
group.MapPost("/link-token", CreateLinkToken).RequireAuthorization().Produces<LinkTokenResponseDto>();
|
||||
group.MapPost("/unlink", Unlink).RequireAuthorization().Produces(StatusCodes.Status204NoContent);
|
||||
group.MapPost("/login-request", CreateLoginRequest).Produces<TelegramLoginRequestResponseDto>();
|
||||
group.MapGet("/login-request/{id:guid}", GetLoginRequestStatus).Produces<TelegramLoginStatusResponseDto>();
|
||||
|
||||
return app;
|
||||
}
|
||||
@@ -34,7 +36,7 @@ public static class TelegramEndpoints
|
||||
? null
|
||||
: $"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)
|
||||
@@ -56,10 +58,11 @@ public static class TelegramEndpoints
|
||||
? null
|
||||
: $"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);
|
||||
if (!result.IsSuccess)
|
||||
@@ -67,25 +70,29 @@ public static class TelegramEndpoints
|
||||
|
||||
if (result.Value.Auth is { } auth)
|
||||
{
|
||||
// Secure = IsHttps запроса (учитывает ForwardedHeaders за внешним TLS-прокси) —
|
||||
// иначе браузер/HttpClient не пришлёт cookie обратно на plain-http (см. AuthEndpoints).
|
||||
var cookieOptions = new CookieOptions
|
||||
{
|
||||
HttpOnly = true,
|
||||
Secure = true,
|
||||
Secure = request.IsHttps,
|
||||
SameSite = SameSiteMode.Strict,
|
||||
Path = "/api/auth",
|
||||
Expires = auth.RefreshTokenExpiresAt,
|
||||
};
|
||||
response.Cookies.Append("pnv_refresh_token", auth.RefreshToken, cookieOptions);
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
status = result.Value.Status.ToString(),
|
||||
accessToken = auth.AccessToken,
|
||||
expiresAt = auth.AccessTokenExpiresAt,
|
||||
user = auth.User,
|
||||
});
|
||||
return Results.Ok(new TelegramLoginStatusResponseDto(
|
||||
result.Value.Status, auth.AccessToken, auth.AccessTokenExpiresAt, 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);
|
||||
|
||||
Reference in New Issue
Block a user