using System.Text; using PartnerSL.Middleware; 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}"); 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(); }); #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(); builder.Services.Configure(options => { options.AllowSynchronousIO = true; }); builder.Services.AddResponseCompression(o => { o.EnableForHttps = true; o.Providers.Add(); }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Enterprise AI engine (AI-Enterprise-v1.0) link — shared by EAIAdmin and KMS modules (both // composed into this host). IVaultService/IJwtAccessTokenIssuer are already registered by // EntitlementModule.Register() below (idempotent-safe if registered twice), so only the // transport client + named HttpClient are added here, once, centrally. builder.Services.AddScoped(); builder.Services.AddHttpClient("ai-enterprise", client => client.Timeout = TimeSpan.FromSeconds(120)); builder.Services.Configure(builder.Configuration.GetSection("Gb5SystemDTO")); builder.Services.AddGB5Telemetry(builder.Configuration, "GB5-PLATFORM"); // Module discovery + registration - fully dynamic, no hardcoded module list. Every module DLL // referenced by PlatformHost.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 PlatformHost down. To add a module to this host: add a to // PlatformHost.csproj. Nothing below needs to change. // (JobEngineSL/IDMSSL still have no ProjectReference at all - see PlatformHost.csproj - so they // stay excluded at the build level regardless of this loader.) using var startupLoggerFactory = LoggerFactory.Create(b => b.AddConsole()); var startupLogger = startupLoggerFactory.CreateLogger("PlatformHost.ModuleLoader"); var moduleLoad = ModuleLoader.LoadAll(builder.Services, builder.Configuration, startupLogger, "GB5Shared"); builder.Services.AddFastEndpoints(o => { o.Assemblies = moduleLoad.FastEndpointsAssemblies; o.Filter = moduleLoad.EndpointFilter; }); // 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 Platform API"); #region ── Multi-tenant Keycloak JWT Bearer (GB5 Repo-Wide Authentication Hardening) ── // Additive alongside EntitlementModule's own ClientJwtBearer/PartnerM2MJwtBearer named schemes // (registered above via ModuleLoader.LoadAll) — ASP.NET Core supports multiple schemes with one // default concurrently, no conflict. This call runs after Entitlement's own AddAuthentication() // call, so it becomes the effective DEFAULT scheme (the last-registered Configure // delegate wins) — the two named schemes remain reachable exactly as before via their own // [Authorize(AuthenticationSchemes = "...")] usage, unaffected by which scheme is "default". // UseAuthentication()/UseAuthorization() already exist in this host's middleware pipeline below // (added for Entitlement) — no middleware change needed here, just the new scheme + its services. // 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(); app.UseForwardedHeaders(); app.UseRouting(); // explicit — before FastEndpoints/SignalR hub routing app.UseResponseCompression(); app.UseAuthorization(); app.UseCloudEvents(); app.UseMiddleware(); app.UseMiddleware(); app.UseWhen( ctx => ctx.Request.Path.StartsWithSegments("/partner"), branch => branch.UseMiddleware()); // gbEAI (kms_discovery_adapter.py) → GB5 machine-to-machine callback auth — exact-path // allowlist, never a prefix, so this never touches the browser-session-driven KmDiscovery // endpoints (GetExtractionJobs, TriggerExtractionJob, AcceptCandidate, ...). var kmsM2MPaths = new HashSet(StringComparer.OrdinalIgnoreCase) { "/KmDiscovery/SaveCandidate", "/KmDiscovery/UpdateJobStatus" }; app.UseWhen( ctx => kmsM2MPaths.Contains(ctx.Request.Path.Value ?? ""), branch => branch.UseMiddleware()); // gbEAI (prompt_resolver.py) → GB5 machine-to-machine callback auth — the first GB5 endpoint // gbEAI itself calls (every other integration so far is GB5 calling gbEAI). Same exact-path // allowlist discipline as the KMS block above. var eaiAdminM2MPaths = new HashSet(StringComparer.OrdinalIgnoreCase) { "/EAIAdmin/ResolveContext" }; app.UseWhen( ctx => eaiAdminM2MPaths.Contains(ctx.Request.Path.Value ?? ""), branch => branch.UseMiddleware()); // Required for EntitlementModule's ClientJwtBearer/PartnerM2MJwtBearer schemes — every other // currently-wired module's endpoints are AllowAnonymous() and gated by their own custom mechanism // (SqlWorkbench's [MenuRights], Partner's ApiKeyAuthMiddleware above), so this host never needed // ASP.NET Core's own authentication/authorization pipeline before. Must run before // UseFastEndpoints — Roles()/AuthSchemes() need HttpContext.User populated by request time. app.UseAuthentication(); app.UseAuthorization(); 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, "Platform"); app.MapGet("/", () => $"Hello from GB5 Platform Host (.NET 9) - {moduleLoad.BannerText()}"); app.Run();