using System.Text; using FastEndpoints; using FastEndpoints.Swagger; using GB5Shared.Auth.Jwt; using GB5Shared.Connection; using GB5Shared.DateConverter; using static GB5Shared.DateConverter.GB5JsonOptions; using GB5Shared.DTO.Framework.CommonConfig; using GB5Shared.GB5CommonFunction; using GB5Shared.GenerateAutoNumber; using GB5Shared.Hosting; using GB5Shared.Middleware; using GB5Shared.QueryExecutor; using GB5Shared.Swagger; using GB5Shared.Telemetry; using GB5Shared.Validation; using Microsoft.AspNetCore.HttpOverrides; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.ResponseCompression; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.Extensions.Caching.Hybrid; using Microsoft.Extensions.Logging; Console.OutputEncoding = Encoding.UTF8; GB5DapperTypeHandlers.Register(); var builder = WebApplication.CreateBuilder(args); var appPort = builder.Configuration.GetValue("AppPort"); builder.WebHost.UseUrls($"http://0.0.0.0:{appPort}"); // Dapr & Controllers builder.Services.AddHttpContextAccessor(); builder.Services.AddDaprClient(); builder.Services.AddControllers() .AddDapr() .AddJsonOptions(options => { options.JsonSerializerOptions.PropertyNamingPolicy = null; options.JsonSerializerOptions.DictionaryKeyPolicy = null; options.JsonSerializerOptions.AddGB5Converters(); }); builder.Services.Configure(options => { options.SuppressModelStateInvalidFilter = true; }); builder.Services.ConfigureHttpJsonOptions(options => { options.SerializerOptions.PropertyNamingPolicy = null; options.SerializerOptions.DictionaryKeyPolicy = null; options.SerializerOptions.AddGB5Converters(); }); // Caching #pragma warning disable EXTEXP0018 builder.Services.AddHybridCache(options => { options.DefaultEntryOptions = new HybridCacheEntryOptions(); options.DisableCompression = false; }); #pragma warning restore EXTEXP0018 builder.Services.AddMemoryCache(); builder.Services.AddDistributedMemoryCache(); // CORS // Kestrel builder.Services.Configure(options => { options.AllowSynchronousIO = true; }); builder.Services.AddResponseCompression(o => { o.EnableForHttps = true; o.Providers.Add(); }); // Common services shared across all Engagement modules builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Shared configuration binding builder.Services.Configure(builder.Configuration.GetSection("Gb5SystemDTO")); // OpenTelemetry - single service name for the consolidated host builder.Services.AddGB5Telemetry(builder.Configuration, "GB5-ENGAGEMENT"); // Module discovery + registration - fully dynamic, no hardcoded module list. Every module DLL // referenced by EngagementHost.csproj is discovered from the published output folder, registered, // and safety-checked here. A module that throws during Register() (e.g. missing config) or whose // endpoints have an unresolvable DI dependency is logged and excluded - it never takes the rest // of EngagementHost down. To add a module to this host: add a to // EngagementHost.csproj. Nothing below needs to change. // (OKRSL still has no ProjectReference at all - see EngagementHost.csproj - so it stays excluded // at the build level regardless of this loader.) using var startupLoggerFactory = LoggerFactory.Create(b => b.AddConsole()); var startupLogger = startupLoggerFactory.CreateLogger("EngagementHost.ModuleLoader"); var moduleLoad = ModuleLoader.LoadAll(builder.Services, builder.Configuration, startupLogger, "GB5Shared"); builder.Services.AddFastEndpoints(o => { o.Assemblies = moduleLoad.FastEndpointsAssemblies; o.Filter = moduleLoad.EndpointFilter; }); // A few Dapr [Topic] subscriber routes use a business-domain alias rather than the module's // display name as their first path segment. var pathAliases = new Dictionary(StringComparer.OrdinalIgnoreCase) { ["Competency"] = "CompetencyMgmt", ["PerfMgmt"] = "PERM", }; // Swagger - one native document per module bundled into this Host, instead of a single combined // document covering all of them - see GB5Shared.Swagger.ModuleSwaggerRegistration. builder.Services.AddPerModuleSwaggerDocuments(moduleLoad, "GB5 Engagement API", pathAliases); #region ── Multi-tenant Keycloak JWT Bearer (GB5 Repo-Wide Authentication Hardening) ── // Additive only — this host had NO AddAuthentication()/UseAuthentication() at all before this. // Zero behavior change for every existing caller: AllowAnonymous() endpoints (still the default // everywhere) ignore auth schemes entirely, and BaseEndPoint.GetLoginDTOFromRequestAsync's // dual-mode bridge falls straight through to today's trusted-header path whenever no valid // Keycloak token is presented — which is every request until a tenant's MSERVERCONFIG.AUTHMODE is // deliberately set to Keycloak. Same "oidc" named client / MultiTenantOidcJwksCache / // GB5Shared.Auth.Jwt.KeycloakLoginDTOResolver pattern already proven live in FrameworkSL — see // that Program.cs for the fuller design comment. builder.Services.AddHttpClient("oidc") .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator }); builder.Services.AddGB5MultiTenantJwtBearerSupport(); builder.Services.AddScoped(); builder.Services.AddAuthentication(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, }; options.Events = GB5Shared.Auth.Jwt.MultiTenantJwtBearerEvents.Build(); }); #endregion var app = builder.Build(); app.UseGatewayPrefixForwarding(); // Middleware pipeline // UseRouting() explicit: CollabSL and FBCKSL use SignalR hubs - CORS must apply after routing app.UseForwardedHeaders(); app.UseRouting(); app.UseResponseCompression(); app.UseAuthentication(); app.UseAuthorization(); app.UseCloudEvents(); app.UseMiddleware(); app.UseMiddleware(); app.UseFastEndpoints(c => { c.Serializer.Options.PropertyNamingPolicy = null; c.Serializer.Options.DictionaryKeyPolicy = null; c.Serializer.Options.AddGB5Converters(); }); moduleLoad.MapEndpoints(app, app.Logger); app.MapControllers(); app.MapSubscribeHandler(); // Swagger UI - one page per module; /GB5Documentation redirects to the requesting gateway alias's // own module page (see GB5Shared.Swagger.ModuleSwaggerRegistration). app.MapPerModuleSwaggerUi(moduleLoad); moduleLoad.MapModuleHealth(app, "Engagement"); app.MapGet("/", () => $"Hello from GB5 Engagement Host (.NET 9) - {moduleLoad.BannerText()}"); app.Run();