Files
idoklad/Program.cs
T
JiriUhlir b01c09006a swagger: advertise PathBase as OpenAPI server so Try-it-out targets the proxy prefix
Swagger UI was calling the host root (services.csbot.cz/contacts) instead of
services.csbot.cz/apps/idoklad/contacts. Emit servers=[{url: PathBase}] so the
same base that /docs runs under is prefixed to the API requests too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 08:20:14 +02:00

115 lines
4.7 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 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<RequestCredentialsProvider>();
builder.Services.AddScoped<DokladApiFactory>();
builder.Services.AddScoped<IdokladApiAccessor>();
// Agenda services.
builder.Services.AddScoped<ContactsService>();
builder.Services.AddScoped<IssuedInvoicesService>();
builder.Services.AddScoped<ReceivedInvoicesService>();
builder.Services.AddScoped<RegistersService>();
builder.Services.AddScoped<AccountService>();
builder.Services.AddScoped<CodeListsService>();
builder.Services.AddScoped<SalesDocumentsService>();
builder.Services.AddScoped<PurchaseCashService>();
builder.Services.AddScoped<PaymentsService>();
builder.Services.AddScoped<CatalogService>();
builder.Services.AddScoped<IntegrationService>();
builder.Services.AddScoped<StatisticsService>();
// 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\n" +
"- `X-ClientSecret` — iDoklad OAuth2 ClientSecret (sensitive; TLS only)\n" +
"- `X-ApplicationId` — iDoklad ApplicationId from the developer portal\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. " +
"If neither a header nor a default is available, the request is rejected with 401.",
});
options.OperationFilter<CredentialHeadersOperationFilter>();
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();
if (!string.IsNullOrWhiteSpace(settings.RootPath))
{
// UsePathBase requires a leading slash; tolerate ROOT_PATH configured without one.
var basePath = settings.RootPath.StartsWith('/') ? settings.RootPath : "/" + settings.RootPath;
app.UsePathBase(basePath);
}
app.UseMiddleware<ExceptionHandlingMiddleware>();
// 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 path base (ROOT_PATH).
app.UseSwagger(options =>
{
options.RouteTemplate = "docs/{documentName}/swagger.json";
// Advertise the request path base (e.g. /apps/idoklad behind the portal proxy) as the
// OpenAPI server so Swagger UI "Try it out" targets {pathBase}/contacts, not the host root.
options.PreSerializeFilters.Add((swaggerDoc, httpReq) =>
{
var basePath = httpReq.PathBase.HasValue ? httpReq.PathBase.Value : "/";
swaggerDoc.Servers = new List<OpenApiServer> { new() { Url = basePath } };
});
});
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();