diff --git a/Client/CsobApiAccessor.cs b/Client/CsobApiAccessor.cs new file mode 100644 index 0000000..88cb3e0 --- /dev/null +++ b/Client/CsobApiAccessor.cs @@ -0,0 +1,42 @@ +using Csob.Configuration; +using Csob.Credentials; + +namespace Csob.Client; + +/// +/// Scoped accessor that resolves the credentials for the current request and lazily builds a +/// single (bound to the request's mutual-TLS client) shared by all +/// services handling that request. +/// +public sealed class CsobApiAccessor +{ + private readonly RequestCredentialsProvider _credentialsProvider; + private readonly CsobHttpClientProvider _httpClientProvider; + private readonly CsobSettings _settings; + private CsobApiClient? _client; + + public CsobApiAccessor( + RequestCredentialsProvider credentialsProvider, + CsobHttpClientProvider httpClientProvider, + CsobSettings settings) + { + _credentialsProvider = credentialsProvider; + _httpClientProvider = httpClientProvider; + _settings = settings; + } + + public CsobApiClient Client + { + get + { + if (_client is null) + { + var credentials = _credentialsProvider.Resolve(); + var http = _httpClientProvider.GetClient(credentials.Certificate!); + _client = new CsobApiClient(http, credentials, _settings); + } + + return _client; + } + } +} diff --git a/Client/CsobApiClient.cs b/Client/CsobApiClient.cs new file mode 100644 index 0000000..3587c93 --- /dev/null +++ b/Client/CsobApiClient.cs @@ -0,0 +1,130 @@ +using System.Net.Http.Headers; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using Csob.Configuration; +using Csob.Credentials; + +namespace Csob.Client; + +/// +/// Thin HTTP wrapper around the ČSOB PSD2 resource API for a single request. It attaches the +/// mandatory COBS headers (Authorization, APIKEY, TPP-Name, X-Request-ID, +/// Date, User-Involved), sends the call over the per-request mutual-TLS client and +/// returns the response JSON verbatim () so no field is lost in translation. +/// Non-success responses become a . +/// +public sealed class CsobApiClient +{ + /// Web defaults (camelCase) match the COBS JSON contract; null properties are omitted on write. + public static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web) + { + DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull, + }; + + private readonly HttpClient _http; + private readonly CsobCredentials _credentials; + private readonly string _baseUrl; + + public CsobApiClient(HttpClient http, CsobCredentials credentials, CsobSettings settings) + { + _http = http; + _credentials = credentials; + _baseUrl = settings.ApiBaseUrl.TrimEnd('/'); + } + + public Task GetAsync(string path, IReadOnlyDictionary? query, CancellationToken ct) + => SendAsync(HttpMethod.Get, path, query, body: null, ct); + + public Task PostAsync(string path, object? body, CancellationToken ct) + => SendAsync(HttpMethod.Post, path, query: null, body, ct); + + public Task PutAsync(string path, object? body, CancellationToken ct) + => SendAsync(HttpMethod.Put, path, query: null, body, ct); + + public Task DeleteAsync(string path, CancellationToken ct) + => SendAsync(HttpMethod.Delete, path, query: null, body: null, ct); + + private async Task SendAsync( + HttpMethod method, string path, IReadOnlyDictionary? query, object? body, CancellationToken ct) + { + using var request = new HttpRequestMessage(method, _baseUrl + path + BuildQuery(query)); + ApplyHeaders(request); + + if (body is not null) + { + var json = body is JsonNode node ? node.ToJsonString(JsonOptions) : JsonSerializer.Serialize(body, JsonOptions); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + } + + using var response = await _http.SendAsync(request, ct); + var payload = await response.Content.ReadAsStringAsync(ct); + + if (!response.IsSuccessStatusCode) + { + throw new CsobApiException(response.StatusCode, ParseErrorCodes(payload), payload); + } + + return string.IsNullOrWhiteSpace(payload) ? null : JsonNode.Parse(payload); + } + + private void ApplyHeaders(HttpRequestMessage request) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _credentials.AccessToken); + request.Headers.Date = DateTimeOffset.UtcNow; + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + request.Headers.TryAddWithoutValidation("APIKEY", _credentials.ApiKey); + request.Headers.TryAddWithoutValidation("TPP-Name", _credentials.TppName); + request.Headers.TryAddWithoutValidation("X-Request-ID", Guid.NewGuid().ToString()); + request.Headers.TryAddWithoutValidation("User-Involved", _credentials.UserInvolved ? "true" : "false"); + + if (!string.IsNullOrWhiteSpace(_credentials.UserIpAddress)) + { + request.Headers.TryAddWithoutValidation("User-IP-Address", _credentials.UserIpAddress); + } + } + + private static string BuildQuery(IReadOnlyDictionary? query) + { + if (query is null) + { + return string.Empty; + } + + var parts = query + .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value)) + .Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value!)}") + .ToArray(); + + return parts.Length == 0 ? string.Empty : "?" + string.Join("&", parts); + } + + /// Best-effort parse of the COBS error shape { "errors": [ { "error": "CODE" } ] }. + private static IReadOnlyList ParseErrorCodes(string payload) + { + if (string.IsNullOrWhiteSpace(payload)) + { + return Array.Empty(); + } + + try + { + var node = JsonNode.Parse(payload); + if (node?["errors"] is JsonArray errors) + { + return errors + .Select(e => e?["error"]?.GetValue()) + .Where(code => !string.IsNullOrWhiteSpace(code)) + .Select(code => code!) + .ToArray(); + } + } + catch (JsonException) + { + // Upstream returned a non-JSON error body; the raw payload is still carried on the exception. + } + + return Array.Empty(); + } +} diff --git a/Client/CsobApiException.cs b/Client/CsobApiException.cs new file mode 100644 index 0000000..89253de --- /dev/null +++ b/Client/CsobApiException.cs @@ -0,0 +1,26 @@ +using System.Net; + +namespace Csob.Client; + +/// +/// Raised when the ČSOB PSD2 API returns a non-success HTTP status. Carries the upstream status +/// and the raw error body so the middleware can surface it without leaking credentials. +/// +public sealed class CsobApiException : Exception +{ + public HttpStatusCode StatusCode { get; } + + /// Machine-readable error codes parsed from the ČSOB errors[].error array (best effort). + public IReadOnlyList ErrorCodes { get; } + + /// Raw response body (already credential-free — it is the upstream's own error payload). + public string? RawBody { get; } + + public CsobApiException(HttpStatusCode statusCode, IReadOnlyList errorCodes, string? rawBody) + : base($"ČSOB PSD2 API returned {(int)statusCode} ({statusCode}).") + { + StatusCode = statusCode; + ErrorCodes = errorCodes; + RawBody = rawBody; + } +} diff --git a/Client/CsobApiPaths.cs b/Client/CsobApiPaths.cs new file mode 100644 index 0000000..abca975 --- /dev/null +++ b/Client/CsobApiPaths.cs @@ -0,0 +1,44 @@ +namespace Csob.Client; + +/// +/// Central registry of ČSOB PSD2 resource path templates (relative to CSOB_API_BASE_URL). +/// +/// Paths follow the Czech Open Banking Standard (COBS) as implemented by ČSOB. The account-scoped +/// AISP paths and the PISP /my/payments/.../sign/{signId} authorization flow are confirmed +/// against the ČSOB developer portal; the remaining COBS resources use the same /my/ prefix. +/// Keep every path here so a portal-specific correction is a single-file change. +/// +public static class CsobApiPaths +{ + // ----- AISP (Account Information) ----- + public const string Accounts = "/my/accounts"; + public static string Account(string id) => $"/my/accounts/{Esc(id)}"; + public static string Balance(string accountId) => $"/my/accounts/{Esc(accountId)}/balance"; + public static string Transactions(string accountId) => $"/my/accounts/{Esc(accountId)}/transactions"; + public static string AwaitingTransactions(string accountId) => $"/my/accounts/{Esc(accountId)}/transactions/awaiting"; + public static string AccountStandingOrders(string accountId) => $"/my/accounts/{Esc(accountId)}/standingorders"; + public static string AccountStandingOrder(string accountId, string standingOrderId) => $"/my/accounts/{Esc(accountId)}/standingorders/{Esc(standingOrderId)}"; + public static string AccountDirectDebits(string accountId) => $"/my/accounts/{Esc(accountId)}/directdebits"; + + // ----- PISP (Payment Initiation) ----- + public const string Payments = "/my/payments"; + public static string Payment(string id) => $"/my/payments/{Esc(id)}"; + public static string PaymentStatus(string id) => $"/my/payments/{Esc(id)}/status"; + public static string PaymentSign(string id, string signId) => $"/my/payments/{Esc(id)}/sign/{Esc(signId)}"; + + public const string StandingOrders = "/my/standingorders"; + public static string StandingOrder(string id) => $"/my/standingorders/{Esc(id)}"; + public static string StandingOrderStatus(string id) => $"/my/standingorders/{Esc(id)}/status"; + public static string StandingOrderSign(string id, string signId) => $"/my/standingorders/{Esc(id)}/sign/{Esc(signId)}"; + + public const string DirectDebits = "/my/directdebits"; + public static string DirectDebit(string id) => $"/my/directdebits/{Esc(id)}"; + public static string DirectDebitStatus(string id) => $"/my/directdebits/{Esc(id)}/status"; + public static string DirectDebitSign(string id, string signId) => $"/my/directdebits/{Esc(id)}/sign/{Esc(signId)}"; + + // ----- Common (Consents) ----- + public const string Consents = "/consents"; + public static string Consent(string id) => $"/consents/{Esc(id)}"; + + private static string Esc(string segment) => Uri.EscapeDataString(segment); +} diff --git a/Client/CsobHttpClientProvider.cs b/Client/CsobHttpClientProvider.cs new file mode 100644 index 0000000..aeba1f8 --- /dev/null +++ b/Client/CsobHttpClientProvider.cs @@ -0,0 +1,77 @@ +using System.Collections.Concurrent; +using System.Security.Cryptography.X509Certificates; +using Csob.Configuration; + +namespace Csob.Client; + +/// +/// Provides instances configured for mutual TLS with a per-request eIDAS +/// client certificate. ČSOB requires the client certificate at the transport layer, so a single +/// shared client cannot be used across tenants. +/// +/// Clients are cached by certificate thumbprint and reused for connection pooling. In practice the +/// certificate identifies the TPP application (not the PSU), so the number of distinct certificates +/// is small and bounded by the set of calling tenants. Certificates and clients live in memory only +/// and never touch disk. +/// +public sealed class CsobHttpClientProvider : IDisposable +{ + private readonly CsobSettings _settings; + private readonly ConcurrentDictionary _clients = new(); + private readonly object _buildLock = new(); + + public CsobHttpClientProvider(CsobSettings settings) => _settings = settings; + + /// + /// Returns a (cached) mutual-TLS presenting . + /// A fresh certificate instance is built per request; if an equivalent one (same thumbprint) is + /// already cached, the redundant instance is disposed so it does not leak. + /// + public HttpClient GetClient(X509Certificate2 certificate) + { + var thumbprint = certificate.Thumbprint; + + if (_clients.TryGetValue(thumbprint, out var existing)) + { + certificate.Dispose(); + return existing; + } + + lock (_buildLock) + { + if (_clients.TryGetValue(thumbprint, out existing)) + { + certificate.Dispose(); + return existing; + } + + var client = BuildClient(certificate); + _clients[thumbprint] = client; + return client; + } + } + + private HttpClient BuildClient(X509Certificate2 certificate) + { + var handler = new SocketsHttpHandler + { + PooledConnectionLifetime = TimeSpan.FromMinutes(10), + }; + handler.SslOptions.ClientCertificates = new X509CertificateCollection { certificate }; + + return new HttpClient(handler, disposeHandler: true) + { + Timeout = TimeSpan.FromSeconds(_settings.RequestTimeoutSeconds), + }; + } + + public void Dispose() + { + foreach (var client in _clients.Values) + { + client.Dispose(); + } + + _clients.Clear(); + } +} diff --git a/Configuration/CsobSettings.cs b/Configuration/CsobSettings.cs new file mode 100644 index 0000000..6a4f684 --- /dev/null +++ b/Configuration/CsobSettings.cs @@ -0,0 +1,54 @@ +namespace Csob.Configuration; + +/// +/// Service configuration resolved from environment variables. +/// +/// This service is a stateless, multi-tenant proxy in front of the ČSOB PSD2 (Open Banking) +/// API. Unlike the sibling iDoklad service, it holds no per-client secrets: the eIDAS +/// client certificate, OAuth client id/secret, access token, API key and TPP name are all +/// supplied per request as HTTP headers by the calling client service +/// (see ). Only non-secret infrastructure +/// configuration (API/OAuth base URLs, app metadata, reverse-proxy prefix, timeout) lives here. +/// +public sealed class CsobSettings +{ + public string AppName { get; init; } = GetEnv("APP_NAME", "ČSOB PSD2 Service"); + public string AppVersion { get; init; } = GetEnv("APP_VERSION", "1.0.0"); + + /// Public reverse-proxy prefix (e.g. /apps/csob) injected by AppFactory. + public string RootPath { get; init; } = GetEnv("ROOT_PATH", string.Empty); + + /// + /// Base URL of the ČSOB PSD2 resource API. Production default; override for any other + /// environment. All AISP/PISP/consent path templates are appended to this base. + /// + public string ApiBaseUrl { get; init; } = + GetEnv("CSOB_API_BASE_URL", "https://api.csob.cz/api/csob/psd2/v1"); + + /// + /// OAuth2 authorization endpoint (Authorization Code flow, PSU redirect). Production default; + /// verify against the current ČSOB developer portal as the host may change. + /// + public string OAuthAuthorizeUrl { get; init; } = + GetEnv("CSOB_OAUTH_AUTHORIZE_URL", "https://identita.csob.cz/mep/fs/fl/oauth2/auth"); + + /// + /// OAuth2 token endpoint (code->token and refresh). Production default; verify against the + /// current ČSOB developer portal. + /// + public string OAuthTokenUrl { get; init; } = + GetEnv("CSOB_OAUTH_TOKEN_URL", "https://api.csob.cz/api/csob/oauth2/v1/token"); + + /// Upstream HTTP request timeout in seconds. + public int RequestTimeoutSeconds { get; init; } = + ParseInt(GetEnv("CSOB_REQUEST_TIMEOUT_SECONDS", "100"), 100); + + private static string GetEnv(string name, string fallback) + { + var value = Environment.GetEnvironmentVariable(name); + return string.IsNullOrWhiteSpace(value) ? fallback : value; + } + + private static int ParseInt(string value, int fallback) => + int.TryParse(value, out var parsed) && parsed > 0 ? parsed : fallback; +} diff --git a/Controllers/AccountsController.cs b/Controllers/AccountsController.cs new file mode 100644 index 0000000..cecb457 --- /dev/null +++ b/Controllers/AccountsController.cs @@ -0,0 +1,61 @@ +using Microsoft.AspNetCore.Mvc; +using Csob.Services; + +namespace Csob.Controllers; + +/// +/// AISP – account information. All endpoints require the ČSOB credential headers +/// (eIDAS certificate, access token, API key, TPP name). See the Swagger description for details. +/// +[ApiController] +[Route("accounts")] +[Produces("application/json")] +[Tags("AISP – Accounts")] +public sealed class AccountsController : ControllerBase +{ + private readonly AccountsService _service; + + public AccountsController(AccountsService service) => _service = service; + + /// List the PSU's payment accounts (paged). + [HttpGet] + public async Task List( + [FromQuery] int? page, [FromQuery] int? size, [FromQuery] string? sort, [FromQuery] string? order, CancellationToken ct) + => Ok(await _service.ListAsync(page, size, sort, order, ct)); + + /// Get the balance(s) of an account. + [HttpGet("{id}/balance")] + public async Task Balance(string id, [FromQuery] string? currency, CancellationToken ct) + => Ok(await _service.BalanceAsync(id, currency, ct)); + + /// List booked transactions of an account (paged, optional date range). + [HttpGet("{id}/transactions")] + public async Task Transactions( + string id, + [FromQuery] int? page, [FromQuery] int? size, [FromQuery] string? sort, [FromQuery] string? order, + [FromQuery] string? dateFrom, [FromQuery] string? dateTo, CancellationToken ct) + => Ok(await _service.TransactionsAsync(id, new TransactionQuery + { + Page = page, Size = size, Sort = sort, Order = order, DateFrom = dateFrom, DateTo = dateTo, + }, ct)); + + /// List awaiting (pending) transactions of an account. + [HttpGet("{id}/transactions/awaiting")] + public async Task Awaiting(string id, [FromQuery] int? page, [FromQuery] int? size, CancellationToken ct) + => Ok(await _service.AwaitingTransactionsAsync(id, page, size, ct)); + + /// List the account's existing standing orders. + [HttpGet("{id}/standing-orders")] + public async Task StandingOrders(string id, [FromQuery] int? page, [FromQuery] int? size, CancellationToken ct) + => Ok(await _service.StandingOrdersAsync(id, page, size, ct)); + + /// Get a standing-order detail for the account. + [HttpGet("{id}/standing-orders/{standingOrderId}")] + public async Task StandingOrderDetail(string id, string standingOrderId, CancellationToken ct) + => Ok(await _service.StandingOrderDetailAsync(id, standingOrderId, ct)); + + /// List the account's direct-debit mandates. + [HttpGet("{id}/direct-debits")] + public async Task DirectDebits(string id, [FromQuery] int? page, [FromQuery] int? size, CancellationToken ct) + => Ok(await _service.DirectDebitsAsync(id, page, size, ct)); +} diff --git a/Controllers/ConsentsController.cs b/Controllers/ConsentsController.cs new file mode 100644 index 0000000..62b3879 --- /dev/null +++ b/Controllers/ConsentsController.cs @@ -0,0 +1,35 @@ +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Csob.Services; + +namespace Csob.Controllers; + +/// +/// Common – PSU consent lifecycle. Requires the ČSOB credential headers. The consent body is +/// forwarded to ČSOB as raw JSON (COBS consent shape). +/// +[ApiController] +[Route("consents")] +[Produces("application/json")] +[Tags("Consents")] +public sealed class ConsentsController : ControllerBase +{ + private readonly ConsentsService _service; + + public ConsentsController(ConsentsService service) => _service = service; + + /// Create a consent. + [HttpPost] + public async Task Create([FromBody] JsonNode request, CancellationToken ct) + => Ok(await _service.CreateAsync(request, ct)); + + /// Get a consent detail. + [HttpGet("{id}")] + public async Task Detail(string id, CancellationToken ct) + => Ok(await _service.DetailAsync(id, ct)); + + /// Revoke a consent. + [HttpDelete("{id}")] + public async Task Delete(string id, CancellationToken ct) + => Ok(await _service.DeleteAsync(id, ct)); +} diff --git a/Controllers/DirectDebitsController.cs b/Controllers/DirectDebitsController.cs new file mode 100644 index 0000000..d8b5bfd --- /dev/null +++ b/Controllers/DirectDebitsController.cs @@ -0,0 +1,56 @@ +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Csob.Services; + +namespace Csob.Controllers; + +/// +/// PISP – direct-debit mandate initiation and its authorization (sign) flow. Requires the ČSOB +/// credential headers. The mandate body is forwarded to ČSOB as raw JSON (COBS direct-debit shape). +/// +[ApiController] +[Route("direct-debits")] +[Produces("application/json")] +[Tags("PISP – Direct debits")] +public sealed class DirectDebitsController : ControllerBase +{ + private readonly DirectDebitsService _service; + + public DirectDebitsController(DirectDebitsService service) => _service = service; + + /// Initiate a direct-debit mandate. + [HttpPost] + public async Task Initiate([FromBody] JsonNode request, CancellationToken ct) + => Ok(await _service.InitiateAsync(request, ct)); + + /// Get the direct-debit mandate detail. + [HttpGet("{id}")] + public async Task Detail(string id, CancellationToken ct) + => Ok(await _service.DetailAsync(id, ct)); + + /// Get the direct-debit instruction status. + [HttpGet("{id}/status")] + public async Task Status(string id, CancellationToken ct) + => Ok(await _service.StatusAsync(id, ct)); + + /// Revoke a direct-debit mandate. + [HttpDelete("{id}")] + public async Task Cancel(string id, CancellationToken ct) + => Ok(await _service.CancelAsync(id, ct)); + + /// Start the authorization (SCA) of a direct-debit mandate. Returns the PSU redirect details. + [HttpPost("{id}/sign/{signId}")] + public async Task StartSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct) + => Ok(await _service.StartSignAsync(id, signId, body, ct)); + + /// Get the current state of an authorization (sign) transaction. + [HttpGet("{id}/sign/{signId}")] + public async Task SignStatus(string id, string signId, CancellationToken ct) + => Ok(await _service.SignStatusAsync(id, signId, ct)); + + /// Finalize an authorization (sign) transaction. + [HttpPut("{id}/sign/{signId}")] + public async Task FinalizeSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct) + => Ok(await _service.FinalizeSignAsync(id, signId, body, ct)); +} diff --git a/Controllers/MetaController.cs b/Controllers/MetaController.cs new file mode 100644 index 0000000..3f45dad --- /dev/null +++ b/Controllers/MetaController.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using Csob.Configuration; + +namespace Csob.Controllers; + +/// Service metadata endpoints. These do not require ČSOB credentials. +[ApiController] +[Produces("application/json")] +[Tags("Meta")] +public sealed class MetaController : ControllerBase +{ + private readonly CsobSettings _settings; + + public MetaController(CsobSettings settings) => _settings = settings; + + /// Liveness probe. + [HttpGet("/health")] + public IActionResult Health() => Ok(new { status = "ok" }); + + /// Service name, version and the configured upstream endpoints (no secrets). + [HttpGet("/version")] + public IActionResult Version() => Ok(new + { + app = _settings.AppName, + version = _settings.AppVersion, + language = "dotnet", + api = "ČSOB PSD2 (Czech Open Banking Standard / COBS)", + root_path = _settings.RootPath, + }); + + /// + /// Reports the non-secret configuration. This service is stateless and multi-tenant: it holds + /// no credentials, so there is nothing per-client to report — every secret is supplied per + /// request via headers. + /// + [HttpGet("/status")] + public IActionResult Status() => Ok(new + { + stateless = true, + api_base_url = _settings.ApiBaseUrl, + oauth_authorize_url = _settings.OAuthAuthorizeUrl, + oauth_token_url = _settings.OAuthTokenUrl, + request_timeout_seconds = _settings.RequestTimeoutSeconds, + }); +} diff --git a/Controllers/OAuthController.cs b/Controllers/OAuthController.cs new file mode 100644 index 0000000..fec8b08 --- /dev/null +++ b/Controllers/OAuthController.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Csob.Models; +using Csob.Services; + +namespace Csob.Controllers; + +/// +/// OAuth2 Authorization Code helper for the ČSOB PSD2 PSU consent flow. Build the authorization URL, +/// redirect the PSU to it, then exchange the returned code for tokens. The token/refresh calls run +/// over mutual TLS, so they require the X-CSOB-Certificate header; client id/secret are also +/// supplied per request (this service is multi-tenant). +/// +[ApiController] +[Route("oauth")] +[Produces("application/json")] +[Tags("OAuth2")] +public sealed class OAuthController : ControllerBase +{ + private readonly OAuthService _service; + + public OAuthController(OAuthService service) => _service = service; + + /// Build the ČSOB authorization URL to which the PSU must be redirected. + [HttpGet("authorization-url")] + public IActionResult AuthorizationUrl( + [FromQuery] string redirectUri, [FromQuery] string? scope, [FromQuery] string? state) + => Ok(_service.BuildAuthorizationUrl(redirectUri, scope, state)); + + /// Exchange an authorization code for an access/refresh token (mutual TLS). + [HttpPost("token")] + public async Task> Token([FromBody] TokenExchangeRequest request, CancellationToken ct) + => Ok(await _service.ExchangeCodeAsync(request, ct)); + + /// Refresh an access token using a refresh token (mutual TLS). + [HttpPost("refresh")] + public async Task> Refresh([FromBody] TokenRefreshRequest request, CancellationToken ct) + => Ok(await _service.RefreshAsync(request, ct)); +} diff --git a/Controllers/PaymentsController.cs b/Controllers/PaymentsController.cs new file mode 100644 index 0000000..9109584 --- /dev/null +++ b/Controllers/PaymentsController.cs @@ -0,0 +1,58 @@ +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Csob.Models; +using Csob.Services; + +namespace Csob.Controllers; + +/// +/// PISP – single payment initiation and its authorization (sign) flow. Requires the ČSOB credential +/// headers. After POST /payments the response carries signInfo.signId; use it with the +/// sign endpoints to drive Strong Customer Authentication (SCA). +/// +[ApiController] +[Route("payments")] +[Produces("application/json")] +[Tags("PISP – Payments")] +public sealed class PaymentsController : ControllerBase +{ + private readonly PaymentsService _service; + + public PaymentsController(PaymentsService service) => _service = service; + + /// Initiate a domestic (DMCT) or SEPA (ESCT) payment. + [HttpPost] + public async Task Initiate([FromBody] PaymentInitiationRequest request, CancellationToken ct) + => Ok(await _service.InitiateAsync(request, ct)); + + /// Get the full payment detail. + [HttpGet("{id}")] + public async Task Detail(string id, CancellationToken ct) + => Ok(await _service.DetailAsync(id, ct)); + + /// Get the payment instruction status. + [HttpGet("{id}/status")] + public async Task Status(string id, CancellationToken ct) + => Ok(await _service.StatusAsync(id, ct)); + + /// Cancel a not-yet-authorized payment. + [HttpDelete("{id}")] + public async Task Cancel(string id, CancellationToken ct) + => Ok(await _service.CancelAsync(id, ct)); + + /// Start the authorization (SCA) of a payment. Returns the PSU redirect details. + [HttpPost("{id}/sign/{signId}")] + public async Task StartSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct) + => Ok(await _service.StartSignAsync(id, signId, body, ct)); + + /// Get the current state of an authorization (sign) transaction. + [HttpGet("{id}/sign/{signId}")] + public async Task SignStatus(string id, string signId, CancellationToken ct) + => Ok(await _service.SignStatusAsync(id, signId, ct)); + + /// Finalize an authorization (sign) transaction. + [HttpPut("{id}/sign/{signId}")] + public async Task FinalizeSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct) + => Ok(await _service.FinalizeSignAsync(id, signId, body, ct)); +} diff --git a/Controllers/StandingOrdersController.cs b/Controllers/StandingOrdersController.cs new file mode 100644 index 0000000..46bd51c --- /dev/null +++ b/Controllers/StandingOrdersController.cs @@ -0,0 +1,54 @@ +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Csob.Models; +using Csob.Services; + +namespace Csob.Controllers; + +/// PISP – standing-order initiation and its authorization (sign) flow. Requires the ČSOB credential headers. +[ApiController] +[Route("standing-orders")] +[Produces("application/json")] +[Tags("PISP – Standing orders")] +public sealed class StandingOrdersController : ControllerBase +{ + private readonly StandingOrdersService _service; + + public StandingOrdersController(StandingOrdersService service) => _service = service; + + /// Initiate a standing order. + [HttpPost] + public async Task Initiate([FromBody] StandingOrderInitiationRequest request, CancellationToken ct) + => Ok(await _service.InitiateAsync(request, ct)); + + /// Get the standing-order detail. + [HttpGet("{id}")] + public async Task Detail(string id, CancellationToken ct) + => Ok(await _service.DetailAsync(id, ct)); + + /// Get the standing-order instruction status. + [HttpGet("{id}/status")] + public async Task Status(string id, CancellationToken ct) + => Ok(await _service.StatusAsync(id, ct)); + + /// Cancel a standing order. + [HttpDelete("{id}")] + public async Task Cancel(string id, CancellationToken ct) + => Ok(await _service.CancelAsync(id, ct)); + + /// Start the authorization (SCA) of a standing order. Returns the PSU redirect details. + [HttpPost("{id}/sign/{signId}")] + public async Task StartSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct) + => Ok(await _service.StartSignAsync(id, signId, body, ct)); + + /// Get the current state of an authorization (sign) transaction. + [HttpGet("{id}/sign/{signId}")] + public async Task SignStatus(string id, string signId, CancellationToken ct) + => Ok(await _service.SignStatusAsync(id, signId, ct)); + + /// Finalize an authorization (sign) transaction. + [HttpPut("{id}/sign/{signId}")] + public async Task FinalizeSign(string id, string signId, [FromBody(EmptyBodyBehavior = EmptyBodyBehavior.Allow)] JsonNode? body, CancellationToken ct) + => Ok(await _service.FinalizeSignAsync(id, signId, body, ct)); +} diff --git a/Credentials/CredentialConstants.cs b/Credentials/CredentialConstants.cs new file mode 100644 index 0000000..1230e09 --- /dev/null +++ b/Credentials/CredentialConstants.cs @@ -0,0 +1,44 @@ +namespace Csob.Credentials; + +/// +/// Names of the HTTP headers that carry per-request ČSOB credentials and context. +/// +/// This service is multi-tenant: it stores no credentials itself. Every sensitive value is +/// supplied per request in a header (never the query string or body) and is forwarded to ČSOB. +/// Headers must therefore only be sent over TLS. Nothing here is logged or persisted. +/// +public static class CredentialConstants +{ + /// + /// eIDAS client certificate (QWAC) as a Base64-encoded PKCS#12 / PFX bundle, including the + /// private key and the full chain. Used to establish the mutual-TLS connection to ČSOB. + /// Analogous to Node's https.Agent({ pfx, passphrase }). + /// + public const string CertificateHeader = "X-CSOB-Certificate"; + + /// Optional passphrase protecting the PFX in . + public const string CertificatePasswordHeader = "X-CSOB-Certificate-Password"; + + /// OAuth2 Bearer access token obtained for the PSU; forwarded as Authorization: Bearer. + public const string AccessTokenHeader = "X-Access-Token"; + + /// ČSOB application API key; forwarded as the APIKEY header. + public const string ApiKeyHeader = "X-API-Key"; + + /// TPP (third-party provider) organisation name; forwarded as the TPP-Name header. + public const string TppNameHeader = "X-TPP-Name"; + + /// OAuth2 client id of the registered TPP application (used by the OAuth helper endpoints). + public const string ClientIdHeader = "X-CSOB-Client-Id"; + + /// OAuth2 client secret of the registered TPP application (used by the OAuth helper endpoints). + public const string ClientSecretHeader = "X-CSOB-Client-Secret"; + + // Optional PSU (end-user) context, forwarded verbatim to ČSOB when present. + + /// Whether the PSU is online/involved in the request; forwarded as User-Involved (default false). + public const string UserInvolvedHeader = "X-User-Involved"; + + /// PSU IP address; forwarded as User-IP-Address. + public const string UserIpAddressHeader = "X-User-IP-Address"; +} diff --git a/Credentials/CsobCredentials.cs b/Credentials/CsobCredentials.cs new file mode 100644 index 0000000..4f20b6e --- /dev/null +++ b/Credentials/CsobCredentials.cs @@ -0,0 +1,31 @@ +using System.Security.Cryptography.X509Certificates; + +namespace Csob.Credentials; + +/// +/// Fully resolved set of per-request credentials and PSU context used to call the ČSOB PSD2 API. +/// Built from request headers by ; never logged. +/// +public sealed record CsobCredentials +{ + /// OAuth2 Bearer access token (forwarded as Authorization: Bearer). + public required string AccessToken { get; init; } + + /// ČSOB application API key (forwarded as APIKEY). + public required string ApiKey { get; init; } + + /// TPP organisation name (forwarded as TPP-Name). + public required string TppName { get; init; } + + /// + /// eIDAS client certificate (with private key) for mutual TLS. Optional at the type level so + /// metadata endpoints can resolve context, but required for any real upstream call. + /// + public X509Certificate2? Certificate { get; init; } + + /// Whether the PSU is online for this request (User-Involved); defaults to false. + public bool UserInvolved { get; init; } + + /// Optional PSU IP address (User-IP-Address). + public string? UserIpAddress { get; init; } +} diff --git a/Credentials/MissingCredentialsException.cs b/Credentials/MissingCredentialsException.cs new file mode 100644 index 0000000..b174e10 --- /dev/null +++ b/Credentials/MissingCredentialsException.cs @@ -0,0 +1,16 @@ +namespace Csob.Credentials; + +/// +/// Raised when a request does not provide the credential headers required to call ČSOB. +/// Translated to HTTP 401 by the exception-handling middleware. +/// +public sealed class MissingCredentialsException : Exception +{ + public IReadOnlyList MissingHeaders { get; } + + public MissingCredentialsException(IReadOnlyList missingHeaders) + : base("Incomplete ČSOB credentials. Provide the missing values as request headers (TLS only).") + { + MissingHeaders = missingHeaders; + } +} diff --git a/Credentials/RequestCredentialsProvider.cs b/Credentials/RequestCredentialsProvider.cs new file mode 100644 index 0000000..a6743d4 --- /dev/null +++ b/Credentials/RequestCredentialsProvider.cs @@ -0,0 +1,97 @@ +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace Csob.Credentials; + +/// +/// Resolves the ČSOB credentials and PSU context for the current request, exclusively from HTTP +/// headers (this service stores no secrets). Missing required headers produce a 401; a malformed +/// certificate or wrong passphrase produces a 400 (via ). +/// +public sealed class RequestCredentialsProvider +{ + private readonly IHttpContextAccessor _httpContextAccessor; + + public RequestCredentialsProvider(IHttpContextAccessor httpContextAccessor) + { + _httpContextAccessor = httpContextAccessor; + } + + /// Reads a single request header, returning null when absent or blank. + public string? Header(string name) + { + var headers = _httpContextAccessor.HttpContext?.Request.Headers; + if (headers is not null && headers.TryGetValue(name, out var value)) + { + var raw = value.ToString(); + if (!string.IsNullOrWhiteSpace(raw)) + { + return raw; + } + } + + return null; + } + + /// + /// Builds the eIDAS client certificate from the Base64 PFX header (+ optional passphrase). + /// Returns null when no certificate header is present. + /// + /// The header is not valid Base64 or the PFX/passphrase is invalid. + public X509Certificate2? TryBuildCertificate() + { + var base64 = Header(CredentialConstants.CertificateHeader); + if (base64 is null) + { + return null; + } + + byte[] raw; + try + { + raw = Convert.FromBase64String(base64.Trim()); + } + catch (FormatException ex) + { + throw new CryptographicException($"{CredentialConstants.CertificateHeader} is not valid Base64.", ex); + } + + var password = Header(CredentialConstants.CertificatePasswordHeader); + + // EphemeralKeySet keeps the private key in memory only — never written to the machine key store / disk. + return new X509Certificate2(raw, password, X509KeyStorageFlags.EphemeralKeySet); + } + + /// + /// Resolves the full credential set required for an AISP/PISP/consent call. Throws + /// if any required header is absent. + /// + public CsobCredentials Resolve() + { + var accessToken = Header(CredentialConstants.AccessTokenHeader); + var apiKey = Header(CredentialConstants.ApiKeyHeader); + var tppName = Header(CredentialConstants.TppNameHeader); + var certificate = TryBuildCertificate(); + + var missing = new List(); + if (string.IsNullOrWhiteSpace(accessToken)) missing.Add(CredentialConstants.AccessTokenHeader); + if (string.IsNullOrWhiteSpace(apiKey)) missing.Add(CredentialConstants.ApiKeyHeader); + if (string.IsNullOrWhiteSpace(tppName)) missing.Add(CredentialConstants.TppNameHeader); + if (certificate is null) missing.Add(CredentialConstants.CertificateHeader); + if (missing.Count > 0) + { + certificate?.Dispose(); + throw new MissingCredentialsException(missing); + } + + return new CsobCredentials + { + AccessToken = accessToken!, + ApiKey = apiKey!, + TppName = tppName!, + Certificate = certificate, + UserInvolved = string.Equals(Header(CredentialConstants.UserInvolvedHeader), "true", StringComparison.OrdinalIgnoreCase), + UserIpAddress = Header(CredentialConstants.UserIpAddressHeader), + }; + } +} diff --git a/Csob.csproj b/Csob.csproj index 24ec0e8..b80d4ff 100644 --- a/Csob.csproj +++ b/Csob.csproj @@ -3,5 +3,13 @@ net8.0 enable enable + Csob + true + + $(NoWarn);1591 + + + + diff --git a/Infrastructure/CredentialHeadersOperationFilter.cs b/Infrastructure/CredentialHeadersOperationFilter.cs new file mode 100644 index 0000000..f56993c --- /dev/null +++ b/Infrastructure/CredentialHeadersOperationFilter.cs @@ -0,0 +1,70 @@ +using Microsoft.OpenApi.Any; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using Csob.Controllers; +using Csob.Credentials; + +namespace Csob.Infrastructure; + +/// +/// Documents the per-request credential headers in Swagger. Metadata endpoints need none; the +/// OAuth helper needs the certificate + client id/secret; every other (AISP/PISP/consent) endpoint +/// needs the certificate + access token + API key + TPP name. Headers are marked optional at the +/// schema level (the service validates them at runtime) but the descriptions state what is required. +/// +public sealed class CredentialHeadersOperationFilter : IOperationFilter +{ + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + var declaringType = context.MethodInfo.DeclaringType; + + // Metadata endpoints (health/version/status) do not talk to ČSOB. + if (declaringType == typeof(MetaController)) + { + return; + } + + operation.Parameters ??= new List(); + + AddHeader(operation, CredentialConstants.CertificateHeader, + "eIDAS client certificate (QWAC) as a Base64-encoded PFX/PKCS#12 bundle incl. private key and chain. Used for mutual TLS to ČSOB. SENSITIVE — TLS only."); + AddHeader(operation, CredentialConstants.CertificatePasswordHeader, + "Passphrase protecting the PFX in X-CSOB-Certificate (omit if the PFX has no password). SENSITIVE."); + + if (declaringType == typeof(OAuthController)) + { + AddHeader(operation, CredentialConstants.ClientIdHeader, + "OAuth2 client id of the registered TPP application. Required."); + AddHeader(operation, CredentialConstants.ClientSecretHeader, + "OAuth2 client secret of the registered TPP application. Required for token/refresh. SENSITIVE."); + return; + } + + AddHeader(operation, CredentialConstants.AccessTokenHeader, + "OAuth2 Bearer access token obtained for the PSU. Forwarded as 'Authorization: Bearer'. Required. SENSITIVE."); + AddHeader(operation, CredentialConstants.ApiKeyHeader, + "ČSOB application API key. Forwarded as 'APIKEY'. Required."); + AddHeader(operation, CredentialConstants.TppNameHeader, + "TPP organisation name. Forwarded as 'TPP-Name'. Required."); + AddHeader(operation, CredentialConstants.UserInvolvedHeader, + "Optional. 'true' if the PSU is online for this request (forwarded as 'User-Involved'). Defaults to false.", example: "false"); + AddHeader(operation, CredentialConstants.UserIpAddressHeader, + "Optional. PSU IP address (forwarded as 'User-IP-Address')."); + } + + private static void AddHeader(OpenApiOperation operation, string name, string description, string? example = null) + { + operation.Parameters.Add(new OpenApiParameter + { + Name = name, + In = ParameterLocation.Header, + Required = false, + Description = description, + Schema = new OpenApiSchema + { + Type = "string", + Example = example is null ? null : new OpenApiString(example), + }, + }); + } +} diff --git a/Infrastructure/ExceptionHandlingMiddleware.cs b/Infrastructure/ExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..3419c6c --- /dev/null +++ b/Infrastructure/ExceptionHandlingMiddleware.cs @@ -0,0 +1,127 @@ +using System.Net; +using System.Security.Cryptography; +using System.Text.Json.Nodes; +using Microsoft.AspNetCore.Mvc; +using Csob.Client; +using Csob.Credentials; + +namespace Csob.Infrastructure; + +/// +/// Translates domain exceptions into JSON responses. Credentials are +/// never logged — only upstream status codes and error codes (the upstream's own payload). Errors +/// are always logged (no silent failures). +/// +public sealed class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (MissingCredentialsException ex) + { + await WriteProblem(context, HttpStatusCode.Unauthorized, ex.Message, new Dictionary + { + ["missingHeaders"] = ex.MissingHeaders, + }); + } + catch (CryptographicException ex) + { + // Invalid Base64 PFX or wrong passphrase in the certificate header. Do not log the value. + _logger.LogWarning("Client certificate could not be loaded: {Message}", ex.Message); + await WriteProblem(context, HttpStatusCode.BadRequest, + "The provided client certificate could not be loaded. Check X-CSOB-Certificate (Base64 PFX) and X-CSOB-Certificate-Password.", null); + } + catch (CsobApiException ex) + { + _logger.LogWarning("ČSOB PSD2 API call failed: {Status} {Codes}", (int)ex.StatusCode, string.Join(",", ex.ErrorCodes)); + await WriteProblem(context, ex.StatusCode, ex.Message, new Dictionary + { + ["csobErrorCodes"] = ex.ErrorCodes, + ["upstreamBody"] = ParseBody(ex.RawBody), + }); + } + catch (TaskCanceledException) when (!context.RequestAborted.IsCancellationRequested) + { + _logger.LogWarning("ČSOB PSD2 API call timed out."); + await WriteProblem(context, HttpStatusCode.GatewayTimeout, "The ČSOB PSD2 API did not respond in time.", null); + } + catch (HttpRequestException ex) + { + // Network failure, or the mutual-TLS handshake was rejected (e.g. wrong/expired eIDAS certificate). + _logger.LogWarning(ex, "ČSOB PSD2 API unreachable or TLS handshake failed."); + await WriteProblem(context, HttpStatusCode.BadGateway, + "The ČSOB PSD2 API is unreachable or the mutual-TLS handshake failed.", null); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled error while processing the request."); + await WriteProblem(context, HttpStatusCode.InternalServerError, "An unexpected error occurred.", null); + } + } + + private static JsonNode? ParseBody(string? rawBody) + { + if (string.IsNullOrWhiteSpace(rawBody)) + { + return null; + } + + try + { + return JsonNode.Parse(rawBody); + } + catch (System.Text.Json.JsonException) + { + return JsonValue.Create(rawBody); + } + } + + private static async Task WriteProblem(HttpContext context, HttpStatusCode status, string detail, IDictionary? extensions) + { + if (context.Response.HasStarted) + { + return; + } + + var problem = new ProblemDetails + { + Status = (int)status, + Title = ReasonPhrase(status), + Detail = detail, + }; + + if (extensions is not null) + { + foreach (var (key, value) in extensions) + { + problem.Extensions[key] = value; + } + } + + context.Response.Clear(); + context.Response.StatusCode = (int)status; + context.Response.ContentType = "application/problem+json"; + await context.Response.WriteAsJsonAsync(problem); + } + + private static string ReasonPhrase(HttpStatusCode status) => status switch + { + HttpStatusCode.Unauthorized => "Unauthorized", + HttpStatusCode.BadRequest => "Bad Request", + HttpStatusCode.BadGateway => "Upstream ČSOB PSD2 API error", + HttpStatusCode.GatewayTimeout => "Upstream ČSOB PSD2 API timeout", + _ => status.ToString(), + }; +} diff --git a/Models/Common.cs b/Models/Common.cs new file mode 100644 index 0000000..c9b760a --- /dev/null +++ b/Models/Common.cs @@ -0,0 +1,91 @@ +using System.Text.Json.Nodes; + +namespace Csob.Models; + +// Reusable building blocks shared by the typed PISP request bodies. Field names follow the COBS +// JSON contract (camelCase via System.Text.Json web defaults). Deeply variable sub-trees (party +// identification, structured remittance) are typed as JsonNode so any valid COBS payload passes +// through unchanged. + +/// Monetary amount with ISO 4217 currency. +public sealed class Amount +{ + public decimal Value { get; set; } + public string Currency { get; set; } = "CZK"; +} + +/// Wrapper matching the amount object whose instructedAmount holds the value/currency. +public sealed class AmountContainer +{ + public Amount InstructedAmount { get; set; } = new(); +} + +/// Account number identification. For domestic/SEPA writes only iban is required. +public sealed class AccountIdentification +{ + public string? Iban { get; set; } + public string? Other { get; set; } +} + +/// Account reference with optional currency (debtorAccount/creditorAccount). +public sealed class AccountReference +{ + public AccountIdentification Identification { get; set; } = new(); + public string? Currency { get; set; } +} + +public sealed class ServiceLevel +{ + /// Payment scheme: DMCT (domestic), ESCT (SEPA), XBCT (cross-border), EXCT, NXCT. + public string? Code { get; set; } +} + +public sealed class PaymentTypeInformation +{ + /// NORM (default), HIGH (express) or INST (instant). + public string? InstructionPriority { get; set; } + public ServiceLevel? ServiceLevel { get; set; } +} + +public sealed class FinancialInstitutionIdentification +{ + public string? Bic { get; set; } +} + +public sealed class Agent +{ + public FinancialInstitutionIdentification? FinancialInstitutionIdentification { get; set; } +} + +public sealed class PostalAddress +{ + public string? StreetName { get; set; } + public string? BuildingNumber { get; set; } + public string? PostCode { get; set; } + public string? TownName { get; set; } + public string? Country { get; set; } +} + +/// A party (debtor/creditor/ultimate*). Identification is variable, so kept as raw JSON. +public sealed class Party +{ + public string? Name { get; set; } + public PostalAddress? PostalAddress { get; set; } + public JsonNode? Identification { get; set; } +} + +/// +/// Remittance information. unstructured is free text (Czech symbols may be encoded as +/// /VS/.../SS/.../KS/...); structured varies (its reference may be a string or +/// an array) so it is passed through as raw JSON. +/// +public sealed class RemittanceInformation +{ + public string? Unstructured { get; set; } + public JsonNode? Structured { get; set; } +} + +public sealed class Purpose +{ + public string? Proprietary { get; set; } +} diff --git a/Models/OAuth.cs b/Models/OAuth.cs new file mode 100644 index 0000000..b466b01 --- /dev/null +++ b/Models/OAuth.cs @@ -0,0 +1,25 @@ +namespace Csob.Models; + +/// Body for POST /oauth/token (Authorization Code grant). Secrets travel in headers. +public sealed class TokenExchangeRequest +{ + /// Authorization code returned to the redirect URI after PSU consent. + public string Code { get; set; } = string.Empty; + /// Redirect URI registered for the TPP app; must match the one used to obtain the code. + public string RedirectUri { get; set; } = string.Empty; +} + +/// Body for POST /oauth/refresh (Refresh Token grant). +public sealed class TokenRefreshRequest +{ + public string RefreshToken { get; set; } = string.Empty; +} + +/// Response of GET /oauth/authorization-url. +public sealed class AuthorizationUrlResponse +{ + /// Fully-built ČSOB authorization URL to which the PSU must be redirected. + public string AuthorizationUrl { get; set; } = string.Empty; + /// The opaque state value echoed back on the redirect (CSRF protection). + public string? State { get; set; } +} diff --git a/Models/Payments.cs b/Models/Payments.cs new file mode 100644 index 0000000..6daf7ef --- /dev/null +++ b/Models/Payments.cs @@ -0,0 +1,38 @@ +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Csob.Models; + +public sealed class PaymentIdentification +{ + /// Unique instruction id assigned by the TPP (idempotency key). Max 35 chars. + public string? InstructionIdentification { get; set; } + public string? EndToEndIdentification { get; set; } +} + +/// +/// Payment initiation request (POST /my/payments). Covers domestic (DMCT) and SEPA (ESCT) +/// payments — SEPA-only fields (creditor address, agent, ultimate parties, purpose) are optional. +/// Any additional COBS field not modelled here is preserved via . +/// +public sealed class PaymentInitiationRequest +{ + public PaymentIdentification? PaymentIdentification { get; set; } + public PaymentTypeInformation? PaymentTypeInformation { get; set; } + public AmountContainer Amount { get; set; } = new(); + public string? RequestedExecutionDate { get; set; } + + public Party? UltimateDebtor { get; set; } + public AccountReference DebtorAccount { get; set; } = new(); + public Agent? CreditorAgent { get; set; } + public Party? Creditor { get; set; } + public AccountReference CreditorAccount { get; set; } = new(); + public Party? UltimateCreditor { get; set; } + + public RemittanceInformation? RemittanceInformation { get; set; } + public Purpose? Purpose { get; set; } + + /// Any COBS fields not explicitly modelled are forwarded to ČSOB unchanged. + [JsonExtensionData] + public Dictionary? AdditionalData { get; set; } +} diff --git a/Models/StandingOrders.cs b/Models/StandingOrders.cs new file mode 100644 index 0000000..4dd0a10 --- /dev/null +++ b/Models/StandingOrders.cs @@ -0,0 +1,48 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Csob.Models; + +public sealed class StandingOrderIdentification +{ + public string? InstructionIdentification { get; set; } + public string? TransactionIdentification { get; set; } +} + +public sealed class StandingOrderExecution +{ + /// DAILY, WEEKLY, MONTHLY, BI_MONTHLY, QUARTERLY, HALFYEARLY, YEARLY, SINGLE, IRREGULAR. + public string? Interval { get; set; } + /// Day within the interval (e.g. day-of-month "25"). + public string? IntervalDue { get; set; } + /// e.g. MAX_AMOUNT_EXCEEDED, UNTIL_CANCELLATION. + public string? Mode { get; set; } + public string? ModeDue { get; set; } +} + +public sealed class StandingOrderDetail +{ + public string? Alias { get; set; } + public StandingOrderExecution? Execution { get; set; } + /// Optional exceptions block (stoppages/breaks); shape varies, kept as raw JSON. + public JsonNode? Exceptions { get; set; } + /// Optional validity block (lastExecutionDate/maxAmount); kept as raw JSON. + public JsonNode? Validity { get; set; } +} + +/// Standing-order initiation request (POST /my/standingorders). +public sealed class StandingOrderInitiationRequest +{ + public StandingOrderIdentification? StandingOrderIdentification { get; set; } + public PaymentTypeInformation? PaymentTypeInformation { get; set; } + public AmountContainer Amount { get; set; } = new(); + public string? RequestedExecutionDate { get; set; } + public StandingOrderDetail? StandingOrder { get; set; } + public AccountReference DebtorAccount { get; set; } = new(); + public AccountReference CreditorAccount { get; set; } = new(); + public RemittanceInformation? RemittanceInformation { get; set; } + + [JsonExtensionData] + public Dictionary? AdditionalData { get; set; } +} diff --git a/Program.cs b/Program.cs index 6ae7e8b..bdf1406 100644 --- a/Program.cs +++ b/Program.cs @@ -1,22 +1,116 @@ +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(); -var rootPath = Environment.GetEnvironmentVariable("ROOT_PATH"); -if (!string.IsNullOrWhiteSpace(rootPath)) +// 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(rootPath); + app.UsePathBase(publicPrefix); } -app.MapGet("/", () => Results.Json(new -{ - name = "csob", - service = "csob", - status = "ok" -})); +app.UseMiddleware(); -app.MapGet("/health", () => Results.Json(new +// 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 => { - status = "ok" -})); + 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(); diff --git a/README.md b/README.md index 6b19598..624e17e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,90 @@ # csob -.NET API služba vytvořená přes CSBot Services Portal. +Multi-tenant REST integrace na **ČSOB PSD2 (Open Banking) API** podle Czech Open Banking Standard (COBS). +.NET 8 služba běžící v AppFactory za reverse proxy `/apps/csob`. + +Pokrývá **AISP** (informace o účtech), **PISP** (iniciace plateb, trvalých příkazů a inkas vč. sign/SCA flow), +**consents** a **OAuth2** helper (Authorization Code flow). + +## Architektura + +Služba je **bezstavový proxy** – neukládá žádné credentials. Volající klientská služba předává veškeré +citlivé údaje (eIDAS certifikát, OAuth client id/secret, access token, APIKEY, TPP name) **per-request +v HTTP hlavičkách** (jen přes TLS). mTLS se na základě certifikátu z hlavičky staví per-request a klienti +se cachují podle thumbprintu certifikátu. Nic se neloguje ani neukládá na disk. + +Strukturně zrcadlí sesterskou službu `idoklad` (Configuration → env, Credentials → hlavičky, Client factory ++ Accessor, Services, Controllers, Infrastructure middleware + Swagger operation filter, `/docs`). ## Endpointy -- GET / -- GET /health +Veřejně dostupné přes `https://services.csbot.cz/apps/csob/...`. + +### Meta (bez credentials) +- `GET /health` – liveness +- `GET /version` – název/verze + API info +- `GET /status` – ne-secret konfigurace (base/oauth URL, timeout) +- `GET /docs` – Swagger UI +- `GET /docs/v1/swagger.json` – OpenAPI dokument + +### OAuth2 +- `GET /oauth/authorization-url` – sestaví authorize URL pro přesměrování PSU +- `POST /oauth/token` – výměna `code` → access/refresh token (mTLS) +- `POST /oauth/refresh` – obnova access tokenu (mTLS) + +### AISP – účty +- `GET /accounts` +- `GET /accounts/{id}/balance` +- `GET /accounts/{id}/transactions` +- `GET /accounts/{id}/transactions/awaiting` +- `GET /accounts/{id}/standing-orders` +- `GET /accounts/{id}/standing-orders/{standingOrderId}` +- `GET /accounts/{id}/direct-debits` + +### PISP – platby / trvalé příkazy / inkasa +Pro každou agendu: `POST` (iniciace), `GET /{id}` (detail), `GET /{id}/status`, `DELETE /{id}` (zrušení), +a sign/SCA flow `POST|GET|PUT /{id}/sign/{signId}`. +- `/payments` +- `/standing-orders` +- `/direct-debits` + +### Consents +- `POST /consents`, `GET /consents/{id}`, `DELETE /consents/{id}` + +## Credential hlavičky (per-request, jen přes TLS) + +| Hlavička | Význam | Předáno do ČSOB jako | +|---|---|---| +| `X-CSOB-Certificate` | eIDAS klientský certifikát (QWAC), **Base64 PFX/PKCS#12** vč. privátního klíče a chainu | mutual TLS | +| `X-CSOB-Certificate-Password` | heslo k PFX (volitelné) | – | +| `X-Access-Token` | OAuth2 Bearer access token PSU (AISP/PISP/consents) | `Authorization: Bearer` | +| `X-API-Key` | ČSOB application API key | `APIKEY` | +| `X-TPP-Name` | název TPP organizace | `TPP-Name` | +| `X-CSOB-Client-Id` / `X-CSOB-Client-Secret` | OAuth2 app credentials (jen `/oauth/*`) | OAuth form | +| `X-User-Involved`, `X-User-IP-Address` | volitelný PSU kontext | `User-Involved`, `User-IP-Address` | + +Hlavičky `X-Request-ID` a `Date` generuje služba automaticky. + +## Konfigurace (environment variables – pouze ne-secret) + +| Proměnná | Default | +|---|---| +| `APP_NAME` | `ČSOB PSD2 Service` | +| `APP_VERSION` | `1.0.0` | +| `ROOT_PATH` | (prázdné; AppFactory nastaví `/apps/csob`) | +| `CSOB_API_BASE_URL` | `https://api.csob.cz/api/csob/psd2/v1` | +| `CSOB_OAUTH_AUTHORIZE_URL` | `https://identita.csob.cz/mep/fs/fl/oauth2/auth` | +| `CSOB_OAUTH_TOKEN_URL` | `https://api.csob.cz/api/csob/oauth2/v1/token` | +| `CSOB_REQUEST_TIMEOUT_SECONDS` | `100` | + +> **Žádné secrets v env.** Certifikát, client id/secret, access token a APIKEY jdou výhradně per-request hlavičkami. + +## Lokální spuštění + +```bash +dotnet run --project Csob.csproj +# nebo +ROOT_PATH=/apps/csob ASPNETCORE_URLS=http://0.0.0.0:8080 dotnet run +``` + +Podrobnosti k OAuth/SCA flow a kompletní seznam endpointů viz [documentation/](documentation/). diff --git a/Services/AccountsService.cs b/Services/AccountsService.cs new file mode 100644 index 0000000..d208086 --- /dev/null +++ b/Services/AccountsService.cs @@ -0,0 +1,65 @@ +using System.Text.Json.Nodes; +using Csob.Client; + +namespace Csob.Services; + +/// AISP – account information (accounts, balance, transactions, standing orders, direct debits). +public sealed class AccountsService +{ + private readonly CsobApiAccessor _accessor; + + public AccountsService(CsobApiAccessor accessor) => _accessor = accessor; + + public Task ListAsync(int? page, int? size, string? sort, string? order, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.Accounts, Paging(page, size, sort, order), ct); + + public Task BalanceAsync(string accountId, string? currency, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.Balance(accountId), + new Dictionary { ["currency"] = currency }, ct); + + public Task TransactionsAsync(string accountId, TransactionQuery query, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.Transactions(accountId), query.ToDictionary(), ct); + + public Task AwaitingTransactionsAsync(string accountId, int? page, int? size, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.AwaitingTransactions(accountId), Paging(page, size, null, null), ct); + + public Task StandingOrdersAsync(string accountId, int? page, int? size, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.AccountStandingOrders(accountId), Paging(page, size, null, null), ct); + + public Task StandingOrderDetailAsync(string accountId, string standingOrderId, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.AccountStandingOrder(accountId, standingOrderId), null, ct); + + public Task DirectDebitsAsync(string accountId, int? page, int? size, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.AccountDirectDebits(accountId), Paging(page, size, null, null), ct); + + private static Dictionary Paging(int? page, int? size, string? sort, string? order) => new() + { + ["page"] = page?.ToString(), + ["size"] = size?.ToString(), + ["sort"] = sort, + ["order"] = order, + }; +} + +/// Optional filters for the transactions listing (forwarded as query parameters). +public sealed class TransactionQuery +{ + public int? Page { get; set; } + public int? Size { get; set; } + public string? Sort { get; set; } + public string? Order { get; set; } + /// ISO date (YYYY-MM-DD) lower bound. + public string? DateFrom { get; set; } + /// ISO date (YYYY-MM-DD) upper bound. + public string? DateTo { get; set; } + + public Dictionary ToDictionary() => new() + { + ["page"] = Page?.ToString(), + ["size"] = Size?.ToString(), + ["sort"] = Sort, + ["order"] = Order, + ["dateFrom"] = DateFrom, + ["dateTo"] = DateTo, + }; +} diff --git a/Services/ConsentsService.cs b/Services/ConsentsService.cs new file mode 100644 index 0000000..d73a3ad --- /dev/null +++ b/Services/ConsentsService.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Nodes; +using Csob.Client; + +namespace Csob.Services; + +/// +/// Common – PSU consent lifecycle (create / detail / revoke). The consent request body varies +/// across COBS profiles, so it is accepted and forwarded as raw JSON. +/// +public sealed class ConsentsService +{ + private readonly CsobApiAccessor _accessor; + + public ConsentsService(CsobApiAccessor accessor) => _accessor = accessor; + + public Task CreateAsync(JsonNode request, CancellationToken ct) + => _accessor.Client.PostAsync(CsobApiPaths.Consents, request, ct); + + public Task DetailAsync(string id, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.Consent(id), null, ct); + + public Task DeleteAsync(string id, CancellationToken ct) + => _accessor.Client.DeleteAsync(CsobApiPaths.Consent(id), ct); +} diff --git a/Services/DirectDebitsService.cs b/Services/DirectDebitsService.cs new file mode 100644 index 0000000..b5a9f61 --- /dev/null +++ b/Services/DirectDebitsService.cs @@ -0,0 +1,36 @@ +using System.Text.Json.Nodes; +using Csob.Client; + +namespace Csob.Services; + +/// +/// PISP – direct-debit mandate initiation, detail/status, revocation and the sign (SCA) flow. +/// The mandate creation body varies across COBS profiles, so it is accepted and forwarded as raw JSON. +/// +public sealed class DirectDebitsService +{ + private readonly CsobApiAccessor _accessor; + + public DirectDebitsService(CsobApiAccessor accessor) => _accessor = accessor; + + public Task InitiateAsync(JsonNode request, CancellationToken ct) + => _accessor.Client.PostAsync(CsobApiPaths.DirectDebits, request, ct); + + public Task DetailAsync(string id, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.DirectDebit(id), null, ct); + + public Task StatusAsync(string id, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.DirectDebitStatus(id), null, ct); + + public Task CancelAsync(string id, CancellationToken ct) + => _accessor.Client.DeleteAsync(CsobApiPaths.DirectDebit(id), ct); + + public Task StartSignAsync(string id, string signId, JsonNode? body, CancellationToken ct) + => _accessor.Client.PostAsync(CsobApiPaths.DirectDebitSign(id, signId), body, ct); + + public Task SignStatusAsync(string id, string signId, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.DirectDebitSign(id, signId), null, ct); + + public Task FinalizeSignAsync(string id, string signId, JsonNode? body, CancellationToken ct) + => _accessor.Client.PutAsync(CsobApiPaths.DirectDebitSign(id, signId), body, ct); +} diff --git a/Services/OAuthService.cs b/Services/OAuthService.cs new file mode 100644 index 0000000..d3c2335 --- /dev/null +++ b/Services/OAuthService.cs @@ -0,0 +1,114 @@ +using System.Net; +using System.Text.Json.Nodes; +using Csob.Client; +using Csob.Configuration; +using Csob.Credentials; +using Csob.Models; + +namespace Csob.Services; + +/// +/// OAuth2 Authorization Code helper for the ČSOB PSD2 flow. Builds the PSU authorization URL and +/// exchanges/refreshes tokens against the ČSOB token endpoint. The token endpoint is behind mutual +/// TLS, so the client certificate header is required for the token/refresh calls. Client id/secret +/// are taken from per-request headers (this service is multi-tenant and stores no app credentials). +/// +public sealed class OAuthService +{ + private readonly RequestCredentialsProvider _credentials; + private readonly CsobHttpClientProvider _httpClientProvider; + private readonly CsobSettings _settings; + + public OAuthService( + RequestCredentialsProvider credentials, + CsobHttpClientProvider httpClientProvider, + CsobSettings settings) + { + _credentials = credentials; + _httpClientProvider = httpClientProvider; + _settings = settings; + } + + /// Builds the authorization URL to which the PSU must be redirected. + public AuthorizationUrlResponse BuildAuthorizationUrl(string redirectUri, string? scope, string? state) + { + var clientId = RequireHeader(CredentialConstants.ClientIdHeader); + + var query = new Dictionary + { + ["response_type"] = "code", + ["client_id"] = clientId, + ["redirect_uri"] = redirectUri, + ["scope"] = scope, + ["state"] = state, + }; + + var url = _settings.OAuthAuthorizeUrl + "?" + string.Join("&", query + .Where(kvp => !string.IsNullOrWhiteSpace(kvp.Value)) + .Select(kvp => $"{Uri.EscapeDataString(kvp.Key)}={Uri.EscapeDataString(kvp.Value!)}")); + + return new AuthorizationUrlResponse { AuthorizationUrl = url, State = state }; + } + + public Task ExchangeCodeAsync(TokenExchangeRequest request, CancellationToken ct) + => PostTokenAsync(new Dictionary + { + ["grant_type"] = "authorization_code", + ["code"] = request.Code, + ["redirect_uri"] = request.RedirectUri, + }, ct); + + public Task RefreshAsync(TokenRefreshRequest request, CancellationToken ct) + => PostTokenAsync(new Dictionary + { + ["grant_type"] = "refresh_token", + ["refresh_token"] = request.RefreshToken, + }, ct); + + private async Task PostTokenAsync(Dictionary form, CancellationToken ct) + { + var clientId = RequireHeader(CredentialConstants.ClientIdHeader); + var clientSecret = RequireHeader(CredentialConstants.ClientSecretHeader); + + var certificate = _credentials.TryBuildCertificate() + ?? throw new MissingCredentialsException(new[] { CredentialConstants.CertificateHeader }); + + form["client_id"] = clientId; + form["client_secret"] = clientSecret; + + var http = _httpClientProvider.GetClient(certificate); + + using var content = new FormUrlEncodedContent(form); + using var response = await http.PostAsync(_settings.OAuthTokenUrl, content, ct); + var payload = await response.Content.ReadAsStringAsync(ct); + + if (!response.IsSuccessStatusCode) + { + throw new CsobApiException(response.StatusCode, ParseOAuthError(payload), payload); + } + + return string.IsNullOrWhiteSpace(payload) ? null : JsonNode.Parse(payload); + } + + private string RequireHeader(string name) + => _credentials.Header(name) ?? throw new MissingCredentialsException(new[] { name }); + + private static IReadOnlyList ParseOAuthError(string payload) + { + if (string.IsNullOrWhiteSpace(payload)) + { + return Array.Empty(); + } + + try + { + // OAuth2 errors use { "error": "...", "error_description": "..." }. + var error = JsonNode.Parse(payload)?["error"]?.GetValue(); + return string.IsNullOrWhiteSpace(error) ? Array.Empty() : new[] { error! }; + } + catch (System.Text.Json.JsonException) + { + return Array.Empty(); + } + } +} diff --git a/Services/PaymentsService.cs b/Services/PaymentsService.cs new file mode 100644 index 0000000..e9064a4 --- /dev/null +++ b/Services/PaymentsService.cs @@ -0,0 +1,37 @@ +using System.Text.Json.Nodes; +using Csob.Client; +using Csob.Models; + +namespace Csob.Services; + +/// PISP – single payment initiation, status/detail, cancellation and the sign (SCA) flow. +public sealed class PaymentsService +{ + private readonly CsobApiAccessor _accessor; + + public PaymentsService(CsobApiAccessor accessor) => _accessor = accessor; + + public Task InitiateAsync(PaymentInitiationRequest request, CancellationToken ct) + => _accessor.Client.PostAsync(CsobApiPaths.Payments, request, ct); + + public Task DetailAsync(string id, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.Payment(id), null, ct); + + public Task StatusAsync(string id, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.PaymentStatus(id), null, ct); + + public Task CancelAsync(string id, CancellationToken ct) + => _accessor.Client.DeleteAsync(CsobApiPaths.Payment(id), ct); + + /// Starts transaction authorization (SCA). Returns the redirect details for the PSU. + public Task StartSignAsync(string id, string signId, JsonNode? body, CancellationToken ct) + => _accessor.Client.PostAsync(CsobApiPaths.PaymentSign(id, signId), body, ct); + + /// Gets the current state of an authorization (sign) transaction. + public Task SignStatusAsync(string id, string signId, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.PaymentSign(id, signId), null, ct); + + /// Finalizes an authorization (sign) transaction. + public Task FinalizeSignAsync(string id, string signId, JsonNode? body, CancellationToken ct) + => _accessor.Client.PutAsync(CsobApiPaths.PaymentSign(id, signId), body, ct); +} diff --git a/Services/StandingOrdersService.cs b/Services/StandingOrdersService.cs new file mode 100644 index 0000000..44ee1aa --- /dev/null +++ b/Services/StandingOrdersService.cs @@ -0,0 +1,34 @@ +using System.Text.Json.Nodes; +using Csob.Client; +using Csob.Models; + +namespace Csob.Services; + +/// PISP – standing-order initiation, detail/status, cancellation and the sign (SCA) flow. +public sealed class StandingOrdersService +{ + private readonly CsobApiAccessor _accessor; + + public StandingOrdersService(CsobApiAccessor accessor) => _accessor = accessor; + + public Task InitiateAsync(StandingOrderInitiationRequest request, CancellationToken ct) + => _accessor.Client.PostAsync(CsobApiPaths.StandingOrders, request, ct); + + public Task DetailAsync(string id, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.StandingOrder(id), null, ct); + + public Task StatusAsync(string id, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.StandingOrderStatus(id), null, ct); + + public Task CancelAsync(string id, CancellationToken ct) + => _accessor.Client.DeleteAsync(CsobApiPaths.StandingOrder(id), ct); + + public Task StartSignAsync(string id, string signId, JsonNode? body, CancellationToken ct) + => _accessor.Client.PostAsync(CsobApiPaths.StandingOrderSign(id, signId), body, ct); + + public Task SignStatusAsync(string id, string signId, CancellationToken ct) + => _accessor.Client.GetAsync(CsobApiPaths.StandingOrderSign(id, signId), null, ct); + + public Task FinalizeSignAsync(string id, string signId, JsonNode? body, CancellationToken ct) + => _accessor.Client.PutAsync(CsobApiPaths.StandingOrderSign(id, signId), body, ct); +} diff --git a/bin/Release/net8.0/Csob.deps.json b/bin/Release/net8.0/Csob.deps.json new file mode 100644 index 0000000..103545a --- /dev/null +++ b/bin/Release/net8.0/Csob.deps.json @@ -0,0 +1,106 @@ +{ + "runtimeTarget": { + "name": ".NETCoreApp,Version=v8.0", + "signature": "" + }, + "compilationOptions": {}, + "targets": { + ".NETCoreApp,Version=v8.0": { + "Csob/1.0.0": { + "dependencies": { + "Swashbuckle.AspNetCore": "6.9.0" + }, + "runtime": { + "Csob.dll": {} + } + }, + "Microsoft.OpenApi/1.6.14": { + "runtime": { + "lib/netstandard2.0/Microsoft.OpenApi.dll": { + "assemblyVersion": "1.6.14.0", + "fileVersion": "1.6.14.0" + } + } + }, + "Swashbuckle.AspNetCore/6.9.0": { + "dependencies": { + "Swashbuckle.AspNetCore.Swagger": "6.9.0", + "Swashbuckle.AspNetCore.SwaggerGen": "6.9.0", + "Swashbuckle.AspNetCore.SwaggerUI": "6.9.0" + } + }, + "Swashbuckle.AspNetCore.Swagger/6.9.0": { + "dependencies": { + "Microsoft.OpenApi": "1.6.14" + }, + "runtime": { + "lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": { + "assemblyVersion": "6.9.0.0", + "fileVersion": "6.9.0.799" + } + } + }, + "Swashbuckle.AspNetCore.SwaggerGen/6.9.0": { + "dependencies": { + "Swashbuckle.AspNetCore.Swagger": "6.9.0" + }, + "runtime": { + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": { + "assemblyVersion": "6.9.0.0", + "fileVersion": "6.9.0.799" + } + } + }, + "Swashbuckle.AspNetCore.SwaggerUI/6.9.0": { + "runtime": { + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": { + "assemblyVersion": "6.9.0.0", + "fileVersion": "6.9.0.799" + } + } + } + } + }, + "libraries": { + "Csob/1.0.0": { + "type": "project", + "serviceable": false, + "sha512": "" + }, + "Microsoft.OpenApi/1.6.14": { + "type": "package", + "serviceable": true, + "sha512": "sha512-tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==", + "path": "microsoft.openapi/1.6.14", + "hashPath": "microsoft.openapi.1.6.14.nupkg.sha512" + }, + "Swashbuckle.AspNetCore/6.9.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-lvI+XHF21tkwXd2nDCLGJsdhdUYsY3Ax2fWUlvw81Oa6EedtnIAf5tThy8ZnPcz/9/TwsLgjgtX9ifOCIjbEPA==", + "path": "swashbuckle.aspnetcore/6.9.0", + "hashPath": "swashbuckle.aspnetcore.6.9.0.nupkg.sha512" + }, + "Swashbuckle.AspNetCore.Swagger/6.9.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-P316kpxx5DnDvJwNWW8iTAXkh9DVenAxFGe9v4OUS0gil+vitH7F1feXhCtVeHN/616EFNTMh4pV2lcr9kkw/w==", + "path": "swashbuckle.aspnetcore.swagger/6.9.0", + "hashPath": "swashbuckle.aspnetcore.swagger.6.9.0.nupkg.sha512" + }, + "Swashbuckle.AspNetCore.SwaggerGen/6.9.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-FjeMR3fBzwVc5plfYjoHw9ptf8SOWMupvO9X35J5EgzT3L9dRqSxa+cBKzL8PwCyemY0xNrggQSB5+MFWx1axg==", + "path": "swashbuckle.aspnetcore.swaggergen/6.9.0", + "hashPath": "swashbuckle.aspnetcore.swaggergen.6.9.0.nupkg.sha512" + }, + "Swashbuckle.AspNetCore.SwaggerUI/6.9.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-0OxlWBFLl2gUESZX/K7QCTz9KctKy0VxHTvLIBcyWGD4z/fv5MCMW02qzYGcReLJr4yBnNDRzApKtLh6oBpe9A==", + "path": "swashbuckle.aspnetcore.swaggerui/6.9.0", + "hashPath": "swashbuckle.aspnetcore.swaggerui.6.9.0.nupkg.sha512" + } + } +} \ No newline at end of file diff --git a/bin/Release/net8.0/Csob.dll b/bin/Release/net8.0/Csob.dll new file mode 100644 index 0000000..f133367 Binary files /dev/null and b/bin/Release/net8.0/Csob.dll differ diff --git a/bin/Release/net8.0/Csob.exe b/bin/Release/net8.0/Csob.exe new file mode 100644 index 0000000..b47b671 Binary files /dev/null and b/bin/Release/net8.0/Csob.exe differ diff --git a/bin/Release/net8.0/Csob.pdb b/bin/Release/net8.0/Csob.pdb new file mode 100644 index 0000000..fab13fe Binary files /dev/null and b/bin/Release/net8.0/Csob.pdb differ diff --git a/bin/Release/net8.0/Csob.runtimeconfig.json b/bin/Release/net8.0/Csob.runtimeconfig.json new file mode 100644 index 0000000..6a48a7e --- /dev/null +++ b/bin/Release/net8.0/Csob.runtimeconfig.json @@ -0,0 +1,20 @@ +{ + "runtimeOptions": { + "tfm": "net8.0", + "frameworks": [ + { + "name": "Microsoft.NETCore.App", + "version": "8.0.0" + }, + { + "name": "Microsoft.AspNetCore.App", + "version": "8.0.0" + } + ], + "configProperties": { + "System.GC.Server": true, + "System.Reflection.Metadata.MetadataUpdater.IsSupported": false, + "System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false + } + } +} \ No newline at end of file diff --git a/bin/Release/net8.0/Csob.staticwebassets.endpoints.json b/bin/Release/net8.0/Csob.staticwebassets.endpoints.json new file mode 100644 index 0000000..5576e88 --- /dev/null +++ b/bin/Release/net8.0/Csob.staticwebassets.endpoints.json @@ -0,0 +1 @@ +{"Version":1,"ManifestType":"Build","Endpoints":[]} \ No newline at end of file diff --git a/bin/Release/net8.0/Csob.xml b/bin/Release/net8.0/Csob.xml new file mode 100644 index 0000000..3c80cbb --- /dev/null +++ b/bin/Release/net8.0/Csob.xml @@ -0,0 +1,502 @@ + + + + Csob + + + + + Scoped accessor that resolves the credentials for the current request and lazily builds a + single (bound to the request's mutual-TLS client) shared by all + services handling that request. + + + + + Thin HTTP wrapper around the ČSOB PSD2 resource API for a single request. It attaches the + mandatory COBS headers (Authorization, APIKEY, TPP-Name, X-Request-ID, + Date, User-Involved), sends the call over the per-request mutual-TLS client and + returns the response JSON verbatim () so no field is lost in translation. + Non-success responses become a . + + + + Web defaults (camelCase) match the COBS JSON contract; null properties are omitted on write. + + + Best-effort parse of the COBS error shape { "errors": [ { "error": "CODE" } ] }. + + + + Raised when the ČSOB PSD2 API returns a non-success HTTP status. Carries the upstream status + and the raw error body so the middleware can surface it without leaking credentials. + + + + Machine-readable error codes parsed from the ČSOB errors[].error array (best effort). + + + Raw response body (already credential-free — it is the upstream's own error payload). + + + + Central registry of ČSOB PSD2 resource path templates (relative to CSOB_API_BASE_URL). + + Paths follow the Czech Open Banking Standard (COBS) as implemented by ČSOB. The account-scoped + AISP paths and the PISP /my/payments/.../sign/{signId} authorization flow are confirmed + against the ČSOB developer portal; the remaining COBS resources use the same /my/ prefix. + Keep every path here so a portal-specific correction is a single-file change. + + + + + Provides instances configured for mutual TLS with a per-request eIDAS + client certificate. ČSOB requires the client certificate at the transport layer, so a single + shared client cannot be used across tenants. + + Clients are cached by certificate thumbprint and reused for connection pooling. In practice the + certificate identifies the TPP application (not the PSU), so the number of distinct certificates + is small and bounded by the set of calling tenants. Certificates and clients live in memory only + and never touch disk. + + + + + Returns a (cached) mutual-TLS presenting . + A fresh certificate instance is built per request; if an equivalent one (same thumbprint) is + already cached, the redundant instance is disposed so it does not leak. + + + + + Service configuration resolved from environment variables. + + This service is a stateless, multi-tenant proxy in front of the ČSOB PSD2 (Open Banking) + API. Unlike the sibling iDoklad service, it holds no per-client secrets: the eIDAS + client certificate, OAuth client id/secret, access token, API key and TPP name are all + supplied per request as HTTP headers by the calling client service + (see ). Only non-secret infrastructure + configuration (API/OAuth base URLs, app metadata, reverse-proxy prefix, timeout) lives here. + + + + Public reverse-proxy prefix (e.g. /apps/csob) injected by AppFactory. + + + + Base URL of the ČSOB PSD2 resource API. Production default; override for any other + environment. All AISP/PISP/consent path templates are appended to this base. + + + + + OAuth2 authorization endpoint (Authorization Code flow, PSU redirect). Production default; + verify against the current ČSOB developer portal as the host may change. + + + + + OAuth2 token endpoint (code->token and refresh). Production default; verify against the + current ČSOB developer portal. + + + + Upstream HTTP request timeout in seconds. + + + + AISP – account information. All endpoints require the ČSOB credential headers + (eIDAS certificate, access token, API key, TPP name). See the Swagger description for details. + + + + List the PSU's payment accounts (paged). + + + Get the balance(s) of an account. + + + List booked transactions of an account (paged, optional date range). + + + List awaiting (pending) transactions of an account. + + + List the account's existing standing orders. + + + Get a standing-order detail for the account. + + + List the account's direct-debit mandates. + + + + Common – PSU consent lifecycle. Requires the ČSOB credential headers. The consent body is + forwarded to ČSOB as raw JSON (COBS consent shape). + + + + Create a consent. + + + Get a consent detail. + + + Revoke a consent. + + + + PISP – direct-debit mandate initiation and its authorization (sign) flow. Requires the ČSOB + credential headers. The mandate body is forwarded to ČSOB as raw JSON (COBS direct-debit shape). + + + + Initiate a direct-debit mandate. + + + Get the direct-debit mandate detail. + + + Get the direct-debit instruction status. + + + Revoke a direct-debit mandate. + + + Start the authorization (SCA) of a direct-debit mandate. Returns the PSU redirect details. + + + Get the current state of an authorization (sign) transaction. + + + Finalize an authorization (sign) transaction. + + + Service metadata endpoints. These do not require ČSOB credentials. + + + Liveness probe. + + + Service name, version and the configured upstream endpoints (no secrets). + + + + Reports the non-secret configuration. This service is stateless and multi-tenant: it holds + no credentials, so there is nothing per-client to report — every secret is supplied per + request via headers. + + + + + OAuth2 Authorization Code helper for the ČSOB PSD2 PSU consent flow. Build the authorization URL, + redirect the PSU to it, then exchange the returned code for tokens. The token/refresh calls run + over mutual TLS, so they require the X-CSOB-Certificate header; client id/secret are also + supplied per request (this service is multi-tenant). + + + + Build the ČSOB authorization URL to which the PSU must be redirected. + + + Exchange an authorization code for an access/refresh token (mutual TLS). + + + Refresh an access token using a refresh token (mutual TLS). + + + + PISP – single payment initiation and its authorization (sign) flow. Requires the ČSOB credential + headers. After POST /payments the response carries signInfo.signId; use it with the + sign endpoints to drive Strong Customer Authentication (SCA). + + + + Initiate a domestic (DMCT) or SEPA (ESCT) payment. + + + Get the full payment detail. + + + Get the payment instruction status. + + + Cancel a not-yet-authorized payment. + + + Start the authorization (SCA) of a payment. Returns the PSU redirect details. + + + Get the current state of an authorization (sign) transaction. + + + Finalize an authorization (sign) transaction. + + + PISP – standing-order initiation and its authorization (sign) flow. Requires the ČSOB credential headers. + + + Initiate a standing order. + + + Get the standing-order detail. + + + Get the standing-order instruction status. + + + Cancel a standing order. + + + Start the authorization (SCA) of a standing order. Returns the PSU redirect details. + + + Get the current state of an authorization (sign) transaction. + + + Finalize an authorization (sign) transaction. + + + + Names of the HTTP headers that carry per-request ČSOB credentials and context. + + This service is multi-tenant: it stores no credentials itself. Every sensitive value is + supplied per request in a header (never the query string or body) and is forwarded to ČSOB. + Headers must therefore only be sent over TLS. Nothing here is logged or persisted. + + + + + eIDAS client certificate (QWAC) as a Base64-encoded PKCS#12 / PFX bundle, including the + private key and the full chain. Used to establish the mutual-TLS connection to ČSOB. + Analogous to Node's https.Agent({ pfx, passphrase }). + + + + Optional passphrase protecting the PFX in . + + + OAuth2 Bearer access token obtained for the PSU; forwarded as Authorization: Bearer. + + + ČSOB application API key; forwarded as the APIKEY header. + + + TPP (third-party provider) organisation name; forwarded as the TPP-Name header. + + + OAuth2 client id of the registered TPP application (used by the OAuth helper endpoints). + + + OAuth2 client secret of the registered TPP application (used by the OAuth helper endpoints). + + + Whether the PSU is online/involved in the request; forwarded as User-Involved (default false). + + + PSU IP address; forwarded as User-IP-Address. + + + + Fully resolved set of per-request credentials and PSU context used to call the ČSOB PSD2 API. + Built from request headers by ; never logged. + + + + OAuth2 Bearer access token (forwarded as Authorization: Bearer). + + + ČSOB application API key (forwarded as APIKEY). + + + TPP organisation name (forwarded as TPP-Name). + + + + eIDAS client certificate (with private key) for mutual TLS. Optional at the type level so + metadata endpoints can resolve context, but required for any real upstream call. + + + + Whether the PSU is online for this request (User-Involved); defaults to false. + + + Optional PSU IP address (User-IP-Address). + + + + Raised when a request does not provide the credential headers required to call ČSOB. + Translated to HTTP 401 by the exception-handling middleware. + + + + + Resolves the ČSOB credentials and PSU context for the current request, exclusively from HTTP + headers (this service stores no secrets). Missing required headers produce a 401; a malformed + certificate or wrong passphrase produces a 400 (via ). + + + + Reads a single request header, returning null when absent or blank. + + + + Builds the eIDAS client certificate from the Base64 PFX header (+ optional passphrase). + Returns null when no certificate header is present. + + The header is not valid Base64 or the PFX/passphrase is invalid. + + + + Resolves the full credential set required for an AISP/PISP/consent call. Throws + if any required header is absent. + + + + + Documents the per-request credential headers in Swagger. Metadata endpoints need none; the + OAuth helper needs the certificate + client id/secret; every other (AISP/PISP/consent) endpoint + needs the certificate + access token + API key + TPP name. Headers are marked optional at the + schema level (the service validates them at runtime) but the descriptions state what is required. + + + + + Translates domain exceptions into JSON responses. Credentials are + never logged — only upstream status codes and error codes (the upstream's own payload). Errors + are always logged (no silent failures). + + + + Monetary amount with ISO 4217 currency. + + + Wrapper matching the amount object whose instructedAmount holds the value/currency. + + + Account number identification. For domestic/SEPA writes only iban is required. + + + Account reference with optional currency (debtorAccount/creditorAccount). + + + Payment scheme: DMCT (domestic), ESCT (SEPA), XBCT (cross-border), EXCT, NXCT. + + + NORM (default), HIGH (express) or INST (instant). + + + A party (debtor/creditor/ultimate*). Identification is variable, so kept as raw JSON. + + + + Remittance information. unstructured is free text (Czech symbols may be encoded as + /VS/.../SS/.../KS/...); structured varies (its reference may be a string or + an array) so it is passed through as raw JSON. + + + + Body for POST /oauth/token (Authorization Code grant). Secrets travel in headers. + + + Authorization code returned to the redirect URI after PSU consent. + + + Redirect URI registered for the TPP app; must match the one used to obtain the code. + + + Body for POST /oauth/refresh (Refresh Token grant). + + + Response of GET /oauth/authorization-url. + + + Fully-built ČSOB authorization URL to which the PSU must be redirected. + + + The opaque state value echoed back on the redirect (CSRF protection). + + + Unique instruction id assigned by the TPP (idempotency key). Max 35 chars. + + + + Payment initiation request (POST /my/payments). Covers domestic (DMCT) and SEPA (ESCT) + payments — SEPA-only fields (creditor address, agent, ultimate parties, purpose) are optional. + Any additional COBS field not modelled here is preserved via . + + + + Any COBS fields not explicitly modelled are forwarded to ČSOB unchanged. + + + DAILY, WEEKLY, MONTHLY, BI_MONTHLY, QUARTERLY, HALFYEARLY, YEARLY, SINGLE, IRREGULAR. + + + Day within the interval (e.g. day-of-month "25"). + + + e.g. MAX_AMOUNT_EXCEEDED, UNTIL_CANCELLATION. + + + Optional exceptions block (stoppages/breaks); shape varies, kept as raw JSON. + + + Optional validity block (lastExecutionDate/maxAmount); kept as raw JSON. + + + Standing-order initiation request (POST /my/standingorders). + + + AISP – account information (accounts, balance, transactions, standing orders, direct debits). + + + Optional filters for the transactions listing (forwarded as query parameters). + + + ISO date (YYYY-MM-DD) lower bound. + + + ISO date (YYYY-MM-DD) upper bound. + + + + Common – PSU consent lifecycle (create / detail / revoke). The consent request body varies + across COBS profiles, so it is accepted and forwarded as raw JSON. + + + + + PISP – direct-debit mandate initiation, detail/status, revocation and the sign (SCA) flow. + The mandate creation body varies across COBS profiles, so it is accepted and forwarded as raw JSON. + + + + + OAuth2 Authorization Code helper for the ČSOB PSD2 flow. Builds the PSU authorization URL and + exchanges/refreshes tokens against the ČSOB token endpoint. The token endpoint is behind mutual + TLS, so the client certificate header is required for the token/refresh calls. Client id/secret + are taken from per-request headers (this service is multi-tenant and stores no app credentials). + + + + Builds the authorization URL to which the PSU must be redirected. + + + PISP – single payment initiation, status/detail, cancellation and the sign (SCA) flow. + + + Starts transaction authorization (SCA). Returns the redirect details for the PSU. + + + Gets the current state of an authorization (sign) transaction. + + + Finalizes an authorization (sign) transaction. + + + PISP – standing-order initiation, detail/status, cancellation and the sign (SCA) flow. + + + diff --git a/bin/Release/net8.0/Microsoft.OpenApi.dll b/bin/Release/net8.0/Microsoft.OpenApi.dll new file mode 100644 index 0000000..aac9a6d Binary files /dev/null and b/bin/Release/net8.0/Microsoft.OpenApi.dll differ diff --git a/bin/Release/net8.0/Swashbuckle.AspNetCore.Swagger.dll b/bin/Release/net8.0/Swashbuckle.AspNetCore.Swagger.dll new file mode 100644 index 0000000..b473263 Binary files /dev/null and b/bin/Release/net8.0/Swashbuckle.AspNetCore.Swagger.dll differ diff --git a/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll b/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll new file mode 100644 index 0000000..288a90f Binary files /dev/null and b/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll differ diff --git a/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll b/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll new file mode 100644 index 0000000..71e6db4 Binary files /dev/null and b/bin/Release/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll differ diff --git a/bin/Release/net8.0/appsettings.json b/bin/Release/net8.0/appsettings.json new file mode 100644 index 0000000..10f68b8 --- /dev/null +++ b/bin/Release/net8.0/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/documentation/authentication.md b/documentation/authentication.md new file mode 100644 index 0000000..02ec4e0 --- /dev/null +++ b/documentation/authentication.md @@ -0,0 +1,86 @@ +# Autentizace a OAuth2 / SCA flow + +ČSOB PSD2 vyžaduje tři vrstvy: + +1. **Mutual TLS** s eIDAS QWAC certifikátem (na transportní vrstvě). +2. **APIKEY** identifikující registrovanou TPP aplikaci. +3. **OAuth2 Bearer access token** prokazující souhlas (consent) PSU – získaný Authorization Code flow. + +Tato služba je multi-tenant a **neukládá nic** – všechny tři vrstvy dostává per-request v hlavičkách. + +## Credential hlavičky + +| Hlavička | Povinná pro | Předáno do ČSOB jako | +|---|---|---| +| `X-CSOB-Certificate` (Base64 PFX) | vše (mTLS) | TLS klientský certifikát | +| `X-CSOB-Certificate-Password` | volitelné | – | +| `X-Access-Token` | AISP/PISP/consents | `Authorization: Bearer` | +| `X-API-Key` | AISP/PISP/consents | `APIKEY` | +| `X-TPP-Name` | AISP/PISP/consents | `TPP-Name` | +| `X-CSOB-Client-Id` | `/oauth/*` | OAuth `client_id` | +| `X-CSOB-Client-Secret` | `/oauth/token`, `/oauth/refresh` | OAuth `client_secret` | + +`X-Request-ID` (UUID) a `Date` (RFC 7231) doplňuje služba sama. + +### Příprava Base64 PFX + +```bash +base64 -w0 client_qwac.pfx # Linux/macOS -> hodnota X-CSOB-Certificate +certutil -encode client_qwac.pfx out.txt # Windows (odstranit hlavičky/řádky) +``` + +Heslo k PFX jde do `X-CSOB-Certificate-Password`. Hodnoty posílej **výhradně přes HTTPS**. + +## OAuth2 Authorization Code flow + +``` +1. GET /oauth/authorization-url?redirect_uri=...&scope=...&state=... + hlavičky: X-CSOB-Client-Id + -> { "authorizationUrl": "...", "state": "..." } + +2. Klient přesměruje PSU na authorizationUrl. PSU se přihlásí a udělí souhlas v ČSOB. + ČSOB přesměruje zpět na redirect_uri s parametrem ?code=...&state=... + +3. POST /oauth/token { "code": "...", "redirectUri": "..." } + hlavičky: X-CSOB-Certificate (+password), X-CSOB-Client-Id, X-CSOB-Client-Secret + -> { "access_token": "...", "refresh_token": "...", "expires_in": ... } + +4. AISP/PISP volání s hlavičkami: + X-CSOB-Certificate, X-Access-Token, X-API-Key, X-TPP-Name + +5. Obnova: POST /oauth/refresh { "refreshToken": "..." } (stejné hlavičky jako krok 3) +``` + +### Scopes (COBS) + +`aisp.accounts`, `aisp.balances`, `aisp.transactions`, `aisp.standingorders`, `aisp.directdebits`, +`aisp.notifications`, `pisp.payments`, `pisp.standingorders`, `pisp.directdebits`, `pisp.accounts`. + +## PISP autorizace platby (SCA / sign flow) + +``` +1. POST /payments (body s platbou) + -> odpověď obsahuje signInfo.signId a instructionStatus + +2. POST /payments/{id}/sign/{signId} + -> autorizační detaily pro PSU (např. authorizationType, href.url pro redirect SCA) + +3. (volitelně) GET /payments/{id}/sign/{signId} – stav autorizace + (volitelně) PUT /payments/{id}/sign/{signId} – finalizace + +4. GET /payments/{id}/status – konečný stav (ACTC, ACSC, RJCT, …) +``` + +Stejný sign flow platí pro `/standing-orders` a `/direct-debits`. + +## Chybové odpovědi + +Služba vrací `application/problem+json`: + +- `401` – chybí povinná credential hlavička (`missingHeaders`). +- `400` – neplatný Base64 PFX nebo špatné heslo certifikátu. +- upstream status (4xx/5xx) – chyba z ČSOB; pole `csobErrorCodes` a `upstreamBody` nesou originální payload. +- `502` – ČSOB nedostupné nebo selhal mTLS handshake (např. neplatný/expirovaný certifikát). +- `504` – timeout. + +Secrets se nikdy nelogují ani nevracejí. diff --git a/documentation/overview.md b/documentation/overview.md new file mode 100644 index 0000000..31b2786 --- /dev/null +++ b/documentation/overview.md @@ -0,0 +1,54 @@ +# ČSOB PSD2 služba – přehled + +Tato služba je tenký, **bezstavový multi-tenant proxy** mezi klientskými aplikacemi a produkčním +**ČSOB PSD2 (Open Banking) API** (Czech Open Banking Standard – COBS). Vystavuje vlastní REST endpointy +a každý request 1:1 přeloží na odpovídající ČSOB volání, přičemž doplní povinné COBS hlavičky a naváže +mutual TLS pomocí certifikátu předaného v hlavičce. + +## Klíčové principy + +- **Žádné uložené secrets.** Certifikát, OAuth client id/secret, access token, APIKEY i TPP name přicházejí + per-request v hlavičkách (viz [authentication.md](authentication.md)). V env jsou jen ne-secret URL a metadata. +- **Věrný průchod dat.** Čtecí (GET) odpovědi a méně stabilní těla (consent, direct-debit, sign) se předávají + jako surové JSON (`JsonNode`), takže se neztrácí žádné pole. Typovaná těla mají jen platba a trvalý příkaz + (pro kvalitní Swagger), s `additionalData` pro forward-kompatibilitu. +- **mTLS per-request.** Certifikát z `X-CSOB-Certificate` (Base64 PFX) se načte do paměti (nikdy na disk) + a použije pro TLS handshake. `HttpClient` se cachuje podle thumbprintu certifikátu kvůli znovupoužití spojení. +- **Reverse proxy.** OpenAPI `servers` se nastavuje z `ROOT_PATH`, takže Swagger „Try it out“ volá přes + `/apps/csob/...` (Caddy `handle_path` prefix předtím odstraní). + +## Mapování na ČSOB + +| Tato služba | ČSOB PSD2 (relativně k `CSOB_API_BASE_URL`) | +|---|---| +| `GET /accounts` | `GET /my/accounts` | +| `GET /accounts/{id}/balance` | `GET /my/accounts/{id}/balance` | +| `GET /accounts/{id}/transactions` | `GET /my/accounts/{id}/transactions` | +| `GET /accounts/{id}/transactions/awaiting` | `GET /my/accounts/{id}/transactions/awaiting` | +| `GET /accounts/{id}/standing-orders` | `GET /my/accounts/{id}/standingorders` | +| `GET /accounts/{id}/direct-debits` | `GET /my/accounts/{id}/directdebits` | +| `POST /payments` | `POST /my/payments` | +| `GET /payments/{id}` / `/status` | `GET /my/payments/{id}` / `/status` | +| `POST|GET|PUT /payments/{id}/sign/{signId}` | `…/my/payments/{id}/sign/{signId}` | +| `…/standing-orders…` | `…/my/standingorders…` | +| `…/direct-debits…` | `…/my/directdebits…` | +| `POST|GET|DELETE /consents…` | `…/consents…` | + +Cesty jsou centralizované v `Client/CsobApiPaths.cs`. + +## Co je vědomě mimo rozsah (zatím nenapojeno) + +V souladu s philosophy sesterské služby `idoklad` (dokumentovat, co není napojeno): + +- **Batch payments** (`/batchpayments`) a **co-signing list** (`/authorizations`) z COBS PISP. +- **PIISP** balance check (`/balanceCheck`). +- **Šifrování hlavičkových credentials** (jako v microsoft-365-service) – odloženo, spoléháme na TLS. + +## Ověření a rizika + +- Build je čistý (0 warnings). Lokálně ověřeno: `/health`, `/docs`, OpenAPI `servers`=`/apps/csob`, + 401 bez credentials, 400 při neplatném certifikátu, dokumentace credential hlaviček per operace. +- **Reálné ČSOB volání** vyžaduje platnou PSD2 licenci + eIDAS QWAC certifikát, takže ho nelze otestovat bez + produkčních údajů klienta. +- **OAuth authorize/token URL** a přesný tvar `sign` sub-cest pocházejí z výzkumu COBS + ČSOB portálu; + URL jsou env-konfigurovatelné a cesty centralizované – při odchylce ČSOB portálu je oprava jednoho místa. diff --git a/obj/Csob.csproj.nuget.dgspec.json b/obj/Csob.csproj.nuget.dgspec.json new file mode 100644 index 0000000..f6e0ade --- /dev/null +++ b/obj/Csob.csproj.nuget.dgspec.json @@ -0,0 +1,84 @@ +{ + "format": 1, + "restore": { + "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj": {} + }, + "projects": { + "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj", + "projectName": "Csob", + "projectPath": "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj", + "packagesPath": "C:\\Users\\GamingPC\\.nuget\\packages\\", + "outputPath": "d:\\GitHubRepository\\Hracicky\\x\\csob\\obj\\", + "projectStyle": "PackageReference", + "fallbackFolders": [ + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" + ], + "configFilePaths": [ + "C:\\Users\\GamingPC\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, + "C:\\Program Files\\dotnet\\library-packs": {}, + "D:\\CustomNuGetPackages": {}, + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.200" + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "dependencies": { + "Swashbuckle.AspNetCore": { + "target": "Package", + "version": "[6.9.0, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.AspNetCore.App": { + "privateAssets": "none" + }, + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.204/PortableRuntimeIdentifierGraph.json" + } + } + } + } +} \ No newline at end of file diff --git a/obj/Csob.csproj.nuget.g.props b/obj/Csob.csproj.nuget.g.props new file mode 100644 index 0000000..e76774a --- /dev/null +++ b/obj/Csob.csproj.nuget.g.props @@ -0,0 +1,23 @@ + + + + True + NuGet + $(MSBuildThisFileDirectory)project.assets.json + $(UserProfile)\.nuget\packages\ + C:\Users\GamingPC\.nuget\packages\;C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages + PackageReference + 7.0.0 + + + + + + + + + + + C:\Users\GamingPC\.nuget\packages\microsoft.extensions.apidescription.server\6.0.5 + + \ No newline at end of file diff --git a/obj/Csob.csproj.nuget.g.targets b/obj/Csob.csproj.nuget.g.targets new file mode 100644 index 0000000..eea8d76 --- /dev/null +++ b/obj/Csob.csproj.nuget.g.targets @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs new file mode 100644 index 0000000..2217181 --- /dev/null +++ b/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] diff --git a/obj/Debug/net8.0/Csob.AssemblyInfo.cs b/obj/Debug/net8.0/Csob.AssemblyInfo.cs new file mode 100644 index 0000000..cd1ad5a --- /dev/null +++ b/obj/Debug/net8.0/Csob.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("Csob")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f44ffae9fc38f3033750d626a06752e62097876f")] +[assembly: System.Reflection.AssemblyProductAttribute("Csob")] +[assembly: System.Reflection.AssemblyTitleAttribute("Csob")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Generated by the MSBuild WriteCodeFragment class. + diff --git a/obj/Debug/net8.0/Csob.AssemblyInfoInputs.cache b/obj/Debug/net8.0/Csob.AssemblyInfoInputs.cache new file mode 100644 index 0000000..62a3e1b --- /dev/null +++ b/obj/Debug/net8.0/Csob.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +0991943a86580b954544318f8a8536f244a189324540e319d6b1d62a8de99070 diff --git a/obj/Debug/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig b/obj/Debug/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..fd0dccc --- /dev/null +++ b/obj/Debug/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,23 @@ +is_global = true +build_property.TargetFramework = net8.0 +build_property.TargetFrameworkIdentifier = .NETCoreApp +build_property.TargetFrameworkVersion = v8.0 +build_property.TargetPlatformMinVersion = +build_property.UsingMicrosoftNETSdkWeb = true +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property.EnforceExtendedAnalyzerRules = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = Csob +build_property.RootNamespace = Csob +build_property.ProjectDir = d:\GitHubRepository\Hracicky\x\csob\ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.RazorLangVersion = 8.0 +build_property.SupportLocalizedComponentNames = +build_property.GenerateRazorMetadataSourceChecksumAttributes = +build_property.MSBuildProjectDirectory = d:\GitHubRepository\Hracicky\x\csob +build_property._RazorSourceGeneratorDebug = +build_property.EffectiveAnalysisLevelStyle = 8.0 +build_property.EnableCodeStyleSeverity = diff --git a/obj/Debug/net8.0/Csob.GlobalUsings.g.cs b/obj/Debug/net8.0/Csob.GlobalUsings.g.cs new file mode 100644 index 0000000..5e6145d --- /dev/null +++ b/obj/Debug/net8.0/Csob.GlobalUsings.g.cs @@ -0,0 +1,17 @@ +// +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Routing; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Net.Http; +global using System.Net.Http.Json; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/obj/Debug/net8.0/Csob.assets.cache b/obj/Debug/net8.0/Csob.assets.cache new file mode 100644 index 0000000..991f750 Binary files /dev/null and b/obj/Debug/net8.0/Csob.assets.cache differ diff --git a/obj/Debug/net8.0/Csob.csproj.AssemblyReference.cache b/obj/Debug/net8.0/Csob.csproj.AssemblyReference.cache new file mode 100644 index 0000000..ccf1f85 Binary files /dev/null and b/obj/Debug/net8.0/Csob.csproj.AssemblyReference.cache differ diff --git a/obj/Release/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs b/obj/Release/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs new file mode 100644 index 0000000..2217181 --- /dev/null +++ b/obj/Release/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v8.0", FrameworkDisplayName = ".NET 8.0")] diff --git a/obj/Release/net8.0/Csob.AssemblyInfo.cs b/obj/Release/net8.0/Csob.AssemblyInfo.cs new file mode 100644 index 0000000..7ee9422 --- /dev/null +++ b/obj/Release/net8.0/Csob.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("Csob")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+f44ffae9fc38f3033750d626a06752e62097876f")] +[assembly: System.Reflection.AssemblyProductAttribute("Csob")] +[assembly: System.Reflection.AssemblyTitleAttribute("Csob")] +[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] + +// Generated by the MSBuild WriteCodeFragment class. + diff --git a/obj/Release/net8.0/Csob.AssemblyInfoInputs.cache b/obj/Release/net8.0/Csob.AssemblyInfoInputs.cache new file mode 100644 index 0000000..38a8fe8 --- /dev/null +++ b/obj/Release/net8.0/Csob.AssemblyInfoInputs.cache @@ -0,0 +1 @@ +60c4996bbd19af9863eeaf63653e6911961f28af17e965a302a0cd7e40f09823 diff --git a/obj/Release/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig b/obj/Release/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig new file mode 100644 index 0000000..fd0dccc --- /dev/null +++ b/obj/Release/net8.0/Csob.GeneratedMSBuildEditorConfig.editorconfig @@ -0,0 +1,23 @@ +is_global = true +build_property.TargetFramework = net8.0 +build_property.TargetFrameworkIdentifier = .NETCoreApp +build_property.TargetFrameworkVersion = v8.0 +build_property.TargetPlatformMinVersion = +build_property.UsingMicrosoftNETSdkWeb = true +build_property.ProjectTypeGuids = +build_property.InvariantGlobalization = +build_property.PlatformNeutralAssembly = +build_property.EnforceExtendedAnalyzerRules = +build_property._SupportedPlatformList = Linux,macOS,Windows +build_property.RootNamespace = Csob +build_property.RootNamespace = Csob +build_property.ProjectDir = d:\GitHubRepository\Hracicky\x\csob\ +build_property.EnableComHosting = +build_property.EnableGeneratedComInterfaceComImportInterop = +build_property.RazorLangVersion = 8.0 +build_property.SupportLocalizedComponentNames = +build_property.GenerateRazorMetadataSourceChecksumAttributes = +build_property.MSBuildProjectDirectory = d:\GitHubRepository\Hracicky\x\csob +build_property._RazorSourceGeneratorDebug = +build_property.EffectiveAnalysisLevelStyle = 8.0 +build_property.EnableCodeStyleSeverity = diff --git a/obj/Release/net8.0/Csob.GlobalUsings.g.cs b/obj/Release/net8.0/Csob.GlobalUsings.g.cs new file mode 100644 index 0000000..5e6145d --- /dev/null +++ b/obj/Release/net8.0/Csob.GlobalUsings.g.cs @@ -0,0 +1,17 @@ +// +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Routing; +global using Microsoft.Extensions.Configuration; +global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.Hosting; +global using Microsoft.Extensions.Logging; +global using System; +global using System.Collections.Generic; +global using System.IO; +global using System.Linq; +global using System.Net.Http; +global using System.Net.Http.Json; +global using System.Threading; +global using System.Threading.Tasks; diff --git a/obj/Release/net8.0/Csob.MvcApplicationPartsAssemblyInfo.cache b/obj/Release/net8.0/Csob.MvcApplicationPartsAssemblyInfo.cache new file mode 100644 index 0000000..e69de29 diff --git a/obj/Release/net8.0/Csob.MvcApplicationPartsAssemblyInfo.cs b/obj/Release/net8.0/Csob.MvcApplicationPartsAssemblyInfo.cs new file mode 100644 index 0000000..5c337f8 --- /dev/null +++ b/obj/Release/net8.0/Csob.MvcApplicationPartsAssemblyInfo.cs @@ -0,0 +1,16 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: Microsoft.AspNetCore.Mvc.ApplicationParts.ApplicationPartAttribute("Swashbuckle.AspNetCore.SwaggerGen")] + +// Generated by the MSBuild WriteCodeFragment class. + diff --git a/obj/Release/net8.0/Csob.assets.cache b/obj/Release/net8.0/Csob.assets.cache new file mode 100644 index 0000000..62d2b5d Binary files /dev/null and b/obj/Release/net8.0/Csob.assets.cache differ diff --git a/obj/Release/net8.0/Csob.csproj.AssemblyReference.cache b/obj/Release/net8.0/Csob.csproj.AssemblyReference.cache new file mode 100644 index 0000000..ccf1f85 Binary files /dev/null and b/obj/Release/net8.0/Csob.csproj.AssemblyReference.cache differ diff --git a/obj/Release/net8.0/Csob.csproj.CoreCompileInputs.cache b/obj/Release/net8.0/Csob.csproj.CoreCompileInputs.cache new file mode 100644 index 0000000..7816add --- /dev/null +++ b/obj/Release/net8.0/Csob.csproj.CoreCompileInputs.cache @@ -0,0 +1 @@ +4fe5ffce92f1961f66e6e5263d5719ffa1de25f67d0273a38e9aa8b4e6cbe7ab diff --git a/obj/Release/net8.0/Csob.csproj.FileListAbsolute.txt b/obj/Release/net8.0/Csob.csproj.FileListAbsolute.txt new file mode 100644 index 0000000..95b7d87 --- /dev/null +++ b/obj/Release/net8.0/Csob.csproj.FileListAbsolute.txt @@ -0,0 +1,36 @@ +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\appsettings.json +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.staticwebassets.endpoints.json +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.exe +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.deps.json +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.runtimeconfig.json +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.dll +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.pdb +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Csob.xml +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Microsoft.OpenApi.dll +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Swashbuckle.AspNetCore.Swagger.dll +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Swashbuckle.AspNetCore.SwaggerGen.dll +d:\GitHubRepository\Hracicky\x\csob\bin\Release\net8.0\Swashbuckle.AspNetCore.SwaggerUI.dll +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.csproj.AssemblyReference.cache +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\rpswa.dswa.cache.json +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.GeneratedMSBuildEditorConfig.editorconfig +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.AssemblyInfoInputs.cache +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.AssemblyInfo.cs +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.csproj.CoreCompileInputs.cache +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.MvcApplicationPartsAssemblyInfo.cs +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.MvcApplicationPartsAssemblyInfo.cache +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\rjimswa.dswa.cache.json +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\rjsmrazor.dswa.cache.json +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\rjsmcshtml.dswa.cache.json +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\scopedcss\bundle\Csob.styles.css +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\staticwebassets.build.json +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\staticwebassets.build.json.cache +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\staticwebassets.development.json +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\staticwebassets.build.endpoints.json +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\swae.build.ex.cache +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.csproj.Up2Date +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.dll +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\refint\Csob.dll +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.xml +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.pdb +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\Csob.genruntimeconfig.cache +d:\GitHubRepository\Hracicky\x\csob\obj\Release\net8.0\ref\Csob.dll diff --git a/obj/Release/net8.0/Csob.csproj.Up2Date b/obj/Release/net8.0/Csob.csproj.Up2Date new file mode 100644 index 0000000..e69de29 diff --git a/obj/Release/net8.0/Csob.dll b/obj/Release/net8.0/Csob.dll new file mode 100644 index 0000000..f133367 Binary files /dev/null and b/obj/Release/net8.0/Csob.dll differ diff --git a/obj/Release/net8.0/Csob.genruntimeconfig.cache b/obj/Release/net8.0/Csob.genruntimeconfig.cache new file mode 100644 index 0000000..5e561ad --- /dev/null +++ b/obj/Release/net8.0/Csob.genruntimeconfig.cache @@ -0,0 +1 @@ +aa0d3e069791e9af17dc7d6aa9ccb6ac770b84a356fe5664aef85ad9983cc8d5 diff --git a/obj/Release/net8.0/Csob.pdb b/obj/Release/net8.0/Csob.pdb new file mode 100644 index 0000000..fab13fe Binary files /dev/null and b/obj/Release/net8.0/Csob.pdb differ diff --git a/obj/Release/net8.0/Csob.xml b/obj/Release/net8.0/Csob.xml new file mode 100644 index 0000000..3c80cbb --- /dev/null +++ b/obj/Release/net8.0/Csob.xml @@ -0,0 +1,502 @@ + + + + Csob + + + + + Scoped accessor that resolves the credentials for the current request and lazily builds a + single (bound to the request's mutual-TLS client) shared by all + services handling that request. + + + + + Thin HTTP wrapper around the ČSOB PSD2 resource API for a single request. It attaches the + mandatory COBS headers (Authorization, APIKEY, TPP-Name, X-Request-ID, + Date, User-Involved), sends the call over the per-request mutual-TLS client and + returns the response JSON verbatim () so no field is lost in translation. + Non-success responses become a . + + + + Web defaults (camelCase) match the COBS JSON contract; null properties are omitted on write. + + + Best-effort parse of the COBS error shape { "errors": [ { "error": "CODE" } ] }. + + + + Raised when the ČSOB PSD2 API returns a non-success HTTP status. Carries the upstream status + and the raw error body so the middleware can surface it without leaking credentials. + + + + Machine-readable error codes parsed from the ČSOB errors[].error array (best effort). + + + Raw response body (already credential-free — it is the upstream's own error payload). + + + + Central registry of ČSOB PSD2 resource path templates (relative to CSOB_API_BASE_URL). + + Paths follow the Czech Open Banking Standard (COBS) as implemented by ČSOB. The account-scoped + AISP paths and the PISP /my/payments/.../sign/{signId} authorization flow are confirmed + against the ČSOB developer portal; the remaining COBS resources use the same /my/ prefix. + Keep every path here so a portal-specific correction is a single-file change. + + + + + Provides instances configured for mutual TLS with a per-request eIDAS + client certificate. ČSOB requires the client certificate at the transport layer, so a single + shared client cannot be used across tenants. + + Clients are cached by certificate thumbprint and reused for connection pooling. In practice the + certificate identifies the TPP application (not the PSU), so the number of distinct certificates + is small and bounded by the set of calling tenants. Certificates and clients live in memory only + and never touch disk. + + + + + Returns a (cached) mutual-TLS presenting . + A fresh certificate instance is built per request; if an equivalent one (same thumbprint) is + already cached, the redundant instance is disposed so it does not leak. + + + + + Service configuration resolved from environment variables. + + This service is a stateless, multi-tenant proxy in front of the ČSOB PSD2 (Open Banking) + API. Unlike the sibling iDoklad service, it holds no per-client secrets: the eIDAS + client certificate, OAuth client id/secret, access token, API key and TPP name are all + supplied per request as HTTP headers by the calling client service + (see ). Only non-secret infrastructure + configuration (API/OAuth base URLs, app metadata, reverse-proxy prefix, timeout) lives here. + + + + Public reverse-proxy prefix (e.g. /apps/csob) injected by AppFactory. + + + + Base URL of the ČSOB PSD2 resource API. Production default; override for any other + environment. All AISP/PISP/consent path templates are appended to this base. + + + + + OAuth2 authorization endpoint (Authorization Code flow, PSU redirect). Production default; + verify against the current ČSOB developer portal as the host may change. + + + + + OAuth2 token endpoint (code->token and refresh). Production default; verify against the + current ČSOB developer portal. + + + + Upstream HTTP request timeout in seconds. + + + + AISP – account information. All endpoints require the ČSOB credential headers + (eIDAS certificate, access token, API key, TPP name). See the Swagger description for details. + + + + List the PSU's payment accounts (paged). + + + Get the balance(s) of an account. + + + List booked transactions of an account (paged, optional date range). + + + List awaiting (pending) transactions of an account. + + + List the account's existing standing orders. + + + Get a standing-order detail for the account. + + + List the account's direct-debit mandates. + + + + Common – PSU consent lifecycle. Requires the ČSOB credential headers. The consent body is + forwarded to ČSOB as raw JSON (COBS consent shape). + + + + Create a consent. + + + Get a consent detail. + + + Revoke a consent. + + + + PISP – direct-debit mandate initiation and its authorization (sign) flow. Requires the ČSOB + credential headers. The mandate body is forwarded to ČSOB as raw JSON (COBS direct-debit shape). + + + + Initiate a direct-debit mandate. + + + Get the direct-debit mandate detail. + + + Get the direct-debit instruction status. + + + Revoke a direct-debit mandate. + + + Start the authorization (SCA) of a direct-debit mandate. Returns the PSU redirect details. + + + Get the current state of an authorization (sign) transaction. + + + Finalize an authorization (sign) transaction. + + + Service metadata endpoints. These do not require ČSOB credentials. + + + Liveness probe. + + + Service name, version and the configured upstream endpoints (no secrets). + + + + Reports the non-secret configuration. This service is stateless and multi-tenant: it holds + no credentials, so there is nothing per-client to report — every secret is supplied per + request via headers. + + + + + OAuth2 Authorization Code helper for the ČSOB PSD2 PSU consent flow. Build the authorization URL, + redirect the PSU to it, then exchange the returned code for tokens. The token/refresh calls run + over mutual TLS, so they require the X-CSOB-Certificate header; client id/secret are also + supplied per request (this service is multi-tenant). + + + + Build the ČSOB authorization URL to which the PSU must be redirected. + + + Exchange an authorization code for an access/refresh token (mutual TLS). + + + Refresh an access token using a refresh token (mutual TLS). + + + + PISP – single payment initiation and its authorization (sign) flow. Requires the ČSOB credential + headers. After POST /payments the response carries signInfo.signId; use it with the + sign endpoints to drive Strong Customer Authentication (SCA). + + + + Initiate a domestic (DMCT) or SEPA (ESCT) payment. + + + Get the full payment detail. + + + Get the payment instruction status. + + + Cancel a not-yet-authorized payment. + + + Start the authorization (SCA) of a payment. Returns the PSU redirect details. + + + Get the current state of an authorization (sign) transaction. + + + Finalize an authorization (sign) transaction. + + + PISP – standing-order initiation and its authorization (sign) flow. Requires the ČSOB credential headers. + + + Initiate a standing order. + + + Get the standing-order detail. + + + Get the standing-order instruction status. + + + Cancel a standing order. + + + Start the authorization (SCA) of a standing order. Returns the PSU redirect details. + + + Get the current state of an authorization (sign) transaction. + + + Finalize an authorization (sign) transaction. + + + + Names of the HTTP headers that carry per-request ČSOB credentials and context. + + This service is multi-tenant: it stores no credentials itself. Every sensitive value is + supplied per request in a header (never the query string or body) and is forwarded to ČSOB. + Headers must therefore only be sent over TLS. Nothing here is logged or persisted. + + + + + eIDAS client certificate (QWAC) as a Base64-encoded PKCS#12 / PFX bundle, including the + private key and the full chain. Used to establish the mutual-TLS connection to ČSOB. + Analogous to Node's https.Agent({ pfx, passphrase }). + + + + Optional passphrase protecting the PFX in . + + + OAuth2 Bearer access token obtained for the PSU; forwarded as Authorization: Bearer. + + + ČSOB application API key; forwarded as the APIKEY header. + + + TPP (third-party provider) organisation name; forwarded as the TPP-Name header. + + + OAuth2 client id of the registered TPP application (used by the OAuth helper endpoints). + + + OAuth2 client secret of the registered TPP application (used by the OAuth helper endpoints). + + + Whether the PSU is online/involved in the request; forwarded as User-Involved (default false). + + + PSU IP address; forwarded as User-IP-Address. + + + + Fully resolved set of per-request credentials and PSU context used to call the ČSOB PSD2 API. + Built from request headers by ; never logged. + + + + OAuth2 Bearer access token (forwarded as Authorization: Bearer). + + + ČSOB application API key (forwarded as APIKEY). + + + TPP organisation name (forwarded as TPP-Name). + + + + eIDAS client certificate (with private key) for mutual TLS. Optional at the type level so + metadata endpoints can resolve context, but required for any real upstream call. + + + + Whether the PSU is online for this request (User-Involved); defaults to false. + + + Optional PSU IP address (User-IP-Address). + + + + Raised when a request does not provide the credential headers required to call ČSOB. + Translated to HTTP 401 by the exception-handling middleware. + + + + + Resolves the ČSOB credentials and PSU context for the current request, exclusively from HTTP + headers (this service stores no secrets). Missing required headers produce a 401; a malformed + certificate or wrong passphrase produces a 400 (via ). + + + + Reads a single request header, returning null when absent or blank. + + + + Builds the eIDAS client certificate from the Base64 PFX header (+ optional passphrase). + Returns null when no certificate header is present. + + The header is not valid Base64 or the PFX/passphrase is invalid. + + + + Resolves the full credential set required for an AISP/PISP/consent call. Throws + if any required header is absent. + + + + + Documents the per-request credential headers in Swagger. Metadata endpoints need none; the + OAuth helper needs the certificate + client id/secret; every other (AISP/PISP/consent) endpoint + needs the certificate + access token + API key + TPP name. Headers are marked optional at the + schema level (the service validates them at runtime) but the descriptions state what is required. + + + + + Translates domain exceptions into JSON responses. Credentials are + never logged — only upstream status codes and error codes (the upstream's own payload). Errors + are always logged (no silent failures). + + + + Monetary amount with ISO 4217 currency. + + + Wrapper matching the amount object whose instructedAmount holds the value/currency. + + + Account number identification. For domestic/SEPA writes only iban is required. + + + Account reference with optional currency (debtorAccount/creditorAccount). + + + Payment scheme: DMCT (domestic), ESCT (SEPA), XBCT (cross-border), EXCT, NXCT. + + + NORM (default), HIGH (express) or INST (instant). + + + A party (debtor/creditor/ultimate*). Identification is variable, so kept as raw JSON. + + + + Remittance information. unstructured is free text (Czech symbols may be encoded as + /VS/.../SS/.../KS/...); structured varies (its reference may be a string or + an array) so it is passed through as raw JSON. + + + + Body for POST /oauth/token (Authorization Code grant). Secrets travel in headers. + + + Authorization code returned to the redirect URI after PSU consent. + + + Redirect URI registered for the TPP app; must match the one used to obtain the code. + + + Body for POST /oauth/refresh (Refresh Token grant). + + + Response of GET /oauth/authorization-url. + + + Fully-built ČSOB authorization URL to which the PSU must be redirected. + + + The opaque state value echoed back on the redirect (CSRF protection). + + + Unique instruction id assigned by the TPP (idempotency key). Max 35 chars. + + + + Payment initiation request (POST /my/payments). Covers domestic (DMCT) and SEPA (ESCT) + payments — SEPA-only fields (creditor address, agent, ultimate parties, purpose) are optional. + Any additional COBS field not modelled here is preserved via . + + + + Any COBS fields not explicitly modelled are forwarded to ČSOB unchanged. + + + DAILY, WEEKLY, MONTHLY, BI_MONTHLY, QUARTERLY, HALFYEARLY, YEARLY, SINGLE, IRREGULAR. + + + Day within the interval (e.g. day-of-month "25"). + + + e.g. MAX_AMOUNT_EXCEEDED, UNTIL_CANCELLATION. + + + Optional exceptions block (stoppages/breaks); shape varies, kept as raw JSON. + + + Optional validity block (lastExecutionDate/maxAmount); kept as raw JSON. + + + Standing-order initiation request (POST /my/standingorders). + + + AISP – account information (accounts, balance, transactions, standing orders, direct debits). + + + Optional filters for the transactions listing (forwarded as query parameters). + + + ISO date (YYYY-MM-DD) lower bound. + + + ISO date (YYYY-MM-DD) upper bound. + + + + Common – PSU consent lifecycle (create / detail / revoke). The consent request body varies + across COBS profiles, so it is accepted and forwarded as raw JSON. + + + + + PISP – direct-debit mandate initiation, detail/status, revocation and the sign (SCA) flow. + The mandate creation body varies across COBS profiles, so it is accepted and forwarded as raw JSON. + + + + + OAuth2 Authorization Code helper for the ČSOB PSD2 flow. Builds the PSU authorization URL and + exchanges/refreshes tokens against the ČSOB token endpoint. The token endpoint is behind mutual + TLS, so the client certificate header is required for the token/refresh calls. Client id/secret + are taken from per-request headers (this service is multi-tenant and stores no app credentials). + + + + Builds the authorization URL to which the PSU must be redirected. + + + PISP – single payment initiation, status/detail, cancellation and the sign (SCA) flow. + + + Starts transaction authorization (SCA). Returns the redirect details for the PSU. + + + Gets the current state of an authorization (sign) transaction. + + + Finalizes an authorization (sign) transaction. + + + PISP – standing-order initiation, detail/status, cancellation and the sign (SCA) flow. + + + diff --git a/obj/Release/net8.0/apphost.exe b/obj/Release/net8.0/apphost.exe new file mode 100644 index 0000000..b47b671 Binary files /dev/null and b/obj/Release/net8.0/apphost.exe differ diff --git a/obj/Release/net8.0/ref/Csob.dll b/obj/Release/net8.0/ref/Csob.dll new file mode 100644 index 0000000..a029dbb Binary files /dev/null and b/obj/Release/net8.0/ref/Csob.dll differ diff --git a/obj/Release/net8.0/refint/Csob.dll b/obj/Release/net8.0/refint/Csob.dll new file mode 100644 index 0000000..a029dbb Binary files /dev/null and b/obj/Release/net8.0/refint/Csob.dll differ diff --git a/obj/Release/net8.0/rjsmcshtml.dswa.cache.json b/obj/Release/net8.0/rjsmcshtml.dswa.cache.json new file mode 100644 index 0000000..01f6eda --- /dev/null +++ b/obj/Release/net8.0/rjsmcshtml.dswa.cache.json @@ -0,0 +1 @@ +{"GlobalPropertiesHash":"cmk/g0ZNXu9gJ27DjjLcpkDBlTy8RbJJZDNXpi5zobE=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["RIECXlOw3nHsuvqvxZXaPj/jy\u002BfI4EndOE5nviSURLE=","7d9SLaipaa/OPEXpY1q2f1TJiMYg8zqpA6Pih/R8Tcs=","lyWTB1SAn8j26WrlgTpjAfgPiRk8CHzueKPU4K7RU/w=","AqlAhVdvD3E/meEAwnjKEEXVbI0nCoFmuUVcx4RWscw=","9bwgN9Y/AEO67vqkjG6ZJRLuRMuKd2T8iH5YYodZ8Qo=","qnfluIVdGAut31To7iOrysqO/P2XiKnuymbhW00B3F4=","lF1S3jjLqtF4rW5S0esV3nAvTIcaMB/6tBlOniDiUvI=","pHVlcHSyz8AmNqJCN6sger3xiF6u4d2yhyHAaRW4PsM="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/obj/Release/net8.0/rjsmrazor.dswa.cache.json b/obj/Release/net8.0/rjsmrazor.dswa.cache.json new file mode 100644 index 0000000..b5be5d0 --- /dev/null +++ b/obj/Release/net8.0/rjsmrazor.dswa.cache.json @@ -0,0 +1 @@ +{"GlobalPropertiesHash":"g3hu5zSe6T51z8fRKoyDb/XOC4JY4b05h9YvX9zmd1I=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["RIECXlOw3nHsuvqvxZXaPj/jy\u002BfI4EndOE5nviSURLE=","7d9SLaipaa/OPEXpY1q2f1TJiMYg8zqpA6Pih/R8Tcs=","lyWTB1SAn8j26WrlgTpjAfgPiRk8CHzueKPU4K7RU/w=","AqlAhVdvD3E/meEAwnjKEEXVbI0nCoFmuUVcx4RWscw=","9bwgN9Y/AEO67vqkjG6ZJRLuRMuKd2T8iH5YYodZ8Qo=","qnfluIVdGAut31To7iOrysqO/P2XiKnuymbhW00B3F4=","lF1S3jjLqtF4rW5S0esV3nAvTIcaMB/6tBlOniDiUvI=","pHVlcHSyz8AmNqJCN6sger3xiF6u4d2yhyHAaRW4PsM="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/obj/Release/net8.0/rpswa.dswa.cache.json b/obj/Release/net8.0/rpswa.dswa.cache.json new file mode 100644 index 0000000..5a0d787 --- /dev/null +++ b/obj/Release/net8.0/rpswa.dswa.cache.json @@ -0,0 +1 @@ +{"GlobalPropertiesHash":"s18Zph11o4XKbouZq9V6XR416tqQ2cEqoV1zo+Pq7KQ=","FingerprintPatternsHash":"gq3WsqcKBUGTSNle7RKKyXRIwh7M8ccEqOqYvIzoM04=","PropertyOverridesHash":"8ZRc1sGeVrPBx4lD717BgRaQekyh78QKV9SKsdt638U=","InputHashes":["RIECXlOw3nHsuvqvxZXaPj/jy\u002BfI4EndOE5nviSURLE="],"CachedAssets":{},"CachedCopyCandidates":{}} \ No newline at end of file diff --git a/obj/Release/net8.0/staticwebassets.build.endpoints.json b/obj/Release/net8.0/staticwebassets.build.endpoints.json new file mode 100644 index 0000000..5576e88 --- /dev/null +++ b/obj/Release/net8.0/staticwebassets.build.endpoints.json @@ -0,0 +1 @@ +{"Version":1,"ManifestType":"Build","Endpoints":[]} \ No newline at end of file diff --git a/obj/Release/net8.0/staticwebassets.build.json b/obj/Release/net8.0/staticwebassets.build.json new file mode 100644 index 0000000..107d3af --- /dev/null +++ b/obj/Release/net8.0/staticwebassets.build.json @@ -0,0 +1 @@ +{"Version":1,"Hash":"BhivtHlvAyC9wdGc/wHwm/2EEkmCBBuuzAERe0/lTAw=","Source":"Csob","BasePath":"/","Mode":"Root","ManifestType":"Build","ReferencedProjectsConfiguration":[],"DiscoveryPatterns":[],"Assets":[],"Endpoints":[]} \ No newline at end of file diff --git a/obj/Release/net8.0/staticwebassets.build.json.cache b/obj/Release/net8.0/staticwebassets.build.json.cache new file mode 100644 index 0000000..72741cd --- /dev/null +++ b/obj/Release/net8.0/staticwebassets.build.json.cache @@ -0,0 +1 @@ +BhivtHlvAyC9wdGc/wHwm/2EEkmCBBuuzAERe0/lTAw= \ No newline at end of file diff --git a/obj/Release/net8.0/swae.build.ex.cache b/obj/Release/net8.0/swae.build.ex.cache new file mode 100644 index 0000000..e69de29 diff --git a/obj/project.assets.json b/obj/project.assets.json new file mode 100644 index 0000000..42401c2 --- /dev/null +++ b/obj/project.assets.json @@ -0,0 +1,533 @@ +{ + "version": 3, + "targets": { + "net8.0": { + "Microsoft.Extensions.ApiDescription.Server/6.0.5": { + "type": "package", + "build": { + "build/Microsoft.Extensions.ApiDescription.Server.props": {}, + "build/Microsoft.Extensions.ApiDescription.Server.targets": {} + }, + "buildMultiTargeting": { + "buildMultiTargeting/Microsoft.Extensions.ApiDescription.Server.props": {}, + "buildMultiTargeting/Microsoft.Extensions.ApiDescription.Server.targets": {} + } + }, + "Microsoft.OpenApi/1.6.14": { + "type": "package", + "compile": { + "lib/netstandard2.0/Microsoft.OpenApi.dll": { + "related": ".pdb;.xml" + } + }, + "runtime": { + "lib/netstandard2.0/Microsoft.OpenApi.dll": { + "related": ".pdb;.xml" + } + } + }, + "Swashbuckle.AspNetCore/6.9.0": { + "type": "package", + "dependencies": { + "Microsoft.Extensions.ApiDescription.Server": "6.0.5", + "Swashbuckle.AspNetCore.Swagger": "6.9.0", + "Swashbuckle.AspNetCore.SwaggerGen": "6.9.0", + "Swashbuckle.AspNetCore.SwaggerUI": "6.9.0" + }, + "build": { + "build/Swashbuckle.AspNetCore.props": {} + }, + "buildMultiTargeting": { + "buildMultiTargeting/Swashbuckle.AspNetCore.props": {} + } + }, + "Swashbuckle.AspNetCore.Swagger/6.9.0": { + "type": "package", + "dependencies": { + "Microsoft.OpenApi": "1.6.14" + }, + "compile": { + "lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": { + "related": ".pdb;.xml" + } + }, + "runtime": { + "lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll": { + "related": ".pdb;.xml" + } + }, + "frameworkReferences": [ + "Microsoft.AspNetCore.App" + ] + }, + "Swashbuckle.AspNetCore.SwaggerGen/6.9.0": { + "type": "package", + "dependencies": { + "Swashbuckle.AspNetCore.Swagger": "6.9.0" + }, + "compile": { + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": { + "related": ".pdb;.xml" + } + }, + "runtime": { + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll": { + "related": ".pdb;.xml" + } + } + }, + "Swashbuckle.AspNetCore.SwaggerUI/6.9.0": { + "type": "package", + "compile": { + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": { + "related": ".pdb;.xml" + } + }, + "runtime": { + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll": { + "related": ".pdb;.xml" + } + }, + "frameworkReferences": [ + "Microsoft.AspNetCore.App" + ] + } + } + }, + "libraries": { + "Microsoft.Extensions.ApiDescription.Server/6.0.5": { + "sha512": "Ckb5EDBUNJdFWyajfXzUIMRkhf52fHZOQuuZg/oiu8y7zDCVwD0iHhew6MnThjHmevanpxL3f5ci2TtHQEN6bw==", + "type": "package", + "path": "microsoft.extensions.apidescription.server/6.0.5", + "hasTools": true, + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "Icon.png", + "build/Microsoft.Extensions.ApiDescription.Server.props", + "build/Microsoft.Extensions.ApiDescription.Server.targets", + "buildMultiTargeting/Microsoft.Extensions.ApiDescription.Server.props", + "buildMultiTargeting/Microsoft.Extensions.ApiDescription.Server.targets", + "microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512", + "microsoft.extensions.apidescription.server.nuspec", + "tools/Newtonsoft.Json.dll", + "tools/dotnet-getdocument.deps.json", + "tools/dotnet-getdocument.dll", + "tools/dotnet-getdocument.runtimeconfig.json", + "tools/net461-x86/GetDocument.Insider.exe", + "tools/net461-x86/GetDocument.Insider.exe.config", + "tools/net461-x86/Microsoft.Win32.Primitives.dll", + "tools/net461-x86/System.AppContext.dll", + "tools/net461-x86/System.Buffers.dll", + "tools/net461-x86/System.Collections.Concurrent.dll", + "tools/net461-x86/System.Collections.NonGeneric.dll", + "tools/net461-x86/System.Collections.Specialized.dll", + "tools/net461-x86/System.Collections.dll", + "tools/net461-x86/System.ComponentModel.EventBasedAsync.dll", + "tools/net461-x86/System.ComponentModel.Primitives.dll", + "tools/net461-x86/System.ComponentModel.TypeConverter.dll", + "tools/net461-x86/System.ComponentModel.dll", + "tools/net461-x86/System.Console.dll", + "tools/net461-x86/System.Data.Common.dll", + "tools/net461-x86/System.Diagnostics.Contracts.dll", + "tools/net461-x86/System.Diagnostics.Debug.dll", + "tools/net461-x86/System.Diagnostics.DiagnosticSource.dll", + "tools/net461-x86/System.Diagnostics.FileVersionInfo.dll", + "tools/net461-x86/System.Diagnostics.Process.dll", + "tools/net461-x86/System.Diagnostics.StackTrace.dll", + "tools/net461-x86/System.Diagnostics.TextWriterTraceListener.dll", + "tools/net461-x86/System.Diagnostics.Tools.dll", + "tools/net461-x86/System.Diagnostics.TraceSource.dll", + "tools/net461-x86/System.Diagnostics.Tracing.dll", + "tools/net461-x86/System.Drawing.Primitives.dll", + "tools/net461-x86/System.Dynamic.Runtime.dll", + "tools/net461-x86/System.Globalization.Calendars.dll", + "tools/net461-x86/System.Globalization.Extensions.dll", + "tools/net461-x86/System.Globalization.dll", + "tools/net461-x86/System.IO.Compression.ZipFile.dll", + "tools/net461-x86/System.IO.Compression.dll", + "tools/net461-x86/System.IO.FileSystem.DriveInfo.dll", + "tools/net461-x86/System.IO.FileSystem.Primitives.dll", + "tools/net461-x86/System.IO.FileSystem.Watcher.dll", + "tools/net461-x86/System.IO.FileSystem.dll", + "tools/net461-x86/System.IO.IsolatedStorage.dll", + "tools/net461-x86/System.IO.MemoryMappedFiles.dll", + "tools/net461-x86/System.IO.Pipes.dll", + "tools/net461-x86/System.IO.UnmanagedMemoryStream.dll", + "tools/net461-x86/System.IO.dll", + "tools/net461-x86/System.Linq.Expressions.dll", + "tools/net461-x86/System.Linq.Parallel.dll", + "tools/net461-x86/System.Linq.Queryable.dll", + "tools/net461-x86/System.Linq.dll", + "tools/net461-x86/System.Memory.dll", + "tools/net461-x86/System.Net.Http.dll", + "tools/net461-x86/System.Net.NameResolution.dll", + "tools/net461-x86/System.Net.NetworkInformation.dll", + "tools/net461-x86/System.Net.Ping.dll", + "tools/net461-x86/System.Net.Primitives.dll", + "tools/net461-x86/System.Net.Requests.dll", + "tools/net461-x86/System.Net.Security.dll", + "tools/net461-x86/System.Net.Sockets.dll", + "tools/net461-x86/System.Net.WebHeaderCollection.dll", + "tools/net461-x86/System.Net.WebSockets.Client.dll", + "tools/net461-x86/System.Net.WebSockets.dll", + "tools/net461-x86/System.Numerics.Vectors.dll", + "tools/net461-x86/System.ObjectModel.dll", + "tools/net461-x86/System.Reflection.Extensions.dll", + "tools/net461-x86/System.Reflection.Primitives.dll", + "tools/net461-x86/System.Reflection.dll", + "tools/net461-x86/System.Resources.Reader.dll", + "tools/net461-x86/System.Resources.ResourceManager.dll", + "tools/net461-x86/System.Resources.Writer.dll", + "tools/net461-x86/System.Runtime.CompilerServices.Unsafe.dll", + "tools/net461-x86/System.Runtime.CompilerServices.VisualC.dll", + "tools/net461-x86/System.Runtime.Extensions.dll", + "tools/net461-x86/System.Runtime.Handles.dll", + "tools/net461-x86/System.Runtime.InteropServices.RuntimeInformation.dll", + "tools/net461-x86/System.Runtime.InteropServices.dll", + "tools/net461-x86/System.Runtime.Numerics.dll", + "tools/net461-x86/System.Runtime.Serialization.Formatters.dll", + "tools/net461-x86/System.Runtime.Serialization.Json.dll", + "tools/net461-x86/System.Runtime.Serialization.Primitives.dll", + "tools/net461-x86/System.Runtime.Serialization.Xml.dll", + "tools/net461-x86/System.Runtime.dll", + "tools/net461-x86/System.Security.Claims.dll", + "tools/net461-x86/System.Security.Cryptography.Algorithms.dll", + "tools/net461-x86/System.Security.Cryptography.Csp.dll", + "tools/net461-x86/System.Security.Cryptography.Encoding.dll", + "tools/net461-x86/System.Security.Cryptography.Primitives.dll", + "tools/net461-x86/System.Security.Cryptography.X509Certificates.dll", + "tools/net461-x86/System.Security.Principal.dll", + "tools/net461-x86/System.Security.SecureString.dll", + "tools/net461-x86/System.Text.Encoding.Extensions.dll", + "tools/net461-x86/System.Text.Encoding.dll", + "tools/net461-x86/System.Text.RegularExpressions.dll", + "tools/net461-x86/System.Threading.Overlapped.dll", + "tools/net461-x86/System.Threading.Tasks.Parallel.dll", + "tools/net461-x86/System.Threading.Tasks.dll", + "tools/net461-x86/System.Threading.Thread.dll", + "tools/net461-x86/System.Threading.ThreadPool.dll", + "tools/net461-x86/System.Threading.Timer.dll", + "tools/net461-x86/System.Threading.dll", + "tools/net461-x86/System.ValueTuple.dll", + "tools/net461-x86/System.Xml.ReaderWriter.dll", + "tools/net461-x86/System.Xml.XDocument.dll", + "tools/net461-x86/System.Xml.XPath.XDocument.dll", + "tools/net461-x86/System.Xml.XPath.dll", + "tools/net461-x86/System.Xml.XmlDocument.dll", + "tools/net461-x86/System.Xml.XmlSerializer.dll", + "tools/net461-x86/netstandard.dll", + "tools/net461/GetDocument.Insider.exe", + "tools/net461/GetDocument.Insider.exe.config", + "tools/net461/Microsoft.Win32.Primitives.dll", + "tools/net461/System.AppContext.dll", + "tools/net461/System.Buffers.dll", + "tools/net461/System.Collections.Concurrent.dll", + "tools/net461/System.Collections.NonGeneric.dll", + "tools/net461/System.Collections.Specialized.dll", + "tools/net461/System.Collections.dll", + "tools/net461/System.ComponentModel.EventBasedAsync.dll", + "tools/net461/System.ComponentModel.Primitives.dll", + "tools/net461/System.ComponentModel.TypeConverter.dll", + "tools/net461/System.ComponentModel.dll", + "tools/net461/System.Console.dll", + "tools/net461/System.Data.Common.dll", + "tools/net461/System.Diagnostics.Contracts.dll", + "tools/net461/System.Diagnostics.Debug.dll", + "tools/net461/System.Diagnostics.DiagnosticSource.dll", + "tools/net461/System.Diagnostics.FileVersionInfo.dll", + "tools/net461/System.Diagnostics.Process.dll", + "tools/net461/System.Diagnostics.StackTrace.dll", + "tools/net461/System.Diagnostics.TextWriterTraceListener.dll", + "tools/net461/System.Diagnostics.Tools.dll", + "tools/net461/System.Diagnostics.TraceSource.dll", + "tools/net461/System.Diagnostics.Tracing.dll", + "tools/net461/System.Drawing.Primitives.dll", + "tools/net461/System.Dynamic.Runtime.dll", + "tools/net461/System.Globalization.Calendars.dll", + "tools/net461/System.Globalization.Extensions.dll", + "tools/net461/System.Globalization.dll", + "tools/net461/System.IO.Compression.ZipFile.dll", + "tools/net461/System.IO.Compression.dll", + "tools/net461/System.IO.FileSystem.DriveInfo.dll", + "tools/net461/System.IO.FileSystem.Primitives.dll", + "tools/net461/System.IO.FileSystem.Watcher.dll", + "tools/net461/System.IO.FileSystem.dll", + "tools/net461/System.IO.IsolatedStorage.dll", + "tools/net461/System.IO.MemoryMappedFiles.dll", + "tools/net461/System.IO.Pipes.dll", + "tools/net461/System.IO.UnmanagedMemoryStream.dll", + "tools/net461/System.IO.dll", + "tools/net461/System.Linq.Expressions.dll", + "tools/net461/System.Linq.Parallel.dll", + "tools/net461/System.Linq.Queryable.dll", + "tools/net461/System.Linq.dll", + "tools/net461/System.Memory.dll", + "tools/net461/System.Net.Http.dll", + "tools/net461/System.Net.NameResolution.dll", + "tools/net461/System.Net.NetworkInformation.dll", + "tools/net461/System.Net.Ping.dll", + "tools/net461/System.Net.Primitives.dll", + "tools/net461/System.Net.Requests.dll", + "tools/net461/System.Net.Security.dll", + "tools/net461/System.Net.Sockets.dll", + "tools/net461/System.Net.WebHeaderCollection.dll", + "tools/net461/System.Net.WebSockets.Client.dll", + "tools/net461/System.Net.WebSockets.dll", + "tools/net461/System.Numerics.Vectors.dll", + "tools/net461/System.ObjectModel.dll", + "tools/net461/System.Reflection.Extensions.dll", + "tools/net461/System.Reflection.Primitives.dll", + "tools/net461/System.Reflection.dll", + "tools/net461/System.Resources.Reader.dll", + "tools/net461/System.Resources.ResourceManager.dll", + "tools/net461/System.Resources.Writer.dll", + "tools/net461/System.Runtime.CompilerServices.Unsafe.dll", + "tools/net461/System.Runtime.CompilerServices.VisualC.dll", + "tools/net461/System.Runtime.Extensions.dll", + "tools/net461/System.Runtime.Handles.dll", + "tools/net461/System.Runtime.InteropServices.RuntimeInformation.dll", + "tools/net461/System.Runtime.InteropServices.dll", + "tools/net461/System.Runtime.Numerics.dll", + "tools/net461/System.Runtime.Serialization.Formatters.dll", + "tools/net461/System.Runtime.Serialization.Json.dll", + "tools/net461/System.Runtime.Serialization.Primitives.dll", + "tools/net461/System.Runtime.Serialization.Xml.dll", + "tools/net461/System.Runtime.dll", + "tools/net461/System.Security.Claims.dll", + "tools/net461/System.Security.Cryptography.Algorithms.dll", + "tools/net461/System.Security.Cryptography.Csp.dll", + "tools/net461/System.Security.Cryptography.Encoding.dll", + "tools/net461/System.Security.Cryptography.Primitives.dll", + "tools/net461/System.Security.Cryptography.X509Certificates.dll", + "tools/net461/System.Security.Principal.dll", + "tools/net461/System.Security.SecureString.dll", + "tools/net461/System.Text.Encoding.Extensions.dll", + "tools/net461/System.Text.Encoding.dll", + "tools/net461/System.Text.RegularExpressions.dll", + "tools/net461/System.Threading.Overlapped.dll", + "tools/net461/System.Threading.Tasks.Parallel.dll", + "tools/net461/System.Threading.Tasks.dll", + "tools/net461/System.Threading.Thread.dll", + "tools/net461/System.Threading.ThreadPool.dll", + "tools/net461/System.Threading.Timer.dll", + "tools/net461/System.Threading.dll", + "tools/net461/System.ValueTuple.dll", + "tools/net461/System.Xml.ReaderWriter.dll", + "tools/net461/System.Xml.XDocument.dll", + "tools/net461/System.Xml.XPath.XDocument.dll", + "tools/net461/System.Xml.XPath.dll", + "tools/net461/System.Xml.XmlDocument.dll", + "tools/net461/System.Xml.XmlSerializer.dll", + "tools/net461/netstandard.dll", + "tools/netcoreapp2.1/GetDocument.Insider.deps.json", + "tools/netcoreapp2.1/GetDocument.Insider.dll", + "tools/netcoreapp2.1/GetDocument.Insider.runtimeconfig.json", + "tools/netcoreapp2.1/System.Diagnostics.DiagnosticSource.dll" + ] + }, + "Microsoft.OpenApi/1.6.14": { + "sha512": "tTaBT8qjk3xINfESyOPE2rIellPvB7qpVqiWiyA/lACVvz+xOGiXhFUfohcx82NLbi5avzLW0lx+s6oAqQijfw==", + "type": "package", + "path": "microsoft.openapi/1.6.14", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "README.md", + "lib/netstandard2.0/Microsoft.OpenApi.dll", + "lib/netstandard2.0/Microsoft.OpenApi.pdb", + "lib/netstandard2.0/Microsoft.OpenApi.xml", + "microsoft.openapi.1.6.14.nupkg.sha512", + "microsoft.openapi.nuspec" + ] + }, + "Swashbuckle.AspNetCore/6.9.0": { + "sha512": "lvI+XHF21tkwXd2nDCLGJsdhdUYsY3Ax2fWUlvw81Oa6EedtnIAf5tThy8ZnPcz/9/TwsLgjgtX9ifOCIjbEPA==", + "type": "package", + "path": "swashbuckle.aspnetcore/6.9.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "build/Swashbuckle.AspNetCore.props", + "buildMultiTargeting/Swashbuckle.AspNetCore.props", + "swashbuckle.aspnetcore.6.9.0.nupkg.sha512", + "swashbuckle.aspnetcore.nuspec" + ] + }, + "Swashbuckle.AspNetCore.Swagger/6.9.0": { + "sha512": "P316kpxx5DnDvJwNWW8iTAXkh9DVenAxFGe9v4OUS0gil+vitH7F1feXhCtVeHN/616EFNTMh4pV2lcr9kkw/w==", + "type": "package", + "path": "swashbuckle.aspnetcore.swagger/6.9.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/net5.0/Swashbuckle.AspNetCore.Swagger.dll", + "lib/net5.0/Swashbuckle.AspNetCore.Swagger.pdb", + "lib/net5.0/Swashbuckle.AspNetCore.Swagger.xml", + "lib/net6.0/Swashbuckle.AspNetCore.Swagger.dll", + "lib/net6.0/Swashbuckle.AspNetCore.Swagger.pdb", + "lib/net6.0/Swashbuckle.AspNetCore.Swagger.xml", + "lib/net7.0/Swashbuckle.AspNetCore.Swagger.dll", + "lib/net7.0/Swashbuckle.AspNetCore.Swagger.pdb", + "lib/net7.0/Swashbuckle.AspNetCore.Swagger.xml", + "lib/net8.0/Swashbuckle.AspNetCore.Swagger.dll", + "lib/net8.0/Swashbuckle.AspNetCore.Swagger.pdb", + "lib/net8.0/Swashbuckle.AspNetCore.Swagger.xml", + "lib/netcoreapp3.0/Swashbuckle.AspNetCore.Swagger.dll", + "lib/netcoreapp3.0/Swashbuckle.AspNetCore.Swagger.pdb", + "lib/netcoreapp3.0/Swashbuckle.AspNetCore.Swagger.xml", + "lib/netstandard2.0/Swashbuckle.AspNetCore.Swagger.dll", + "lib/netstandard2.0/Swashbuckle.AspNetCore.Swagger.pdb", + "lib/netstandard2.0/Swashbuckle.AspNetCore.Swagger.xml", + "package-readme.md", + "swashbuckle.aspnetcore.swagger.6.9.0.nupkg.sha512", + "swashbuckle.aspnetcore.swagger.nuspec" + ] + }, + "Swashbuckle.AspNetCore.SwaggerGen/6.9.0": { + "sha512": "FjeMR3fBzwVc5plfYjoHw9ptf8SOWMupvO9X35J5EgzT3L9dRqSxa+cBKzL8PwCyemY0xNrggQSB5+MFWx1axg==", + "type": "package", + "path": "swashbuckle.aspnetcore.swaggergen/6.9.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/net5.0/Swashbuckle.AspNetCore.SwaggerGen.dll", + "lib/net5.0/Swashbuckle.AspNetCore.SwaggerGen.pdb", + "lib/net5.0/Swashbuckle.AspNetCore.SwaggerGen.xml", + "lib/net6.0/Swashbuckle.AspNetCore.SwaggerGen.dll", + "lib/net6.0/Swashbuckle.AspNetCore.SwaggerGen.pdb", + "lib/net6.0/Swashbuckle.AspNetCore.SwaggerGen.xml", + "lib/net7.0/Swashbuckle.AspNetCore.SwaggerGen.dll", + "lib/net7.0/Swashbuckle.AspNetCore.SwaggerGen.pdb", + "lib/net7.0/Swashbuckle.AspNetCore.SwaggerGen.xml", + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.dll", + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.pdb", + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerGen.xml", + "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerGen.dll", + "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerGen.pdb", + "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerGen.xml", + "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerGen.dll", + "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerGen.pdb", + "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerGen.xml", + "package-readme.md", + "swashbuckle.aspnetcore.swaggergen.6.9.0.nupkg.sha512", + "swashbuckle.aspnetcore.swaggergen.nuspec" + ] + }, + "Swashbuckle.AspNetCore.SwaggerUI/6.9.0": { + "sha512": "0OxlWBFLl2gUESZX/K7QCTz9KctKy0VxHTvLIBcyWGD4z/fv5MCMW02qzYGcReLJr4yBnNDRzApKtLh6oBpe9A==", + "type": "package", + "path": "swashbuckle.aspnetcore.swaggerui/6.9.0", + "files": [ + ".nupkg.metadata", + ".signature.p7s", + "lib/net5.0/Swashbuckle.AspNetCore.SwaggerUI.dll", + "lib/net5.0/Swashbuckle.AspNetCore.SwaggerUI.pdb", + "lib/net5.0/Swashbuckle.AspNetCore.SwaggerUI.xml", + "lib/net6.0/Swashbuckle.AspNetCore.SwaggerUI.dll", + "lib/net6.0/Swashbuckle.AspNetCore.SwaggerUI.pdb", + "lib/net6.0/Swashbuckle.AspNetCore.SwaggerUI.xml", + "lib/net7.0/Swashbuckle.AspNetCore.SwaggerUI.dll", + "lib/net7.0/Swashbuckle.AspNetCore.SwaggerUI.pdb", + "lib/net7.0/Swashbuckle.AspNetCore.SwaggerUI.xml", + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.dll", + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.pdb", + "lib/net8.0/Swashbuckle.AspNetCore.SwaggerUI.xml", + "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerUI.dll", + "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerUI.pdb", + "lib/netcoreapp3.0/Swashbuckle.AspNetCore.SwaggerUI.xml", + "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerUI.dll", + "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerUI.pdb", + "lib/netstandard2.0/Swashbuckle.AspNetCore.SwaggerUI.xml", + "package-readme.md", + "swashbuckle.aspnetcore.swaggerui.6.9.0.nupkg.sha512", + "swashbuckle.aspnetcore.swaggerui.nuspec" + ] + } + }, + "projectFileDependencyGroups": { + "net8.0": [ + "Swashbuckle.AspNetCore >= 6.9.0" + ] + }, + "packageFolders": { + "C:\\Users\\GamingPC\\.nuget\\packages\\": {}, + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages": {} + }, + "project": { + "version": "1.0.0", + "restore": { + "projectUniqueName": "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj", + "projectName": "Csob", + "projectPath": "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj", + "packagesPath": "C:\\Users\\GamingPC\\.nuget\\packages\\", + "outputPath": "d:\\GitHubRepository\\Hracicky\\x\\csob\\obj\\", + "projectStyle": "PackageReference", + "fallbackFolders": [ + "C:\\Program Files (x86)\\Microsoft Visual Studio\\Shared\\NuGetPackages" + ], + "configFilePaths": [ + "C:\\Users\\GamingPC\\AppData\\Roaming\\NuGet\\NuGet.Config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.FallbackLocation.config", + "C:\\Program Files (x86)\\NuGet\\Config\\Microsoft.VisualStudio.Offline.config" + ], + "originalTargetFrameworks": [ + "net8.0" + ], + "sources": { + "C:\\Program Files (x86)\\Microsoft SDKs\\NuGetPackages\\": {}, + "C:\\Program Files\\dotnet\\library-packs": {}, + "D:\\CustomNuGetPackages": {}, + "https://api.nuget.org/v3/index.json": {} + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "projectReferences": {} + } + }, + "warningProperties": { + "warnAsError": [ + "NU1605" + ] + }, + "restoreAuditProperties": { + "enableAudit": "true", + "auditLevel": "low", + "auditMode": "direct" + }, + "SdkAnalysisLevel": "10.0.200" + }, + "frameworks": { + "net8.0": { + "targetAlias": "net8.0", + "dependencies": { + "Swashbuckle.AspNetCore": { + "target": "Package", + "version": "[6.9.0, )" + } + }, + "imports": [ + "net461", + "net462", + "net47", + "net471", + "net472", + "net48", + "net481" + ], + "assetTargetFallback": true, + "warn": true, + "frameworkReferences": { + "Microsoft.AspNetCore.App": { + "privateAssets": "none" + }, + "Microsoft.NETCore.App": { + "privateAssets": "all" + } + }, + "runtimeIdentifierGraphPath": "C:\\Program Files\\dotnet\\sdk\\10.0.204/PortableRuntimeIdentifierGraph.json" + } + } + } +} \ No newline at end of file diff --git a/obj/project.nuget.cache b/obj/project.nuget.cache new file mode 100644 index 0000000..57542e8 --- /dev/null +++ b/obj/project.nuget.cache @@ -0,0 +1,15 @@ +{ + "version": 2, + "dgSpecHash": "oxCvoNmhuPk=", + "success": true, + "projectFilePath": "d:\\GitHubRepository\\Hracicky\\x\\csob\\Csob.csproj", + "expectedPackageFiles": [ + "C:\\Users\\GamingPC\\.nuget\\packages\\microsoft.extensions.apidescription.server\\6.0.5\\microsoft.extensions.apidescription.server.6.0.5.nupkg.sha512", + "C:\\Users\\GamingPC\\.nuget\\packages\\microsoft.openapi\\1.6.14\\microsoft.openapi.1.6.14.nupkg.sha512", + "C:\\Users\\GamingPC\\.nuget\\packages\\swashbuckle.aspnetcore\\6.9.0\\swashbuckle.aspnetcore.6.9.0.nupkg.sha512", + "C:\\Users\\GamingPC\\.nuget\\packages\\swashbuckle.aspnetcore.swagger\\6.9.0\\swashbuckle.aspnetcore.swagger.6.9.0.nupkg.sha512", + "C:\\Users\\GamingPC\\.nuget\\packages\\swashbuckle.aspnetcore.swaggergen\\6.9.0\\swashbuckle.aspnetcore.swaggergen.6.9.0.nupkg.sha512", + "C:\\Users\\GamingPC\\.nuget\\packages\\swashbuckle.aspnetcore.swaggerui\\6.9.0\\swashbuckle.aspnetcore.swaggerui.6.9.0.nupkg.sha512" + ], + "logs": [] +} \ No newline at end of file