using Knot.Modules.Messaging; using Carter; using Knot.Shared.Infrastructure; using Knot.Modules.Auth; using Knot.Modules.Conversations; using Knot.Modules.Conversations.Infrastructure.SignalR; using Knot.Modules.Auth.Infrastructure.Persistence; using Knot.Modules.Conversations.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using System.Text; using System.IdentityModel.Tokens.Jwt; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.FileProviders; using Microsoft.AspNetCore.SignalR; using System.Security.Claims; JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); var builder = WebApplication.CreateBuilder(args); // Регистрация модулей // Маппинг стандартных переменных окружения в иерархию .NET var envMappings = new Dictionary { ["ConnectionStrings:DefaultConnection"] = builder.Configuration["DATABASE_URL"] ?? "Host=localhost;Database=knot;Username=postgres;Password=postgres", ["ConnectionStrings:MongoConnection"] = builder.Configuration["MONGO_CONNECTION"], ["Jwt:Secret"] = builder.Configuration["JWT_SECRET"], ["Jwt:Issuer"] = builder.Configuration["JWT_ISSUER"], ["Jwt:Audience"] = builder.Configuration["JWT_AUDIENCE"], ["WebRtc:TurnUrl"] = builder.Configuration["TURN_URL"], ["WebRtc:TurnUsername"] = builder.Configuration["TURN_USERNAME"], ["WebRtc:TurnPassword"] = builder.Configuration["TURN_PASSWORD"], ["KNOT_MASTER_ENCRYPTION_KEY"] = builder.Configuration["KNOT_MASTER_ENCRYPTION_KEY"], ["S3_ENDPOINT"] = builder.Configuration["S3_ENDPOINT"], ["S3_ACCESS_KEY"] = builder.Configuration["S3_ACCESS_KEY"], ["S3_SECRET_KEY"] = builder.Configuration["S3_SECRET_KEY"], ["S3_BUCKET"] = builder.Configuration["S3_BUCKET"], ["KNOT_ADMIN_USER"] = builder.Configuration["KNOT_ADMIN_USER"], ["KNOT_ADMIN_PASSWORD"] = builder.Configuration["KNOT_ADMIN_PASSWORD"] }; // Добавляем только те, что реально заданы в ENV builder.Configuration.AddInMemoryCollection( envMappings.Where(kv => !string.IsNullOrEmpty(kv.Value)) .ToDictionary(kv => kv.Key, kv => kv.Value)); builder.Services.AddAuthModule(builder.Configuration); builder.Services.AddConversationsModule(builder.Configuration); builder.Services.AddSharedInfrastructure(builder.Configuration); // CQRS / MediatR для команд в Host (например, AdminController) builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)); // Carter для вызова Minimal APIs (Endpoints) builder.Services.AddCarter(); // Настройка CORS builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { var originsFromConfig = builder.Configuration["Cors:Origins"]; var domain = builder.Configuration["DOMAIN"]; var origins = !string.IsNullOrEmpty(originsFromConfig) ? originsFromConfig.Split(',') : (!string.IsNullOrEmpty(domain) ? new[] { $"https://{domain}" } : new[] { "*" }); var corsBuilder = policy.WithOrigins(origins) .AllowAnyHeader() .AllowAnyMethod(); if (origins.Contains("*")) { // The CORS protocol does not allow specifying a wildcard (any) origin and credentials at the same time. } else { corsBuilder.AllowCredentials(); } }); }); // Настройка Swagger/OpenAPI builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Настройка маршрутизации builder.Services.AddRouting(options => { options.LowercaseUrls = true; options.LowercaseQueryStrings = true; }); builder.Services.AddMemoryCache(); builder.Services.AddHttpClient(); builder.Services.AddSignalR() .AddJsonProtocol(options => { options.PayloadSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.PayloadSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); builder.Services.AddSingleton(); // Настройка JWT Аутентификации builder.Services.AddAuthentication(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"]!)) }; options.Events = new JwtBearerEvents { OnMessageReceived = context => { var accessToken = context.Request.Query["access_token"]; var path = context.HttpContext.Request.Path; if (!string.IsNullOrEmpty(accessToken) && path.StartsWithSegments("/hubs")) { context.Token = accessToken; } return Task.CompletedTask; } }; }); builder.Services.AddAuthorization(); var app = builder.Build(); // Применяем миграции при старте using (var scope = app.Services.CreateScope()) { var identityDb = scope.ServiceProvider.GetRequiredService(); await identityDb.Database.MigrateAsync(); var chatsDb = scope.ServiceProvider.GetRequiredService(); await chatsDb.Database.MigrateAsync(); var systemDb = scope.ServiceProvider.GetRequiredService(); await systemDb.Database.MigrateAsync(); // Set Encryption Service for MongoDB serializers var encryptionService = scope.ServiceProvider.GetRequiredService(); Knot.Modules.Messaging.Infrastructure.Persistence.Mongo.EncryptedStringSerializer.EncryptionService = encryptionService; // Initialize Global Settings Cache var settingsService = scope.ServiceProvider.GetRequiredService(); if (settingsService is Knot.Shared.Infrastructure.Configuration.SettingsService concreteSettings) { concreteSettings.Initialize(); } } // Настройка конвейера запросов app.UseCors(); app.UseMiddleware(); app.UseMiddleware(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } // app.UseHttpsRedirection(); // Не раздаем статические файлы, так как теперь используем MinIO app.UseAuthentication(); app.UseAuthorization(); // Добавляем контроллеры и Carter (Minimal APIs) app.MapCarter(); // Добавляем SignalR хабы app.MapHub("/hubs/chat"); app.Run(); public class CustomUserIdProvider : IUserIdProvider { public string? GetUserId(HubConnectionContext connection) { return connection.User?.FindFirstValue("sub") ?? connection.User?.FindFirstValue(ClaimTypes.NameIdentifier); } }