using System.Text; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi; using Prometheus; using Quartz; using Serilog; using UniVerse.Api.BackgroundServices; using UniVerse.Api.Filters; using UniVerse.Api.Middleware; using UniVerse.Api.Options; using UniVerse.Application.Interfaces; using UniVerse.Infrastructure.Services; using UniVerse.Infrastructure.Data; using UniVerse.Infrastructure.ExternalServices; using UniVerse.Infrastructure.Notifications; var builder = WebApplication.CreateBuilder(args); var useAspire = builder.Configuration.GetValue("Aspire:Enabled"); var isOpenApiGeneration = AppDomain.CurrentDomain.GetAssemblies() .Any(assembly => assembly.GetName().Name == "GetDocument.Insider"); if (useAspire) { builder.AddServiceDefaults(); } // --- Serilog --- Log.Logger = new LoggerConfiguration() .ReadFrom.Configuration(builder.Configuration) .Enrich.FromLogContext() .WriteTo.Console() .CreateLogger(); builder.Host.UseSerilog(); // --- DbContext --- builder.Services.AddDbContext(options => { options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"), npgsql => { npgsql.EnableRetryOnFailure(3); npgsql.MigrationsAssembly("UniVerse.Infrastructure"); // Указывает EF Core, в какой сборке искать/хранить миграции. }); }); // --- Authentication --- builder.Services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:Secret"]!)) }; }); builder.Services.AddAuthorization(); // --- CORS --- builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { policy.WithOrigins( builder.Configuration.GetSection("Cors:Origins").Get() ?? ["http://localhost:5173", "http://localhost:3000"]) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); // --- Services DI --- builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); builder.Services.AddTransient(); builder.Services.Configure(builder.Configuration.GetSection("Email:Smtp")); builder.Services.AddOptions() .Bind(builder.Configuration.GetSection(ReviewAnalysisOptions.SectionName)) .Validate(options => options.MaxConcurrentProcessing >= 1, "Llm:ReviewAnalysis:MaxConcurrentProcessing must be greater than or equal to 1.") .ValidateOnStart(); builder.Services.AddQuartz(); if (!isOpenApiGeneration) { builder.Services.AddQuartzHostedService(options => { options.WaitForJobsToComplete = true; }); } if (builder.Environment.IsDevelopment() && !isOpenApiGeneration) { builder.Services.AddQuartzDashboard(options => { options.ReadOnly = true; }); } // --- HTTP Clients --- builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(builder.Configuration["Llm:BaseUrl"] ?? "https://api.openai.com/v1/"); client.Timeout = TimeSpan.FromSeconds(60); }); builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri(builder.Configuration["ModeusApi:BaseUrl"] ?? "https://schedule.rdcenter.ru"); client.Timeout = TimeSpan.FromSeconds(30); }); // --- Background Services --- if (!isOpenApiGeneration) { builder.Services.AddHostedService(); builder.Services.AddHostedService(); } // --- Controllers --- builder.Services.AddControllers() .AddJsonOptions(o => { o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); o.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); // --- Swagger --- builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = "UniVerse API", Version = "v1", Description = "REST API веб-платформы UniVerse.\n\n" + "Аутентификация: JWT Bearer (получить через `POST /api/v1/auth/login/microsoft` или `POST /api/v1/auth/login/dev` в Development).", Contact = new OpenApiContact { Name = "UniVerse Dev" } }); // Bearer security scheme definition (used per-endpoint by AuthorizeOperationFilter) options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme { Name = "Authorization", Type = SecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT", In = ParameterLocation.Header, Description = "Введите JWT access token, полученный из `/api/v1/auth/login/microsoft`.\n\nПример: `eyJhbGci...`" }); // Include XML doc comments generated from controller /// summaries var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); if (File.Exists(xmlPath)) options.IncludeXmlComments(xmlPath); // Per-endpoint security requirement + role documentation (replaces global AddSecurityRequirement) options.OperationFilter(); }); var app = builder.Build(); if (useAspire) { app.MapDefaultEndpoints(); } // --- Middleware Pipeline --- app.UseMiddleware(); app.UseMiddleware(); if (app.Environment.IsDevelopment()) { app.UseStaticFiles(); app.UseSwagger(c => { c.RouteTemplate = "api/docs/{documentName}/swagger.json"; }); app.UseSwaggerUI(c => { c.RoutePrefix = "api/docs"; c.SwaggerEndpoint("v1/swagger.json", "UniVerse API v1"); }); } app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); app.UseHttpMetrics(); if (app.Environment.IsDevelopment()) { app.UseAntiforgery(); app.MapQuartzDashboard(); } app.MapControllers(); // Restrict Prometheus scrape endpoint to local and private networks. app.UseWhen( context => context.Request.Path.StartsWithSegments("/metrics", StringComparison.OrdinalIgnoreCase), branch => branch.UseMiddleware()); app.MapMetrics(); app.Run();