diff --git a/src/presentation/DigitalData.TaskFlow.API/Contracts/Common/MetaOnlyResponse.cs b/src/presentation/DigitalData.TaskFlow.API/Contracts/Common/MetaOnlyResponse.cs deleted file mode 100644 index d7148bf..0000000 --- a/src/presentation/DigitalData.TaskFlow.API/Contracts/Common/MetaOnlyResponse.cs +++ /dev/null @@ -1,16 +0,0 @@ -using DigitalData.TaskFlow.Domain.Common; -using System.Text.Json.Serialization; - -namespace DigitalData.TaskFlow.API.Contracts.Common; - -/// -/// Represents a contract response that contains only metadata. -/// -public sealed record MetaOnlyResponse -{ - /// - /// Gets metadata entries. - /// - [JsonPropertyName("meta")] - public required IReadOnlyList Meta { get; init; } -} diff --git a/src/presentation/DigitalData.TaskFlow.API/Contracts/Workflows/CloseWorkflowRequest.cs b/src/presentation/DigitalData.TaskFlow.API/Contracts/Workflows/CloseWorkflowRequest.cs deleted file mode 100644 index ad8a6ea..0000000 --- a/src/presentation/DigitalData.TaskFlow.API/Contracts/Workflows/CloseWorkflowRequest.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System.Text.Json.Serialization; - -namespace DigitalData.TaskFlow.API.Contracts.Workflows; - -/// -/// Represents the request payload for closing a workflow. -/// -public sealed class CloseWorkflowRequest -{ - /// - /// Gets or sets the selected closing method identifier. - /// - [JsonPropertyName("closing_method_id")] - public required int ClosingMethodId { get; init; } - - /// - /// Gets or sets an optional user comment. - /// - [JsonPropertyName("comment")] - public string? Comment { get; init; } - - /// - /// Gets or sets an optional rejection reason identifier. - /// - [JsonPropertyName("rejection_reason_id")] - public int? RejectionReasonId { get; init; } -} diff --git a/src/presentation/DigitalData.TaskFlow.API/Contracts/Workflows/WorkflowsResponse.cs b/src/presentation/DigitalData.TaskFlow.API/Contracts/Workflows/WorkflowsResponse.cs deleted file mode 100644 index 1e858af..0000000 --- a/src/presentation/DigitalData.TaskFlow.API/Contracts/Workflows/WorkflowsResponse.cs +++ /dev/null @@ -1,22 +0,0 @@ -using DigitalData.TaskFlow.Domain.Common; -using System.Text.Json.Serialization; - -namespace DigitalData.TaskFlow.API.Contracts.Workflows; - -/// -/// Represents the GET workflows response contract. -/// -public sealed record WorkflowsResponse -{ - /// - /// Gets metadata entries. - /// - [JsonPropertyName("meta")] - public required IReadOnlyList Meta { get; init; } - - /// - /// Gets grouped source blocks. - /// - [JsonPropertyName("sources")] - public required IReadOnlyList Sources { get; init; } -} diff --git a/src/presentation/DigitalData.TaskFlow.API/Controllers/DocumentController.cs b/src/presentation/DigitalData.TaskFlow.API/Controllers/DocumentController.cs new file mode 100644 index 0000000..9398cbd --- /dev/null +++ b/src/presentation/DigitalData.TaskFlow.API/Controllers/DocumentController.cs @@ -0,0 +1,33 @@ +using DigitalData.TaskFlow.Application.Features.Documents.Queries; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using System.Text.Json; + +namespace DigitalData.TaskFlow.API.Controllers; + +/// +/// Provides workflow read and close endpoints. +/// +[ApiController] +[Route("api/documents")] +public sealed class DocumentController(ISender sender) : ControllerBase +{ + /// + /// Returns open documents for a WISAG user identifier. + /// + /// Document identifier. + /// User identifier. + /// Process identifier. + /// Return document as byte array. + /// Cancellation token. + /// Document response envelope grouped by source. + [HttpGet("{id:int}")] + [ProducesResponseType(typeof(JsonElement), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status502BadGateway)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetAsync([FromRoute] int id, [FromQuery] int userId, [FromQuery] long processId, CancellationToken cancellationToken = default) + { + return Ok(await sender.Send(new GetDocumentQuery(Id: id, UserId: userId, ProcessId: processId), cancellationToken)); + } +} diff --git a/src/presentation/DigitalData.TaskFlow.API/Controllers/DocumentsController.cs b/src/presentation/DigitalData.TaskFlow.API/Controllers/DocumentsController.cs deleted file mode 100644 index bea5313..0000000 --- a/src/presentation/DigitalData.TaskFlow.API/Controllers/DocumentsController.cs +++ /dev/null @@ -1,36 +0,0 @@ -using DigitalData.TaskFlow.Application.Features.Documents.Queries.GetDocument; -using DigitalData.TaskFlow.API.Contracts.Common; -using DigitalData.TaskFlow.Domain.Common; -using DigitalData.TaskFlow.Domain.Documents; -using MediatR; -using Microsoft.AspNetCore.Mvc; - -namespace DigitalData.TaskFlow.API.Controllers; - -/// -/// Provides document binary download endpoints. -/// -[ApiController] -[Route("api/documents")] -public sealed class DocumentsController(ISender sender) : ControllerBase -{ - /// - /// Returns binary document content by document identifier. - /// - /// Document identifier. - /// Cancellation token. - /// Binary stream when available. - [HttpGet("{id}")] - [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status401Unauthorized)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status403Forbidden)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status500InternalServerError)] - public async Task GetAsync([FromRoute] string id, CancellationToken cancellationToken) - { - var response = await sender.Send(new GetDocumentQuery(id), cancellationToken); - var data = response.Data ?? throw new InvalidOperationException("Document payload is unexpectedly null."); - return File(data.Content, data.ContentType, data.FileName); - } -} diff --git a/src/presentation/DigitalData.TaskFlow.API/Controllers/WorkflowsController.cs b/src/presentation/DigitalData.TaskFlow.API/Controllers/WorkflowsController.cs index bce3834..9af3a54 100644 --- a/src/presentation/DigitalData.TaskFlow.API/Controllers/WorkflowsController.cs +++ b/src/presentation/DigitalData.TaskFlow.API/Controllers/WorkflowsController.cs @@ -1,9 +1,7 @@ -using DigitalData.TaskFlow.API.Contracts.Workflows; -using DigitalData.TaskFlow.API.Contracts.Common; -using DigitalData.TaskFlow.Application.Features.Workflows.Commands.CloseWorkflow; -using DigitalData.TaskFlow.Application.Features.Workflows.Queries.GetWorkflows; +using DigitalData.TaskFlow.Application.Features.Workflows.Queries; using MediatR; using Microsoft.AspNetCore.Mvc; +using System.Text.Json; namespace DigitalData.TaskFlow.API.Controllers; @@ -21,64 +19,12 @@ public sealed class WorkflowsController(ISender sender) : ControllerBase /// Cancellation token. /// Workflow response envelope grouped by source. [HttpGet("{wisagId:int}")] - [ProducesResponseType(typeof(WorkflowsResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status401Unauthorized)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status500InternalServerError)] - public async Task> GetAsync( - [FromRoute] int wisagId, - CancellationToken cancellationToken) + [ProducesResponseType(typeof(JsonElement), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status401Unauthorized)] + [ProducesResponseType(StatusCodes.Status502BadGateway)] + [ProducesResponseType(StatusCodes.Status500InternalServerError)] + public async Task> GetAsync([FromRoute] int wisagId, CancellationToken cancellationToken) { - var response = await sender.Send(new GetWorkflowsQuery(wisagId), cancellationToken); - - var sources = response.Data?.Sources - .Cast() - .ToList() - ?? []; - - return Ok(new WorkflowsResponse - { - Meta = response.Meta, - Sources = sources - }); - } - - /// - /// Applies a closing decision to a workflow process. - /// - /// WISAG user identifier. - /// Source system name. - /// Process identifier. - /// Closing request payload. - /// Cancellation token. - /// Close result envelope. - [HttpPost("{wisagId:int}/{source}/{processId:int}/closing")] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status401Unauthorized)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status403Forbidden)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status404NotFound)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status409Conflict)] - [ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status500InternalServerError)] - public async Task> CloseAsync( - [FromRoute] int wisagId, - [FromRoute] string source, - [FromRoute] int processId, - [FromBody] CloseWorkflowRequest request, - CancellationToken cancellationToken) - { - var command = new CloseWorkflowCommand( - wisagId, - source, - processId, - request.ClosingMethodId, - request.Comment, - request.RejectionReasonId); - - var response = await sender.Send(command, cancellationToken); - return Ok(new MetaOnlyResponse - { - Meta = response.Meta - }); + return Ok(await sender.Send(new GetWorkflowsQuery(wisagId), cancellationToken)); } } diff --git a/src/presentation/DigitalData.TaskFlow.API/DigitalData.TaskFlow.API.csproj b/src/presentation/DigitalData.TaskFlow.API/DigitalData.TaskFlow.API.csproj index d0caab0..eab84ed 100644 --- a/src/presentation/DigitalData.TaskFlow.API/DigitalData.TaskFlow.API.csproj +++ b/src/presentation/DigitalData.TaskFlow.API/DigitalData.TaskFlow.API.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -31,16 +31,12 @@ - + - - - - - - + + diff --git a/src/presentation/DigitalData.TaskFlow.API/Middleware/ApiKeyAuthMiddleware.cs b/src/presentation/DigitalData.TaskFlow.API/Middleware/ApiKeyAuthMiddleware.cs index 5384d2f..655fc4f 100644 --- a/src/presentation/DigitalData.TaskFlow.API/Middleware/ApiKeyAuthMiddleware.cs +++ b/src/presentation/DigitalData.TaskFlow.API/Middleware/ApiKeyAuthMiddleware.cs @@ -1,54 +1,55 @@ -using DigitalData.TaskFlow.API.Contracts.Common; using DigitalData.TaskFlow.API.Options; -using DigitalData.TaskFlow.Domain.Common; -using DigitalData.TaskFlow.Domain.Enums; using Microsoft.Extensions.Options; using System.Text.Json; namespace DigitalData.TaskFlow.API.Middleware; /// -/// Validates required API key header for API endpoints. +/// Enforces API key authentication for all requests under the /api path prefix. +/// Expects the key to be supplied via the X-Api-Key request header. /// -public sealed class ApiKeyAuthMiddleware(RequestDelegate next, IOptions apiKeyOptions) +public sealed class ApiKeyAuthMiddleware(RequestDelegate next, IOptions options) { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); - private readonly string? _configuredApiKey = apiKeyOptions.Value.Key; - /// - /// Executes middleware logic. - /// - /// HTTP context. + // Snapshot at startup; options are not expected to change at runtime. + private readonly string? _configuredKey = options.Value.Key; + + /// public async Task InvokeAsync(HttpContext context) { + // Only guard /api/** routes; let non-API paths pass through (e.g. health, swagger). if (!context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase)) { await next(context); return; } - if (context.Request.Headers.TryGetValue("X-Api-Key", out var values) - && string.Equals(values.FirstOrDefault(), _configuredApiKey, StringComparison.Ordinal)) + // Treat a missing or empty configured key as a misconfiguration — deny all traffic. + if (!string.IsNullOrWhiteSpace(_configuredKey) + && context.Request.Headers.TryGetValue("X-Api-Key", out var provided) + && string.Equals(provided, _configuredKey, StringComparison.Ordinal)) { await next(context); return; } - context.Response.StatusCode = StatusCodes.Status401Unauthorized; - context.Response.ContentType = "application/json"; + await WriteUnauthorizedAsync(context); + } - var response = new MetaOnlyResponse + private static Task WriteUnauthorizedAsync(HttpContext context) + { + context.Response.StatusCode = StatusCodes.Status401Unauthorized; + context.Response.ContentType = "application/problem+json"; + + var body = new { - Meta = - [ - new MetaEntry - { - Code = (int)MetaCode.InvalidRequest, - Message = "Authentifizierung fehlgeschlagen" - } - ] + type = "https://tools.ietf.org/html/rfc7807", + title = "Unauthorized", + status = StatusCodes.Status401Unauthorized, + detail = "A valid API key must be supplied via the X-Api-Key header." }; - await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions)); + return context.Response.WriteAsync(JsonSerializer.Serialize(body, JsonOptions)); } } diff --git a/src/presentation/DigitalData.TaskFlow.API/Middleware/ExceptionHandlingMiddleware.cs b/src/presentation/DigitalData.TaskFlow.API/Middleware/ExceptionHandlingMiddleware.cs index 2ce2c00..41bea14 100644 --- a/src/presentation/DigitalData.TaskFlow.API/Middleware/ExceptionHandlingMiddleware.cs +++ b/src/presentation/DigitalData.TaskFlow.API/Middleware/ExceptionHandlingMiddleware.cs @@ -1,110 +1,82 @@ using DigitalData.TaskFlow.Application.Common.Exceptions; -using DigitalData.TaskFlow.API.Contracts.Common; -using DigitalData.TaskFlow.Domain.Common; -using DigitalData.TaskFlow.Domain.Enums; using FluentValidation; using System.Text.Json; namespace DigitalData.TaskFlow.API.Middleware; /// -/// Handles exceptions globally and returns ticket-compatible API envelopes. +/// Handles exceptions globally and returns RFC 7807 (Problem Details) compatible API envelopes. /// public sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); /// - /// Executes the middleware. + /// /// - /// HTTP context. + /// + /// public async Task InvokeAsync(HttpContext context) { try { await next(context); } - catch (ApiException apiException) + catch (ValidationException ex) { - logger.LogWarning(apiException, "API exception intercepted."); + logger.LogWarning(ex, "Validation failed for {Method} {Path}", + context.Request.Method, context.Request.Path); - context.Response.StatusCode = apiException.StatusCode; - context.Response.ContentType = "application/json"; + var errors = ex.Errors + .GroupBy(e => e.PropertyName) + .ToDictionary( + g => g.Key, + g => g.Select(e => e.ErrorMessage).ToArray() + ); - var response = new MetaOnlyResponse + var response = new { - Meta = - [ - new MetaEntry - { - Code = (int)apiException.MetaCode, - Message = apiException.Message - } - ] + type = "https://tools.ietf.org/html/rfc7807", + title = "Validation Error", + status = StatusCodes.Status400BadRequest, + errors }; + + context.Response.StatusCode = StatusCodes.Status400BadRequest; + context.Response.ContentType = "application/problem+json"; await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions)); } - catch (ValidationException validationException) + catch (InvalidWorkflowPayloadException ex) { - logger.LogWarning(validationException, "Validation failed."); + logger.LogError(ex, "Invalid upstream payload for {Method} {Path}", + context.Request.Method, context.Request.Path); - context.Response.StatusCode = StatusCodes.Status400BadRequest; - context.Response.ContentType = "application/json"; - - var message = validationException.Errors.FirstOrDefault()?.ErrorMessage - ?? "Ungultiger Request"; - - var response = new MetaOnlyResponse + var response = new { - Meta = - [ - new MetaEntry - { - Code = (int)MetaCode.InvalidRequest, - Message = message - } - ] + type = "https://tools.ietf.org/html/rfc7807", + title = "Bad Gateway", + status = StatusCodes.Status502BadGateway, + detail = ex.Message }; + + context.Response.StatusCode = StatusCodes.Status502BadGateway; + context.Response.ContentType = "application/problem+json"; await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions)); } - catch (NotSupportedException notSupportedException) + catch (Exception ex) { - logger.LogWarning(notSupportedException, "Operation not supported yet."); + logger.LogError(ex, "Unhandled exception for {Method} {Path}", + context.Request.Method, context.Request.Path); - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - context.Response.ContentType = "application/json"; - - var response = new MetaOnlyResponse + var response = new { - Meta = - [ - new MetaEntry - { - Code = (int)MetaCode.InternalError, - Message = notSupportedException.Message - } - ] + type = "https://tools.ietf.org/html/rfc7807", + title = "Internal Server Error", + status = StatusCodes.Status500InternalServerError }; - await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions)); - } - catch (Exception exception) - { - logger.LogError(exception, "Unhandled exception."); - context.Response.StatusCode = StatusCodes.Status500InternalServerError; - context.Response.ContentType = "application/json"; - - var response = new MetaOnlyResponse - { - Meta = - [ - new MetaEntry - { - Code = (int)MetaCode.InternalError, - Message = "Interner Fehler" - } - ] - }; + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + context.Response.ContentType = "application/problem+json"; await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions)); } } diff --git a/src/presentation/DigitalData.TaskFlow.API/Program.cs b/src/presentation/DigitalData.TaskFlow.API/Program.cs index eda400f..caf4a4d 100644 --- a/src/presentation/DigitalData.TaskFlow.API/Program.cs +++ b/src/presentation/DigitalData.TaskFlow.API/Program.cs @@ -70,6 +70,8 @@ try builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); + var apiKeyConfigured = !string.IsNullOrWhiteSpace(config[$"{ApiKeyAuthOptions.SectionName}:Key"]); + builder.Services.AddSwaggerGen(options => { options.SwaggerDoc("v1", new OpenApiInfo @@ -77,11 +79,11 @@ try Title = "DigitalData TaskFlow API", Version = "v1", Description = """ - Integration API for taskFLOW workflow and document operations. + Integrations-API für taskFLOW Workflow- und Dokumentenoperationen. --- - - [Serilog Log Viewer](/serilog-ui) + - [Serilog Protokoll-Viewer](/serilog-ui) """, Contact = new OpenApiContact { @@ -90,6 +92,32 @@ try Email = "info-flow@digitaldata.works" } }); + + if (apiKeyConfigured) + { + options.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme + { + Type = SecuritySchemeType.ApiKey, + In = ParameterLocation.Header, + Name = "X-Api-Key", + Description = "Optionaler API-Schlüssel. Leer lassen, falls nicht erforderlich." + }); + + options.AddSecurityRequirement(new OpenApiSecurityRequirement + { + { + new OpenApiSecurityScheme + { + Reference = new OpenApiReference + { + Type = ReferenceType.SecurityScheme, + Id = "ApiKey" + } + }, + [] + } + }); + } }); builder.Services diff --git a/src/presentation/DigitalData.TaskFlow.API/Properties/launchSettings.json b/src/presentation/DigitalData.TaskFlow.API/Properties/launchSettings.json index 367171e..4db1f31 100644 --- a/src/presentation/DigitalData.TaskFlow.API/Properties/launchSettings.json +++ b/src/presentation/DigitalData.TaskFlow.API/Properties/launchSettings.json @@ -4,8 +4,8 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:3557", - "sslPort": 44339 + "applicationUrl": "http://localhost:63206", + "sslPort": 44315 } }, "profiles": { @@ -14,7 +14,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "http://localhost:5298", + "applicationUrl": "http://localhost:5062", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } @@ -24,7 +24,7 @@ "dotnetRunMessages": true, "launchBrowser": true, "launchUrl": "swagger", - "applicationUrl": "https://localhost:7050;http://localhost:5299", + "applicationUrl": "https://localhost:7131;http://localhost:5062", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } diff --git a/src/presentation/DigitalData.TaskFlow.API/README.md b/src/presentation/DigitalData.TaskFlow.API/README.md deleted file mode 100644 index 06d3395..0000000 --- a/src/presentation/DigitalData.TaskFlow.API/README.md +++ /dev/null @@ -1,44 +0,0 @@ -# DigitalData.TaskFlow.API - -Dieses Projekt stellt die HTTP-Schnittstelle fuer externe Konsumenten bereit. - -## Zweck - -Die API-Schicht nimmt HTTP-Requests entgegen, leitet diese an die Application-Schicht weiter und liefert standardisierte Responses zurueck. - -## Aktueller Inhalt - -- `Program.cs` - - Startpunkt der ASP.NET-Core-Anwendung. - - Registriert Application- und Infrastructure-Dienste. - - Enthält Grundkonfiguration fuer Controller und OpenAPI/Swagger. -- `appsettings.json` - - Allgemeine Runtime-Konfiguration (Logging, ConnectionString, MediatR-Lizenz, Swagger-Optionen). -- `Controllers/` - - Zielordner fuer HTTP-Controller (aktuell noch leer). -- `Options/` - - API-spezifische Konfigurationsmodelle (z. B. `SwaggerOptions`). - -## Geplante Inhalte (naechste Phasen) - -- Controller fuer: - - `GET /workflows/{wisagid}` - - `POST /workflows/{wisagid}/{source}/{processid}/closing` - - `GET /documents/{id}` -- Middleware fuer: - - FluentValidation-Fehlerbehandlung - - Generelle Exception-Behandlung -- OpenAPI-Dokumentation: - - Swagger in Development, optional in Production - - Firmen-/Produkt-Metadaten - - Scalar als alternative UI - -## Architektur-Regeln - -- Keine direkte SQL- oder EF-Core-Logik im Controller. -- Transport-spezifische Aufgaben bleiben in API; fachliche Verarbeitung erfolgt in Application. -- Einheitliches Response-Schema gemaess Ticket-Vorgabe (`meta.code`, `meta.message`). - -## Rolle im Gesamtsystem - -Die API ist das Eingangstor fuer externe Systeme (z. B. fabrik19) und sorgt fuer stabile, versionierbare und gut dokumentierte Endpunkte. diff --git a/src/presentation/DigitalData.TaskFlow.API/appsettings.json b/src/presentation/DigitalData.TaskFlow.API/appsettings.json index eb0f98a..04fe40e 100644 --- a/src/presentation/DigitalData.TaskFlow.API/appsettings.json +++ b/src/presentation/DigitalData.TaskFlow.API/appsettings.json @@ -16,16 +16,10 @@ "ApiKey": { "Key": null }, - "MediatR": { - "LuckyPennySoftLicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxODE2MTI4MDAwIiwiaWF0IjoiMTc4NDYyNDU1NyIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiMDE5ODNjNTllNGIzNzI4ZWJmZDMxMDNjMmE0ODZlODUiLCJzdWJfaWQiOiItIiwiZWRpdGlvbiI6IjAiLCJ0eXBlIjoiMiJ9.IUUO926m9crYGYxMjjKD_n9BnUm-EDyjFIn0YmMUCo7C-QTwvB8WhXP8veTSFsBq-leIIDJ4jyl7Pgc_7ciwg1XhUSIs4mkQroEUaSFCGOxw7Pi41WM8MK5YFSaqLTYYXec9zxgiJbGzABbh3CHTSup3okGnVm_CMoPEs91l2c0A6N1JyZy74urd_tF0KGVKf0MOvzdlQIWLQ8o73S4pTv2N-F6UlzI0fdMtTHMLNNQyr0NdWdnuBk_jMBXO-gy5RE_oCRfMTTYRX2n3XLK6pTfXE0Ct338o9F5sH8Ph2lTXSu56cpdsfZOQZGqCH0LoFp1Dd7RJgIgNmBiTGfvDnA" - }, + "LuckyPennySoftLicenseKey": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ikx1Y2t5UGVubnlTb2Z0d2FyZUxpY2Vuc2VLZXkvYmJiMTNhY2I1OTkwNGQ4OWI0Y2IxYzg1ZjA4OGNjZjkiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL2x1Y2t5cGVubnlzb2Z0d2FyZS5jb20iLCJhdWQiOiJMdWNreVBlbm55U29mdHdhcmUiLCJleHAiOiIxODE2MTI4MDAwIiwiaWF0IjoiMTc4NDYyNDU1NyIsImFjY291bnRfaWQiOiIwMTk4M2M1OWU0YjM3MjhlYmZkMzEwM2MyYTQ4NmU4NSIsImN1c3RvbWVyX2lkIjoiMDE5ODNjNTllNGIzNzI4ZWJmZDMxMDNjMmE0ODZlODUiLCJzdWJfaWQiOiItIiwiZWRpdGlvbiI6IjAiLCJ0eXBlIjoiMiJ9.IUUO926m9crYGYxMjjKD_n9BnUm-EDyjFIn0YmMUCo7C-QTwvB8WhXP8veTSFsBq-leIIDJ4jyl7Pgc_7ciwg1XhUSIs4mkQroEUaSFCGOxw7Pi41WM8MK5YFSaqLTYYXec9zxgiJbGzABbh3CHTSup3okGnVm_CMoPEs91l2c0A6N1JyZy74urd_tF0KGVKf0MOvzdlQIWLQ8o73S4pTv2N-F6UlzI0fdMtTHMLNNQyr0NdWdnuBk_jMBXO-gy5RE_oCRfMTTYRX2n3XLK6pTfXE0Ct338o9F5sH8Ph2lTXSu56cpdsfZOQZGqCH0LoFp1Dd7RJgIgNmBiTGfvDnA", "Swagger": { "EnabledInProduction": false }, - "Database": { - "CommandTimeoutSeconds": 30, - "WorkflowListingObjectType": "Ferdinand" - }, "Application": { "LogDirectory": "logs" },