Files
tnsu-approval-route/backend/Tnsu.ApprovalRoute.Api/Program.cs
T
2026-08-17 15:33:54 +05:00

147 lines
6.0 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Text.Json.Serialization;
using Extensions.Hosting.AsyncInitialization;
using Hangfire;
using Hangfire.PostgreSql;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.OpenApi;
using Scalar.AspNetCore;
using Serilog;
using Tnsu.ApprovalRoute.Api.Auth;
using Tnsu.ApprovalRoute.Api.Filters;
using Tnsu.ApprovalRoute.Application;
using Tnsu.ApprovalRoute.Application.ListPartitioning;
using Tnsu.ApprovalRoute.Application.Reminders;
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
using Tnsu.ApprovalRoute.Infrastructure;
const string CorsPolicyName = "Frontend";
var builder = WebApplication.CreateBuilder(args);
// Заменяет собой встроенных провайдеров логирования: все ILogger<T> в приложении
// (включая наши логи в ListPartitioning) идут через Serilog в оба синка ниже.
// ReadFrom.Configuration — чтобы Logging:LogLevel в appsettings продолжал работать
// как обычно, без второй, серилоговской системы настройки уровней рядом с ним.
builder.Host.UseSerilog((context, services, configuration) => configuration
.ReadFrom.Configuration(context.Configuration)
.Enrich.FromLogContext()
.WriteTo.Console()
// Один файл на календарный месяц (rollOnFileSizeLimit не включаем — ротация
// только по времени). retainedFileCountLimit: null — логи не подчищаются
// автоматически, ротацией места на диске занимается инфраструктура хоста.
.WriteTo.File(
"logs/log-.txt",
rollingInterval: RollingInterval.Month,
retainedFileCountLimit: null));
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
options.KnownNetworks.Clear();
options.KnownProxies.Clear();
});
var corsAllowedOrigins = builder.Configuration.GetSection("Cors:AllowedOrigins").Get<string[]>() ?? [];
builder.Services.AddCors(options =>
{
options.AddPolicy(CorsPolicyName, policy => policy
.WithOrigins(corsAllowedOrigins)
.AllowAnyHeader()
.AllowAnyMethod());
});
builder.Services.AddApprovalRouteInfrastructure(builder.Configuration);
builder.Services.AddApprovalRouteApplication();
builder.Services.Configure<RemindersOptions>(builder.Configuration.GetSection("Reminders"));
builder.Services.Configure<ListPartitioningOptions>(builder.Configuration.GetSection("ListPartitioning"));
// Hangfire на том же Postgres, что и остальные данные. Свои таблицы он создаёт сам
// (схема hangfire) при старте — отдельной EF-миграции для них не нужно.
builder.Services.AddHangfire(configuration => configuration
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
.UseSimpleAssemblyNameTypeSerializer()
.UseRecommendedSerializerSettings()
.UsePostgreSqlStorage(postgres =>
postgres.UseNpgsqlConnection(builder.Configuration.GetConnectionString("Postgres"))));
// Воркер живёт в процессе API: задач мало (создание списка — редкое событие), и
// отдельный хост под них был бы лишней инфраструктурой.
builder.Services.AddHangfireServer();
builder.Services.AddApprovalRouteAuth(builder.Configuration);
builder.Services.AddScoped<ICurrentUser, CurrentUserAccessor>();
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, _, _) =>
{
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, IOpenApiSecurityScheme>();
document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "Введите JWT токен"
};
return Task.CompletedTask;
});
options.AddOperationTransformer((operation, context, _) =>
{
operation.Security =
[
new OpenApiSecurityRequirement
{
[new OpenApiSecuritySchemeReference("Bearer", context.Document)] = []
}
];
return Task.CompletedTask;
});
});
builder.Services.AddHealthChecks();
builder.Services.AddControllers(options => options.Filters.Add<ValidationActionFilter>())
.AddJsonOptions(o => o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()));
builder.Services.ConfigureHttpJsonOptions(o => o.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));
var app = builder.Build();
app.UseForwardedHeaders();
app.MapOpenApi();
app.MapScalarApiReference(options =>
{
options
.WithTitle("Tnsu Approval Route")
.AddPreferredSecuritySchemes("Bearer")
.AddHttpAuthentication("Bearer", _ => { });
});
app.MapHealthChecks("/health");
app.UseCors(CorsPolicyName);
app.UseAuthentication();
app.UseAuthorization();
// Свой Basic Auth, а не схемы из AuthSetup: дашборд Hangfire открывается прямо в
// браузере без похода за токеном Entra, ему нужна отдельная, простая проверка.
// Логин/пароль — в конфигурации (Hangfire:Dashboard), см. appsettings.json.
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
Authorization =
[
new HangfireDashboardBasicAuthFilter(
app.Configuration["Hangfire:Dashboard:Username"] ?? string.Empty,
app.Configuration["Hangfire:Dashboard:Password"] ?? string.Empty)
]
});
app.MapControllers();
await app.InitAndRunAsync();
public partial class Program;