YD_Event.Admin/YD_Event.Web.Entry/Controllers/WebsiteCompetitionController.cs
ltx 98aeb67212 feat(website): add authenticated competition management proxy
Forward authenticated GET and PUT requests to the website competition service using a server-side token. Register a dedicated HttpClient and document configuration, deployment, API behavior, and validation.
2026-10-19 16:08:59 +08:00

91 lines
3.7 KiB
C#

using System.Net.Http.Headers;
using System.Text;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace YD_Event.Web.Entry.Controllers;
/// <summary>
/// 官网赛事信息管理。使用后台登录态访问,服务间密钥只保存在服务端。
/// </summary>
[ApiController]
[Authorize]
[Route("api/website-competition")]
public sealed class WebsiteCompetitionController : ControllerBase
{
private readonly IConfiguration _configuration;
private readonly IHttpClientFactory _httpClientFactory;
public WebsiteCompetitionController(IConfiguration configuration, IHttpClientFactory httpClientFactory)
{
_configuration = configuration;
_httpClientFactory = httpClientFactory;
}
[HttpGet]
public Task<IActionResult> Get() => ForwardAsync(HttpMethod.Get);
[HttpPut]
[RequestSizeLimit(8 * 1024 * 1024)]
public Task<IActionResult> Put() => ForwardAsync(HttpMethod.Put);
private async Task<IActionResult> ForwardAsync(HttpMethod method)
{
var token = _configuration["CompetitionApi:AdminToken"]
?? _configuration["COMPETITION_ADMIN_TOKEN"];
var tokenFile = _configuration["CompetitionApi:AdminTokenFile"];
if (string.IsNullOrWhiteSpace(token) && !string.IsNullOrWhiteSpace(tokenFile))
{
try
{
token = (await System.IO.File.ReadAllTextAsync(tokenFile, HttpContext.RequestAborted)).Trim();
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return StatusCode(503, new { error = "官网赛事接口密钥文件不可读取" });
}
}
if (string.IsNullOrWhiteSpace(token))
return StatusCode(503, new { error = "官网赛事接口未配置服务端密钥" });
var adminUrl = _configuration["CompetitionApi:AdminUrl"]
?? "http://127.0.0.1:8787/api/competition/admin";
if (!Uri.TryCreate(adminUrl, UriKind.Absolute, out var target)
|| (target.Scheme != Uri.UriSchemeHttp && target.Scheme != Uri.UriSchemeHttps))
return StatusCode(503, new { error = "官网赛事接口地址无效" });
using var upstreamRequest = new HttpRequestMessage(method, target);
upstreamRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
if (method == HttpMethod.Put)
{
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
var body = await reader.ReadToEndAsync(HttpContext.RequestAborted);
upstreamRequest.Content = new StringContent(body, Encoding.UTF8, "application/json");
}
try
{
var client = _httpClientFactory.CreateClient("WebsiteCompetition");
client.Timeout = TimeSpan.FromSeconds(30);
using var upstreamResponse = await client.SendAsync(
upstreamRequest, HttpCompletionOption.ResponseHeadersRead, HttpContext.RequestAborted);
var response = await upstreamResponse.Content.ReadAsStringAsync(HttpContext.RequestAborted);
return new ContentResult
{
Content = response,
ContentType = "application/json; charset=utf-8",
StatusCode = (int)upstreamResponse.StatusCode
};
}
catch (HttpRequestException)
{
return StatusCode(502, new { error = "官网赛事接口连接失败" });
}
catch (TaskCanceledException) when (!HttpContext.RequestAborted.IsCancellationRequested)
{
return StatusCode(504, new { error = "官网赛事接口响应超时" });
}
}
}