refactor(api): update document endpoints and middleware contracts
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
using DigitalData.TaskFlow.Domain.Common;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DigitalData.TaskFlow.API.Contracts.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a contract response that contains only metadata.
|
||||
/// </summary>
|
||||
public sealed record MetaOnlyResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets metadata entries.
|
||||
/// </summary>
|
||||
[JsonPropertyName("meta")]
|
||||
public required IReadOnlyList<MetaEntry> Meta { get; init; }
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DigitalData.TaskFlow.API.Contracts.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the request payload for closing a workflow.
|
||||
/// </summary>
|
||||
public sealed class CloseWorkflowRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the selected closing method identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("closing_method_id")]
|
||||
public required int ClosingMethodId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional user comment.
|
||||
/// </summary>
|
||||
[JsonPropertyName("comment")]
|
||||
public string? Comment { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an optional rejection reason identifier.
|
||||
/// </summary>
|
||||
[JsonPropertyName("rejection_reason_id")]
|
||||
public int? RejectionReasonId { get; init; }
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
using DigitalData.TaskFlow.Domain.Common;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace DigitalData.TaskFlow.API.Contracts.Workflows;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the GET workflows response contract.
|
||||
/// </summary>
|
||||
public sealed record WorkflowsResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets metadata entries.
|
||||
/// </summary>
|
||||
[JsonPropertyName("meta")]
|
||||
public required IReadOnlyList<MetaEntry> Meta { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets grouped source blocks.
|
||||
/// </summary>
|
||||
[JsonPropertyName("sources")]
|
||||
public required IReadOnlyList<object> Sources { get; init; }
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Provides workflow read and close endpoints.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/documents")]
|
||||
public sealed class DocumentController(ISender sender) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns open documents for a WISAG user identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">Document identifier.</param>
|
||||
/// <param name="userId">User identifier.</param>
|
||||
/// <param name="processId">Process identifier.</param>
|
||||
/// <param name="asByte">Return document as byte array.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Document response envelope grouped by source.</returns>
|
||||
[HttpGet("{id:int}")]
|
||||
[ProducesResponseType(typeof(JsonElement), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status502BadGateway)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<JsonElement>> 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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Provides document binary download endpoints.
|
||||
/// </summary>
|
||||
[ApiController]
|
||||
[Route("api/documents")]
|
||||
public sealed class DocumentsController(ISender sender) : ControllerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns binary document content by document identifier.
|
||||
/// </summary>
|
||||
/// <param name="id">Document identifier.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Binary stream when available.</returns>
|
||||
[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<IActionResult> 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Workflow response envelope grouped by source.</returns>
|
||||
[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<ActionResult<WorkflowsResponse>> GetAsync(
|
||||
[FromRoute] int wisagId,
|
||||
CancellationToken cancellationToken)
|
||||
[ProducesResponseType(typeof(JsonElement), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status502BadGateway)]
|
||||
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<JsonElement>> GetAsync([FromRoute] int wisagId, CancellationToken cancellationToken)
|
||||
{
|
||||
var response = await sender.Send(new GetWorkflowsQuery(wisagId), cancellationToken);
|
||||
|
||||
var sources = response.Data?.Sources
|
||||
.Cast<object>()
|
||||
.ToList()
|
||||
?? [];
|
||||
|
||||
return Ok(new WorkflowsResponse
|
||||
{
|
||||
Meta = response.Meta,
|
||||
Sources = sources
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies a closing decision to a workflow process.
|
||||
/// </summary>
|
||||
/// <param name="wisagId">WISAG user identifier.</param>
|
||||
/// <param name="source">Source system name.</param>
|
||||
/// <param name="processId">Process identifier.</param>
|
||||
/// <param name="request">Closing request payload.</param>
|
||||
/// <param name="cancellationToken">Cancellation token.</param>
|
||||
/// <returns>Close result envelope.</returns>
|
||||
[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<ActionResult<MetaOnlyResponse>> 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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -31,16 +31,12 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Controllers\" />
|
||||
<None Include="Properties\PublishProfiles\IISProfileNet8.pubxml.user" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include="Properties\PublishProfiles\IISProfileNet8.pubxml.user" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\core\DigitalData.TaskFlow.Application\DigitalData.TaskFlow.Application.csproj" />
|
||||
<ProjectReference Include="..\..\infrastructure\DigitalData.TaskFlow.Infrastructure\DigitalData.TaskFlow.Infrastructure.csproj" />
|
||||
<ProjectReference Include="..\..\core\DigitalData.TaskFlow.Application\DigitalData.TaskFlow.Application.csproj" />
|
||||
<ProjectReference Include="..\..\infrastructure\DigitalData.TaskFlow.Infrastructure\DigitalData.TaskFlow.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 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 <c>X-Api-Key</c> request header.
|
||||
/// </summary>
|
||||
public sealed class ApiKeyAuthMiddleware(RequestDelegate next, IOptions<ApiKeyAuthOptions> apiKeyOptions)
|
||||
public sealed class ApiKeyAuthMiddleware(RequestDelegate next, IOptions<ApiKeyAuthOptions> options)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
private readonly string? _configuredApiKey = apiKeyOptions.Value.Key;
|
||||
|
||||
/// <summary>
|
||||
/// Executes middleware logic.
|
||||
/// </summary>
|
||||
/// <param name="context">HTTP context.</param>
|
||||
// Snapshot at startup; options are not expected to change at runtime.
|
||||
private readonly string? _configuredKey = options.Value.Key;
|
||||
|
||||
/// <inheritdoc />
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Handles exceptions globally and returns ticket-compatible API envelopes.
|
||||
/// Handles exceptions globally and returns RFC 7807 (Problem Details) compatible API envelopes.
|
||||
/// </summary>
|
||||
public sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
/// <summary>
|
||||
/// Executes the middleware.
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="context">HTTP context.</param>
|
||||
/// <param name="context"></param>
|
||||
/// <returns></returns>
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user