sozsoft-platform/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs
2026-09-24 14:58:23 +03:00

584 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using DevExpress.AspNetCore;
using DevExpress.AspNetCore.Reporting;
using DevExpress.XtraReports.Web.Extensions;
using Hangfire;
using Hangfire.PostgreSql;
using Hangfire.SqlServer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.OpenApi.Models;
using OpenIddict.Server.AspNetCore;
using OpenIddict.Validation.AspNetCore;
using Sozsoft.Mcp;
using Sozsoft.Languages;
using Sozsoft.MailQueue;
using Sozsoft.Notifications.Application;
using Sozsoft.Platform.Notifications;
using Sozsoft.Platform.DbStartup;
using Sozsoft.Platform.DynamicServices;
using Sozsoft.Platform.EntityFrameworkCore;
using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.FileManagement;
using Sozsoft.Platform.Identity;
using Sozsoft.Platform.Localization;
using Sozsoft.Platform.Messenger;
using Sozsoft.Platform.ReportServices;
using Sozsoft.Settings;
using Sozsoft.SqlQueryManager;
using Volo.Abp;
using Volo.Abp.Account.Web;
using Volo.Abp.AspNetCore.Auditing;
using Volo.Abp.AspNetCore.ExceptionHandling;
using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.AspNetCore.Mvc;
using Volo.Abp.AspNetCore.Mvc.ApplicationConfigurations;
using Volo.Abp.AspNetCore.Mvc.UI.Bundling;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic;
using Volo.Abp.AspNetCore.Mvc.UI.Theme.Basic.Bundling;
using Volo.Abp.AspNetCore.Serilog;
using Volo.Abp.Autofac;
using Volo.Abp.BackgroundWorkers.Hangfire;
using Volo.Abp.BlobStoring;
using Volo.Abp.BlobStoring.FileSystem;
using Volo.Abp.Caching;
using Volo.Abp.Hangfire;
using Volo.Abp.Identity;
using Volo.Abp.Modularity;
using Volo.Abp.Security.Claims;
using Volo.Abp.Swashbuckle;
using Volo.Abp.UI.Navigation.Urls;
using static OpenIddict.Server.OpenIddictServerEvents;
using static Sozsoft.Platform.PlatformConsts;
using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform;
[DependsOn(
typeof(PlatformHttpApiModule),
typeof(AbpAutofacModule),
typeof(AbpAspNetCoreMultiTenancyModule),
typeof(PlatformApplicationModule),
typeof(PlatformEntityFrameworkCoreModule),
typeof(AbpAspNetCoreMvcUiBasicThemeModule),
typeof(AbpAccountWebOpenIddictModule),
typeof(AbpAspNetCoreSerilogModule),
typeof(AbpSwashbuckleModule),
typeof(AbpBackgroundWorkersHangfireModule),
typeof(SqlQueryManagerApplicationModule),
typeof(McpApplicationModule),
typeof(McpHttpApiModule)
)]
public class PlatformHttpApiHostModule : AbpModule
{
public override void PreConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
PreConfigure<OpenIddictBuilder>(builder =>
{
builder.AddServer(server =>
{
server.SetAccessTokenLifetime(TimeSpan.FromMinutes(configuration.GetValue("OpenIddict:TokenLifetimes:AccessTokenMinutes", 60)));
server.SetRefreshTokenLifetime(TimeSpan.FromMinutes(configuration.GetValue("OpenIddict:TokenLifetimes:RefreshTokenMinutes", 90)));
server.AddEventHandler<HandleRevocationRequestContext>(builder =>
builder.UseScopedHandler<PlatformSessionRevocationHandler>());
});
builder.AddValidation(options =>
{
options.AddAudiences(PlatformConsts.AppName);
options.UseLocalServer();
options.UseAspNetCore();
options.EnableTokenEntryValidation();
});
});
// Uzak MCP istemcilerinin OAuth sozlesmesi. Uc adresleri ABP'nin kendi sunucu
// ayarlarindan sonra uygulanmali; bu yuzden OpenIddictServerBuilder uzerinden verilir.
PreConfigure<OpenIddictServerBuilder>(server => McpOAuthServerConfiguration.Configure(server, configuration));
PreConfigure<IdentityBuilder>(builder =>
{
builder.AddClaimsPrincipalFactory<PlatformUserClaimsPrincipalFactory>();
builder.AddSignInManager<PlatformSignInManager>();
});
context.Services.Replace(
ServiceDescriptor.Transient<AbpUserClaimsPrincipalFactory, PlatformUserClaimsPrincipalFactory>());
context.Services.AddTransient<IPlatformSignInManager, PlatformSignInManager>();
}
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
//Tenant bazında lokalizasyon ayarları için
//TenantLocalization Middleware kaydı
context.Services.AddTransient<TenantLocalizationMiddleware>();
context.Services.AddTransient<TenantLocalizationInitializer>();
ConfigureAuthentication(context);
ConfigureBundles();
ConfigureUrls(configuration);
ConfigureConventionalControllers();
//Localization'ı veritabanından kullandıgımız icin bir json resource kullanmıyoruz
//Dolayısıyla şimdilik ihtiyacımız yok ama ileride başka bir sebepten kullanabiliriz diye silmedik
//ConfigureVirtualFileSystem(context);
ConfigureCors(context, configuration);
ConfigureSwaggerServices(context, configuration);
ConfigureIdentity(configuration);
ConfigureCache();
ConfigureHangfire(context, configuration);
context.Services.AddHostedService<BackgroundWorkerStartupService>();
// Sürüm notları imaja gömülü change-logs.json'dan senkronize edilir;
// yeni bir sürüm tag'i için DbMigrator çalıştırmak gerekmez.
context.Services.AddHostedService<Extensions.ChangeLogStartupService>();
ConfigureBlobStoring(configuration);
ConfigureAuditing();
context.Services.AddResponseCompression(options =>
{
options.EnableForHttps = true;
options.Providers.Add<BrotliCompressionProvider>();
options.Providers.Add<GzipCompressionProvider>();
});
ConfigureDynamicServices(context);
ConfigureDevExpressReporting(context, configuration);
var signalR = context.Services.AddSignalR();
if (configuration.GetValue<bool>("Redis:IsEnabled") &&
!configuration["Redis:Configuration"].IsNullOrWhiteSpace())
{
signalR.AddStackExchangeRedis(configuration["Redis:Configuration"]!);
}
Configure<AbpExceptionHttpStatusCodeOptions>(options =>
{
options.Map(AppErrorCodes.NoAuth, System.Net.HttpStatusCode.Unauthorized);
});
Configure<OpenIddictServerAspNetCoreOptions>(options =>
{
options.DisableTransportSecurityRequirement = true;
});
Configure<AbpApplicationConfigurationOptions>(options =>
{
options.Contributors.AddIfNotContains(new PlatformApplicationConfigurationContributor());
});
}
private void ConfigureAuthentication(ServiceConfigurationContext context)
{
context.Services.ForwardIdentityAuthenticationForBearer(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
context.Services.Configure<AbpClaimsPrincipalFactoryOptions>(options =>
{
options.IsDynamicClaimsEnabled = true;
});
}
private void ConfigureBundles()
{
Configure<AbpBundlingOptions>(options =>
{
options.StyleBundles.Configure(
BasicThemeBundles.Styles.Global,
bundle =>
{
bundle.AddFiles("/global-styles.css");
}
);
});
}
private void ConfigureUrls(IConfiguration configuration)
{
Configure<AppUrlOptions>(options =>
{
options.RedirectAllowedUrls.AddRange(configuration["App:RedirectAllowedUrls"]?.Split(',') ?? Array.Empty<string>());
options.Applications[PlatformConsts.React].RootUrl = configuration["App:ClientUrl"];
options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.EmailConfirmation] = "confirm";
options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.PasswordReset] = "reset-password";
options.Applications[PlatformConsts.React].Urls[PlatformConsts.Urls.UserDetail] = "admin/users/detail";
//options.Applications["MVC"].RootUrl = configuration["App:SelfUrl"];
//options.Applications["MVC"].Urls[PlatformConsts.Urls.EmailConfirmation] = "Account/Confirm";
//options.Applications["MVC"].Urls[PlatformConsts.Urls.TwoFactor] = "Account/TwoFactor";
//options.Applications["MVC"].Urls[PlatformConsts.Urls.Login] = "Account/Login";
//options.Applications["MVC"].Urls[PlatformConsts.Urls.UserDetail] = "Identity/Users/Detail";
});
}
private void ConfigureConventionalControllers()
{
Configure<AbpAspNetCoreMvcOptions>(options =>
{
options.ConventionalControllers.Create(typeof(PlatformApplicationModule).Assembly, opts =>
{
opts.TypePredicate = t => t.Namespace == "Sozsoft.Platform.ListForms.Administration";
opts.RootPath = "admin";
});
options.ConventionalControllers.Create(typeof(PlatformApplicationModule).Assembly, opts =>
{
opts.TypePredicate = t => t.Namespace != "Sozsoft.Platform.ListForms.Administration";
opts.RootPath = "app";
});
options.ConventionalControllers.Create(typeof(LanguagesApplicationModule).Assembly);
options.ConventionalControllers.Create(typeof(SettingsApplicationModule).Assembly);
options.ConventionalControllers.Create(typeof(ErpMailQueueModule).Assembly);
options.ConventionalControllers.Create(typeof(NotificationApplicationModule).Assembly);
options.ConventionalControllers.Create(typeof(SqlQueryManagerApplicationModule).Assembly);
options.ChangeControllerModelApiExplorerGroupName = false;
options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(PlatformUpdateProfileDto));
options.ConventionalControllers.FormBodyBindingIgnoredTypes.Add(typeof(UploadFileDto));
});
}
private void ConfigureCors(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddCors(options =>
{
options.AddDefaultPolicy(builder =>
{
builder
.WithOrigins(configuration["App:CorsOrigins"]?
.Split(",", StringSplitOptions.RemoveEmptyEntries)
.Select(o => o.RemovePostFix("/"))
.ToArray() ?? Array.Empty<string>())
.WithAbpExposedHeaders()
.WithExposedHeaders(["X-Correlation-Id"])
.SetIsOriginAllowedToAllowWildcardSubdomains()
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
}
private void ConfigureSwaggerServices(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddAbpSwaggerGenWithOAuth(
configuration["AuthServer:Authority"],
new Dictionary<string, string>
{
{"Platform", "Sozsoft Platform Api"}
},
options =>
{
options.SwaggerDoc("v1", new OpenApiInfo { Title = "Sozsoft Platform Api", Version = "v1" });
options.DocInclusionPredicate((docName, description) =>
{
// DevExpress reporting controller'larını Swagger'dan hariç tut
if (description.ActionDescriptor.RouteValues.TryGetValue("controller", out var controllerName))
{
if (controllerName == "CustomReportDesigner" ||
controllerName == "CustomWebDocumentViewer" ||
controllerName == "QueryBuilder")
{
return false;
}
}
return true;
});
options.CustomSchemaIds(type => type.FullName);
});
}
private void ConfigureIdentity(IConfiguration configuration)
{
//Veritabanından yonetildigi icin bunlar kapatildi
//Configure<IdentityOptions>(options =>
//{
// options.SignIn.RequireConfirmedEmail = true;
// options.SignIn.RequireConfirmedAccount = true;
// options.SignIn.RequireConfirmedPhoneNumber = false;
// options.User.RequireUniqueEmail = true;
// options.Lockout.AllowedForNewUsers = true;
// options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
// options.Lockout.MaxFailedAccessAttempts = 5;
// options.Password.RequiredLength = 6;
// options.Password.RequiredUniqueChars = 0;
// options.Password.RequireNonAlphanumeric = false;
// options.Password.RequireLowercase = false;
// options.Password.RequireUppercase = false;
// options.Password.RequireDigit = false;
//});
Configure<PlatformIdentityOptions>(configuration.GetSection(PlatformConsts.AbpIdentity.GroupName));
}
private void ConfigureCache()
{
Configure<AbpDistributedCacheOptions>(options =>
{
options.KeyPrefix = PlatformConsts.AppName;
});
Configure<RedisCacheOptions>(options =>
{
});
}
private void ConfigureHangfire(ServiceConfigurationContext context, IConfiguration configuration)
{
var connectionString = configuration.GetConnectionString(DefaultDatabaseProvider);
// Program has already selected the full application only after the database
// readiness check succeeds. Repeating the probe here creates a race: one
// transient failure skips AddHangfire while ABP still starts its Hangfire
// modules, causing JobStorage initialization to crash the host.
if (connectionString.IsNullOrWhiteSpace())
{
return;
}
Configure<AbpHangfireOptions>(options =>
{
options.ServerOptions = new BackgroundJobServerOptions
{
Queues = ["default", "platform"]
};
});
#pragma warning disable CS0162 // Unreachable code detected
if (DefaultDatabaseProvider == DatabaseProvider.PostgreSql)
{
context.Services.AddHangfire(options =>
{
options.UsePostgreSqlStorage(
storageOptions => storageOptions.UseNpgsqlConnection(connectionString),
new PostgreSqlStorageOptions
{
PrepareSchemaIfNecessary = true
});
});
}
else if (DefaultDatabaseProvider == DatabaseProvider.SqlServer)
{
context.Services.AddHangfire(options =>
{
options.UseSqlServerStorage(
connectionString,
new SqlServerStorageOptions
{
PrepareSchemaIfNecessary = true,
SchemaName = "HangFire",
CommandBatchMaxTimeout = TimeSpan.FromMinutes(5),
SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5),
QueuePollInterval = TimeSpan.Zero,
UseRecommendedIsolationLevel = true,
DisableGlobalLocks = true
});
});
}
#pragma warning restore CS0162
}
private void ConfigureBlobStoring(IConfiguration configuration)
{
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.UseFileSystem(fileSystem =>
{
fileSystem.BasePath = configuration["App:CdnPath"];
});
});
});
}
private void ConfigureAuditing()
{
Configure<AbpAspNetCoreAuditingOptions>(options =>
{
options.IgnoredUrls.Add("/api/app/list-form-customization");
});
}
private void ConfigureDynamicServices(ServiceConfigurationContext context)
{
// Dynamic AppService Background Service
context.Services.AddHostedService<DynamicAssemblyRegistrationService>();
// Roslyn Compiler
context.Services.AddSingleton<DynamicServiceCompiler>();
// Action descriptor change provider for runtime controller registration
context.Services.AddSingleton<ActionDescriptorChangeProvider>();
context.Services.AddSingleton<IActionDescriptorChangeProvider>(sp => sp.GetRequiredService<ActionDescriptorChangeProvider>());
// Custom controller activator for dynamic dependency injection
context.Services.AddSingleton<Microsoft.AspNetCore.Mvc.Controllers.IControllerActivator, DynamicControllerActivator>();
}
private void ConfigureDevExpressReporting(ServiceConfigurationContext context, IConfiguration configuration)
{
var logger = context.Services.BuildServiceProvider().GetService<ILogger<PlatformHttpApiHostModule>>();
// Configure fonts before initializing DevExpress controls
DevExpressFontConfiguration.ConfigureDefaultFonts(logger);
DevExpressFontConfiguration.InstallLinuxFonts(logger);
context.Services.AddDevExpressControls();
context.Services.ConfigureReportingServices(configurator =>
{
configurator.ConfigureReportDesigner(designerConfigurator =>
{
designerConfigurator.RegisterDataSourceWizardConfigFileConnectionStringsProvider();
});
configurator.ConfigureWebDocumentViewer(viewerConfigurator =>
{
viewerConfigurator.UseCachedReportSourceBuilder();
});
});
// Register report storage extension
context.Services.AddScoped<ReportStorageWebExtension, CustomReportStorageWebExtension>();
}
public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
// Setup delegate for dynamic service registration
DynamicServiceCompiler.NotifyAssemblyRegistration = DynamicAssemblyRegistrationService.RequestAssemblyRegistration;
// Setup delegate for dynamic service unregistration
DynamicServiceCompiler.NotifyAssemblyUnregistration = DynamicAssemblyRegistrationService.RequestAssemblyUnregistration;
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAbpRequestLocalization();
// Auth hataları MVC tarafından /Error sayfasına yönlendirilmesin diye bu kaldırıldı
// if (!env.IsDevelopment())
// {
// app.UseErrorPage();
// }
app.UseCorrelationId();
app.UseResponseCompression();
app.MapAbpStaticAssets();
app.UseRouting();
app.UseCors();
// Not: /api/setup/application-status artık SetupController tarafından karşılanıyor.
// Buradaki kopya middleware kaldırıldı — durum mantığı tek yerde (DatabaseReadinessChecker).
// Veritabanı erişilemezken uygulama konfigürasyonu üretilemez. İsteği burada,
// EF sorguları ve bağlantı zaman aşımları devreye girmeden kesiyoruz: aksi halde
// istemci 10+ saniye bekleyip, contributor'ın içinde patlayan bir bağlantı
// hatasından türeyen belirsiz bir yanıt alıyordu. Kontrol cache'li, maliyeti yok.
app.Use(async (httpContext, next) =>
{
if (httpContext.Request.Path.StartsWithSegments("/api/abp/application-configuration"))
{
var configuration = httpContext.RequestServices.GetRequiredService<IConfiguration>();
if (!DatabaseReadinessChecker.GetStatus(configuration).IsReady)
{
httpContext.Response.StatusCode = StatusCodes.Status503ServiceUnavailable;
httpContext.Response.ContentType = "text/plain; charset=utf-8";
await httpContext.Response.WriteAsync("The database is not reachable.");
return;
}
}
await next();
});
app.Use(async (context, next) =>
{
// SignalR tarayici istemcisi WebSocket disindaki tasimalarda Authorization basligi
// gonderemez; token sorgu dizesinden gelir ve baslik olarak yazilir.
if ((context.Request.Path.StartsWithSegments("/messengerhub") ||
context.Request.Path.StartsWithSegments("/notificationhub")) &&
context.Request.Query.TryGetValue("access_token", out var accessToken) &&
!string.IsNullOrWhiteSpace(accessToken.ToString()))
{
context.Request.Headers.Authorization = $"Bearer {accessToken}";
}
await next();
});
app.UseMiddleware<McpAuthenticationChallengeMiddleware>();
app.UseAuthentication();
app.UseAbpOpenIddictValidation();
app.UseMiddleware<CaptchaMiddleware>();
if (PlatformConsts.IsMultiTenant)
{
app.UseMultiTenancy();
app.UseMiddleware<TenantLocalizationMiddleware>();
}
app.UseUnitOfWork();
app.UseMiddleware<PlatformSessionValidationMiddleware>();
app.UseDynamicClaims();
app.UseAuthorization();
app.UseSwagger();
app.UseAbpSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Sozsoft Platform Api");
var configuration = context.ServiceProvider.GetRequiredService<IConfiguration>();
c.OAuthClientId(configuration["AuthServer:SwaggerClientId"]);
c.OAuthScopes("Platform");
});
app.UseAuditing();
app.UseAbpSerilogEnrichers();
if (env.IsDevelopment())
{
app.UseHangfireDashboard();
}
else
{
app.UseHangfireDashboard("/hangfire", new DashboardOptions
{
AsyncAuthorization = [new AbpHangfireAuthorizationFilter()]
});
}
app.UseConfiguredEndpoints(endpoints =>
{
endpoints.MapHub<MessengerHub>("/messengerhub");
endpoints.MapHub<NotificationHub>("/notificationhub");
// MCP (Model Context Protocol) ucu. Kimlik dogrulama, kiraci cozumu ve unit of work
// yukaridaki ardisik duzenden gelir. Uc yalnizca kimlik ister; Invoke her MCP
// servisinin sinif duzeyindeki [Authorize] attribute'unda aranir. Uca konsaydi yetkisiz
// kullanici nedeni belirtilmeyen bir 403 alirdi; servis duzeyinde reddedildiginde
// istemci eksik yetkiyi adiyla gorur (McpToolErrorTranslator). Sema acikca bearer'a
// baglanir: varsayilan cerez semasi token'siz istegi giris sayfasina 302 ile
// yonlendirir, MCP istemcisi 401 bekler.
endpoints.MapMcp("/mcp").RequireAuthorization(new AuthorizeAttribute
{
AuthenticationSchemes = OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme
});
});
}
}