using Microsoft.OpenApi.Models; using Newtonsoft.Json; using Idoklad.Client; using Idoklad.Configuration; using Idoklad.Credentials; using Idoklad.Infrastructure; using Idoklad.Services; var builder = WebApplication.CreateBuilder(args); // Configuration resolved from environment variables (single shared instance). var settings = new IdokladSettings(); builder.Services.AddSingleton(settings); // HttpClient for the iDoklad SDK, managed by IHttpClientFactory (recommended SDK usage). builder.Services.AddHttpClient(DokladApiFactory.HttpClientName, client => { client.Timeout = TimeSpan.FromSeconds(settings.RequestTimeoutSeconds); }); builder.Services.AddHttpContextAccessor(); // Credential resolution + SDK client wiring. builder.Services.AddScoped(); 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.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); // Use Newtonsoft.Json so request/response binding matches the iDoklad SDK model attributes. builder.Services .AddControllers() .AddNewtonsoftJson(options => { options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo { Title = settings.AppName, Version = settings.AppVersion, Description = "REST integration with iDoklad built on the official IdokladSdk (.NET) 5.3.0, " + "using the OAuth2 client credentials flow.\n\n" + "**Credentials.** Every agenda endpoint needs an iDoklad ClientId, ClientSecret and ApplicationId. " + "Sensitive values are required in request headers and are never accepted in the query string or body:\n\n" + "- `X-ClientId` — iDoklad OAuth2 ClientId (required)\n" + "- `X-ClientSecret` — iDoklad OAuth2 ClientSecret (required; sensitive; TLS only)\n" + "- `X-ApplicationId` — OPTIONAL ApplicationId (GUID) for partner apps only; leave empty for a standard app\n" + "- `X-Idoklad-Language` — optional response language (Cz, Sk, En)\n\n" + "If a header is omitted, the matching environment default " + "(`IDOKLAD_CLIENT_ID`, `IDOKLAD_CLIENT_SECRET`, `IDOKLAD_APPLICATION_ID`) is used. " + "Only ClientId and ClientSecret are required; if either is missing the request is rejected with 401. " + "When no ApplicationId is provided, authentication uses client_id + client_secret only.", }); 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/idoklad). 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('/'); // UsePathBase is a no-op under handle_path (the request no longer carries the prefix) but keeps the // app correct behind a proxy that forwards the prefix intact, so routes still resolve in that case. 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/idoklad behind the portal proxy) as the OpenAPI // server so Swagger UI "Try it out" targets {prefix}/contacts, not the host root. The prefix // comes from ROOT_PATH because handle_path has already stripped it from the request, leaving // httpReq.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 => { // Interactive docs at /docs (matching the sibling microsoft-365-service). options.RoutePrefix = "docs"; // Relative endpoint — resolves to {pathBase}/docs/v1/swagger.json in the browser. options.SwaggerEndpoint("v1/swagger.json", $"{settings.AppName} v1"); options.DocumentTitle = $"{settings.AppName} – API docs"; }); app.MapControllers(); app.Run();