147 lines
6.0 KiB
C#
147 lines
6.0 KiB
C#
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;
|