Files
idoklad/Program.cs
T
2026-07-14 06:21:37 +02:00

125 lines
5.5 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>();
builder.Services.AddScoped<ReportsService>();
builder.Services.AddScoped<MailService>();
// 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<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();
// 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<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 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<OpenApiServer> { 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();