using System.Text; using PnvPanel.Api.Common; using PnvPanel.Application.Common.Messaging; using PnvPanel.Application.Subscriptions; namespace PnvPanel.Api.Endpoints; public static class SubscriptionEndpoints { public static IEndpointRouteBuilder MapSubscriptionEndpoints(this IEndpointRouteBuilder app) { // Вне /api по дизайну (api-design.md) — публичный эндпоинт для VPN-клиентов. app.MapGet("/sub/{token}", GetSubscription) .WithTags("Subscription") .RequireRateLimiting(RateLimiting.AuthPolicy) .Produces(StatusCodes.Status200OK, "text/plain") .Produces(StatusCodes.Status404NotFound); return app; } 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); if (!result.IsSuccess) return Results.NotFound(); var body = string.Join('\n', result.Value.ConnectionStrings); var base64Body = Convert.ToBase64String(Encoding.UTF8.GetBytes(body)); var total = result.Value.UsedUpBytes + result.Value.UsedDownBytes; 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}"); response.Headers.Append("Profile-Update-Interval", "12"); return Results.Text(base64Body, "text/plain; charset=utf-8"); } }