276 lines
11 KiB
C#
276 lines
11 KiB
C#
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<string, string?>
|
||
{
|
||
["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<IUserIdProvider, CustomUserIdProvider>();
|
||
|
||
// Настройка 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<AuthDbContext>();
|
||
await identityDb.Database.MigrateAsync();
|
||
|
||
var chatsDb = scope.ServiceProvider.GetRequiredService<Knot.Modules.Conversations.Infrastructure.Persistence.ChatsDbContext>();
|
||
await chatsDb.Database.MigrateAsync();
|
||
|
||
var systemDb = scope.ServiceProvider.GetRequiredService<Knot.Shared.Infrastructure.Persistence.SystemDbContext>();
|
||
await systemDb.Database.MigrateAsync();
|
||
|
||
var storiesDb = scope.ServiceProvider.GetRequiredService<Knot.Modules.Stories.Infrastructure.Database.StoriesDbContext>();
|
||
await storiesDb.Database.MigrateAsync();
|
||
|
||
var relationsDb = scope.ServiceProvider.GetRequiredService<Knot.Modules.Relations.Infrastructure.Persistence.RelationsDbContext>();
|
||
await relationsDb.Database.MigrateAsync();
|
||
|
||
// Set Encryption Service for MongoDB serializers
|
||
var encryptionService = scope.ServiceProvider.GetRequiredService<Knot.Shared.Kernel.Security.IEncryptionService>();
|
||
Knot.Modules.Messaging.Infrastructure.Persistence.Mongo.EncryptedStringSerializer.EncryptionService = encryptionService;
|
||
|
||
// Initialize Global Settings Cache
|
||
var settingsService = scope.ServiceProvider.GetRequiredService<Knot.Contracts.Settings.Application.Abstractions.ISettingsService>();
|
||
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<IMediator>();
|
||
await mediator.Send(new Knot.Modules.Relations.Application.Contacts.SyncReplicasCommand());
|
||
}
|
||
|
||
// Настройка конвейера запросов
|
||
app.UseCors();
|
||
|
||
app.UseMiddleware<Knot.Shared.Infrastructure.Middleware.ExceptionHandlingMiddleware>();
|
||
app.UseMiddleware<Knot.Shared.Infrastructure.Middleware.AdminAuthMiddleware>();
|
||
|
||
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<ChatHub>("/hubs/chat");
|
||
|
||
app.Run();
|
||
|
||
public class CustomUserIdProvider : IUserIdProvider
|
||
{
|
||
public string? GetUserId(HubConnectionContext connection)
|
||
{
|
||
return connection.User?.FindFirstValue("sub") ?? connection.User?.FindFirstValue(ClaimTypes.NameIdentifier);
|
||
}
|
||
}
|
||
|