125 lines
5.5 KiB
C#
125 lines
5.5 KiB
C#
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();
|