Files
TeleWave/backend/src/TeleWave.Api/Program.cs
T

144 lines
5.8 KiB
C#

using System.Net;
using System.Text.Json.Serialization;
using System.Threading.RateLimiting;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.RateLimiting;
using Scalar.AspNetCore;
using Serilog;
using TeleWave.Api.Common;
using TeleWave.Api.Endpoints;
using TeleWave.Application;
using TeleWave.Infrastructure;
using TeleWave.Infrastructure.Identity;
using TeleWave.Infrastructure.Persistence;
var builder = WebApplication.CreateBuilder(args);
// Загрузка медиа стримится на диск; поднимаем лимит тела запроса Kestrel до максимума загрузки
// (иначе дефолтные ~30 МБ рубят большие файлы). Собственный контроль размера — в MediaEndpoints.
builder.WebHost.ConfigureKestrel(options =>
options.Limits.MaxRequestBodySize =
builder.Configuration.GetValue<long?>("Media:MaxUploadBytes") ?? 20L * 1024 * 1024 * 1024
);
// Структурное логирование (Serilog), конфигурация из appsettings/env.
builder.Services.AddSerilog(
(services, configuration) =>
configuration
.ReadFrom.Configuration(builder.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext()
);
// За внешним прокси доверяем X-Forwarded-* (TLS терминируется вне контейнера), но ТОЛЬКО от явно
// перечисленных адресов/сетей прокси — иначе клиент может подделать свой IP/схему напрямую, минуя
// прокси. По умолчанию (без конфигурации) остаётся дефолт ASP.NET Core — доверие только loopback;
// для прод-топологии прокси задаётся через ForwardedHeaders__KnownProxies/KnownNetworks (см. .env.example).
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
foreach (
var proxy in builder
.Configuration.GetSection("ForwardedHeaders:KnownProxies")
.Get<string[]>()
?? []
)
options.KnownProxies.Add(IPAddress.Parse(proxy));
foreach (
var network in builder
.Configuration.GetSection("ForwardedHeaders:KnownNetworks")
.Get<string[]>()
?? []
)
{
var parts = network.Split('/');
options.KnownIPNetworks.Add(
new System.Net.IPNetwork(IPAddress.Parse(parts[0]), int.Parse(parts[1]))
);
}
});
builder.Services.AddHttpContextAccessor();
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
var authPermitLimit = builder.Configuration.GetValue("RateLimiting:AuthPermitLimit", 20);
builder.Services.AddRateLimiter(options =>
{
// Партиционируем по IP клиента (реальный адрес доступен после UseForwardedHeaders): единый
// непартиционированный лимит превращается в DoS — один клиент исчерпывает окно логина для всех.
options.AddPolicy(
RateLimiting.AuthPolicy,
httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
_ => new FixedWindowRateLimiterOptions
{
PermitLimit = authPermitLimit,
Window = TimeSpan.FromMinutes(1),
QueueLimit = 0,
}
)
);
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});
// Энумы сериализуются строками, не числами — самодокументируемый JSON.
builder.Services.ConfigureHttpJsonOptions(options =>
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter())
);
builder.Services.AddProblemDetails();
builder.Services.AddOpenApi();
builder.Services.AddHealthChecks().AddDbContextCheck<AppDbContext>();
var app = builder.Build();
// Авто-применение миграций и идемпотентный сидинг (роли + админ из env) на старте.
await app.Services.ApplyMigrationsAsync();
await app.Services.RelocateLegacyImagesAsync();
await app.Services.SeedDataAsync();
app.UseForwardedHeaders();
app.UseSerilogRequestLogging();
app.UseExceptionHandler();
app.UseRateLimiter();
app.UseAuthentication();
app.UseAuthorization();
// Схему/UI API публикуем не в проде (или явным флагом Api:EnableOpenApi=true) — чтобы в продакшене
// не раскрывать полную карту эндпоинтов без необходимости.
if (app.Environment.IsDevelopment() || app.Configuration.GetValue("Api:EnableOpenApi", false))
{
app.MapOpenApi();
app.MapScalarApiReference();
}
app.MapHealthChecks("/health");
app.MapAuthEndpoints();
app.MapRoleEndpoints();
app.MapAdminUserEndpoints();
app.MapMediaEndpoints();
app.MapShowEndpoints();
app.MapChannelEndpoints();
app.MapStreamingEndpoints();
app.MapMaintenanceEndpoints();
app.MapSettingsEndpoints();
app.MapMetadataEndpoints();
app.MapImageEndpoints();
// Раздача статики SPA из wwwroot + fallback на index.html для клиентских маршрутов.
app.UseDefaultFiles();
app.UseStaticFiles();
app.MapFallbackToFile("index.html");
app.Run();
/// <summary>Делает неявный класс Program доступным для WebApplicationFactory&lt;Program&gt; в интеграционных тестах.</summary>
public partial class Program;