using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; using Knot.Modules.Admin; using Knot.Modules.Admin.Presentation.Endpoints; using Knot.Modules.Auth; using Knot.Modules.Auth.Infrastructure.Persistence; using Knot.Modules.Auth.Presentation.Endpoints; using Knot.Modules.Conversations; using Knot.Modules.Conversations.Infrastructure.Persistence; using Knot.Modules.Conversations.Infrastructure.SignalR; using Knot.Modules.Conversations.Presentation.Endpoints; using Knot.Modules.Federation; using Knot.Modules.Federation.Presentation.Endpoints; using Knot.Modules.Klipy; using Knot.Modules.Klipy.Presentation.Endpoints; using Knot.Modules.Messaging; using Knot.Modules.Profiles; using Knot.Modules.Profiles.Presentation.Endpoints; using Knot.Modules.Relations; using Knot.Modules.Relations.Presentation.Endpoints; using Knot.Modules.Settings; using Knot.Modules.Settings.Presentation.Endpoints; using Knot.Modules.Storage; using Knot.Modules.Storage.Presentation.Endpoints; using Knot.Modules.Stories; using Knot.Modules.Stories.Presentation.Endpoints; using Knot.Modules.TelegramImport; using Knot.Modules.TelegramImport.Presentation.Endpoints; using Knot.Modules.WebRtc; using Knot.Modules.WebRtc.Presentation.Endpoints; using Knot.Shared.Infrastructure; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.SignalR; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using MediatR; JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); Encoding.RegisterProvider(CodePagesEncodingProvider.Instance); var builder = WebApplication.CreateBuilder(args); // Регистрация модулей // Маппинг стандартных переменных окружения в иерархию .NET var envMappings = new Dictionary { ["ConnectionStrings:DefaultConnection"] = builder.Configuration["DATABASE_URL"] ?? builder.Configuration.GetConnectionString("DefaultConnection") ?? "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.AddSettingsModule(builder.Configuration); builder.Services.AddMessagingModule(builder.Configuration); builder.Services.AddConversationsModule(builder.Configuration); builder.Services.AddProfilesModule(builder.Configuration); builder.Services.AddRelationsModule(builder.Configuration); builder.Services.AddStorageModule(builder.Configuration); builder.Services.AddStoriesModule(builder.Configuration); builder.Services.AddKlipyModule(); builder.Services.AddAdminModule(); builder.Services.AddWebRtcModule(); builder.Services.AddTelegramImportModule(); builder.Services.AddSharedInfrastructure(builder.Configuration); // CQRS / MediatR для команд в Host (например, AdminController) builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies( typeof(Program).Assembly, typeof(Knot.Modules.Settings.DependencyInjection).Assembly, typeof(Knot.Modules.Admin.DependencyInjection).Assembly, typeof(Knot.Modules.Messaging.DependencyInjection).Assembly, typeof(Knot.Modules.Conversations.DependencyInjection).Assembly, typeof(Knot.Modules.Stories.DependencyInjection).Assembly, typeof(Knot.Modules.Klipy.DependencyInjection).Assembly, typeof(Knot.Modules.Relations.DependencyInjection).Assembly, typeof(Knot.Modules.WebRtc.DependencyInjection).Assembly, typeof(Knot.Modules.TelegramImport.DependencyInjection).Assembly, typeof(Knot.Modules.Auth.Infrastructure.Persistence.AuthDbContext).Assembly, typeof(Knot.Modules.Profiles.DependencyInjection).Assembly )); // Настройка 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.ConfigureHttpJsonOptions(options => { options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase; options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); 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(); var storiesDb = scope.ServiceProvider.GetRequiredService(); await storiesDb.Database.MigrateAsync(); var relationsDb = scope.ServiceProvider.GetRequiredService(); await relationsDb.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.Modules.Settings.Infrastructure.Configuration.SettingsService concreteSettings) { concreteSettings.Initialize(); } // Sync user replicas for Relations module on startup var mediator = scope.ServiceProvider.GetRequiredService(); await mediator.Send(new Knot.Modules.Relations.Application.Contacts.SyncReplicasCommand()); } // Настройка конвейера запросов app.UseCors(); app.UseMiddleware(); app.UseMiddleware(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } // app.UseHttpsRedirection(); // Не раздаем статические файлы, так как теперь используем MinIO app.UseAuthentication(); app.UseAuthorization(); // Регистрация эндпоинтов app.MapAuthEndpoints(); app.MapStoriesEndpoints(); app.MapContactsEndpoints(); app.MapSettingsEndpoints(); app.MapProfilesEndpoints(); app.MapStatusesEndpoints(); app.MapFederationEndpoints(); app.MapKlipyEndpoints(); app.MapChatsEndpoints(); app.MapMessagesEndpoints(); app.MapAdminEndpoints(); app.MapFilesEndpoints(); app.MapWebRtcEndpoints(); app.MapTelegramImportEndpoints(); // Добавляем 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); } }