using System.Text.Json.Serialization; using Microsoft.OpenApi.Models; using Csob.Client; using Csob.Configuration; using Csob.Credentials; using Csob.Infrastructure; using Csob.Services; var builder = WebApplication.CreateBuilder(args); // Configuration resolved from environment variables (non-secret infra config only). var settings = new CsobSettings(); builder.Services.AddSingleton(settings); // The eIDAS client certificate is supplied per request as a Base64 PFX header, which is large. // Raise Kestrel's header size limit so the certificate header is not rejected. builder.WebHost.ConfigureKestrel(options => { options.Limits.MaxRequestHeadersTotalSize = 1024 * 1024; // 1 MiB }); builder.Services.AddHttpContextAccessor(); // Builds/caches mutual-TLS HttpClients by certificate thumbprint; shared across requests. builder.Services.AddSingleton(); // Per-request credential resolution and API access. builder.Services.AddScoped(); builder.Services.AddScoped(); // Agenda services. builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services .AddControllers() .AddJsonOptions(options => { // camelCase (web defaults) matches the COBS JSON contract; omit null properties on write. options.JsonSerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = settings.AppName, Version = settings.AppVersion, Description = "Multi-tenant REST integration with the ČSOB PSD2 (Open Banking) API, following the " + "Czech Open Banking Standard (COBS). Covers AISP (account information), PISP (payment, " + "standing-order and direct-debit initiation with the sign/SCA flow), consents and the " + "OAuth2 Authorization Code helper.\n\n" + "**Credentials.** This service stores no secrets — every sensitive value is supplied per " + "request in a header and forwarded to ČSOB over TLS (never the query string or body):\n\n" + "- `X-CSOB-Certificate` — eIDAS client certificate (QWAC) as a Base64 PFX (mutual TLS). Required.\n" + "- `X-CSOB-Certificate-Password` — PFX passphrase (optional).\n" + "- `X-Access-Token` — OAuth2 Bearer access token for the PSU (AISP/PISP/consents). Required.\n" + "- `X-API-Key` — ČSOB application API key (sent upstream as `APIKEY`). Required.\n" + "- `X-TPP-Name` — TPP organisation name (sent upstream as `TPP-Name`). Required.\n" + "- `X-CSOB-Client-Id` / `X-CSOB-Client-Secret` — OAuth2 app credentials (OAuth endpoints only).\n\n" + "Obtain the access token via the `/oauth/*` endpoints (Authorization Code flow with PSU redirect).", }); options.OperationFilter(); var xmlPath = Path.Combine(AppContext.BaseDirectory, $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml"); if (File.Exists(xmlPath)) { options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true); } }); var app = builder.Build(); // ROOT_PATH carries the public reverse-proxy prefix (e.g. /apps/csob). AppFactory's Caddy uses // handle_path, which strips that prefix before the request reaches the container, so PathBase ends // up empty and the OpenAPI server URL must come from ROOT_PATH directly (see the filter below). var publicPrefix = string.IsNullOrWhiteSpace(settings.RootPath) ? null : "/" + settings.RootPath.Trim('/'); if (publicPrefix is not null) { app.UsePathBase(publicPrefix); } app.UseMiddleware(); // Serve the OpenAPI document under the same /docs prefix as the UI so a relative endpoint resolves // correctly both locally and behind a reverse-proxy prefix (ROOT_PATH). app.UseSwagger(options => { options.RouteTemplate = "docs/{documentName}/swagger.json"; // Advertise the public prefix (e.g. /apps/csob) as the OpenAPI server so Swagger UI "Try it out" // targets {prefix}/accounts, not the host root. The prefix comes from ROOT_PATH because // handle_path has already stripped it from the request, leaving PathBase empty. Fall back to // PathBase (a proxy that keeps the prefix) and finally "/". options.PreSerializeFilters.Add((swaggerDoc, httpReq) => { var serverUrl = publicPrefix ?? (httpReq.PathBase.HasValue ? httpReq.PathBase.Value : "/"); swaggerDoc.Servers = new List { new() { Url = serverUrl } }; }); }); app.UseSwaggerUI(options => { options.RoutePrefix = "docs"; options.SwaggerEndpoint("v1/swagger.json", $"{settings.AppName} v1"); options.DocumentTitle = $"{settings.AppName} – API docs"; }); app.MapControllers(); app.Run();