ad62ac96ac
Шаг маршрута получает флаг «Автосогласование» (AutoApprove). Когда у активной строки такого шага истекает SLA, часовая задача Hangfire принимает за согласующего положительное решение: помечает строку, двигает очередь и пересчитывает статус заявки — тем же набором правил, что и портал, когда решение принимает человек (зеркало approverDecision.service.ts/decide). Флаг не nullable, в отличие от SLA рядом: наследовать его неоткуда — у типа заявки такого поля нет и быть не должно. - ApprovalRow и константы статусов переехали в общий Approvals: их теперь читают и напоминания, и автосогласование, разъезжаться разбору одного и того же списка нельзя; - SharePointClient научился MERGE'ить элементы (раньше умел только читать). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
166 lines
6.5 KiB
C#
166 lines
6.5 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.Extensions.Options;
|
|
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.Approvals;
|
|
using Tnsu.ApprovalRoute.Application.ListPartitioning;
|
|
using Tnsu.ApprovalRoute.Application.Reminders;
|
|
using Tnsu.ApprovalRoute.Application.Reports;
|
|
using Tnsu.ApprovalRoute.Application.Common.Interfaces;
|
|
using Tnsu.ApprovalRoute.Infrastructure;
|
|
|
|
const string CorsPolicyName = "Frontend";
|
|
|
|
var builder = WebApplication.CreateBuilder(args);
|
|
|
|
// Локальные оверрайды, не попадающие в git (см. .gitignore) — на своей машине
|
|
// можно, например, занизить ListPartitioning:ItemCountThreshold, чтобы не
|
|
// накапливать тысячи тестовых заявок для проверки создания партиций.
|
|
builder.Configuration.AddJsonFile("appsettings.Local.json", optional: true, reloadOnChange: true);
|
|
|
|
builder.Host.UseSerilog((context, services, configuration) => configuration
|
|
.ReadFrom.Configuration(context.Configuration)
|
|
.Enrich.FromLogContext()
|
|
.WriteTo.Console()
|
|
.WriteTo.File(
|
|
"logs/log-.txt",
|
|
rollingInterval: RollingInterval.Month,
|
|
retainedFileCountLimit: 6));
|
|
|
|
builder.Services.Configure<ForwardedHeadersOptions>(options =>
|
|
{
|
|
options.ForwardedHeaders =
|
|
ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
|
|
|
|
options.KnownIPNetworks.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.AddSingleton(TimeProvider.System);
|
|
builder.Services.AddApprovalRouteInfrastructure(builder.Configuration);
|
|
builder.Services.AddApprovalRouteApplication();
|
|
builder.Services.Configure<RemindersOptions>(builder.Configuration.GetSection("Reminders"));
|
|
builder.Services.Configure<ListPartitioningOptions>(builder.Configuration.GetSection("ListPartitioning"));
|
|
builder.Services.Configure<ReportsOptions>(builder.Configuration.GetSection("Reports"));
|
|
builder.Services.Configure<AutoApproveOptions>(builder.Configuration.GetSection("AutoApprove"));
|
|
|
|
builder.Services.AddHangfire(configuration => configuration
|
|
.SetDataCompatibilityLevel(CompatibilityLevel.Version_180)
|
|
.UseSimpleAssemblyNameTypeSerializer()
|
|
.UseRecommendedSerializerSettings()
|
|
.UsePostgreSqlStorage(postgres =>
|
|
postgres.UseNpgsqlConnection(builder.Configuration.GetConnectionString("Postgres"))));
|
|
|
|
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();
|
|
|
|
app.UseHangfireDashboard("/hangfire", new DashboardOptions
|
|
{
|
|
Authorization =
|
|
[
|
|
new HangfireDashboardBasicAuthFilter(
|
|
app.Configuration["Hangfire:Dashboard:Username"] ?? string.Empty,
|
|
app.Configuration["Hangfire:Dashboard:Password"] ?? string.Empty)
|
|
]
|
|
});
|
|
|
|
// Рекуррентная задача регистрируется ПОСЛЕ Build (нужен собранный контейнер) и
|
|
// на каждом старте: AddOrUpdate идемпотентен и заодно подхватывает изменившееся
|
|
// расписание из конфигурации — иначе задача навсегда осталась бы с тем cron, с
|
|
// которым её создали в первый раз.
|
|
var autoApprove = app.Services.GetRequiredService<IOptions<AutoApproveOptions>>().Value;
|
|
var recurringJobs = app.Services.GetRequiredService<IRecurringJobManager>();
|
|
if (autoApprove.Enabled)
|
|
{
|
|
recurringJobs.AddOrUpdate<AutoApproveJob>(
|
|
AutoApproveJob.RecurringJobId,
|
|
job => job.RunAsync(CancellationToken.None),
|
|
autoApprove.Cron,
|
|
// Cron в настройке — в UTC, как и всё остальное время в сервисе; иначе
|
|
// расписание поехало бы при смене часового пояса машины.
|
|
new RecurringJobOptions { TimeZone = TimeZoneInfo.Utc });
|
|
}
|
|
else
|
|
{
|
|
// Выключили настройкой — снимаем задачу, а не оставляем её вхолостую крутиться.
|
|
recurringJobs.RemoveIfExists(AutoApproveJob.RecurringJobId);
|
|
}
|
|
|
|
app.MapControllers();
|
|
|
|
await app.InitAndRunAsync();
|
|
|
|
public partial class Program;
|