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.Application.Features.Workflows.Queries;
|
||||||
using DigitalData.TaskFlow.API.Contracts.Common;
|
|
||||||
using DigitalData.TaskFlow.Application.Features.Workflows.Commands.CloseWorkflow;
|
|
||||||
using DigitalData.TaskFlow.Application.Features.Workflows.Queries.GetWorkflows;
|
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace DigitalData.TaskFlow.API.Controllers;
|
namespace DigitalData.TaskFlow.API.Controllers;
|
||||||
|
|
||||||
@@ -21,64 +19,12 @@ public sealed class WorkflowsController(ISender sender) : ControllerBase
|
|||||||
/// <param name="cancellationToken">Cancellation token.</param>
|
/// <param name="cancellationToken">Cancellation token.</param>
|
||||||
/// <returns>Workflow response envelope grouped by source.</returns>
|
/// <returns>Workflow response envelope grouped by source.</returns>
|
||||||
[HttpGet("{wisagId:int}")]
|
[HttpGet("{wisagId:int}")]
|
||||||
[ProducesResponseType(typeof(WorkflowsResponse), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(JsonElement), StatusCodes.Status200OK)]
|
||||||
[ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status400BadRequest)]
|
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||||
[ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status401Unauthorized)]
|
[ProducesResponseType(StatusCodes.Status502BadGateway)]
|
||||||
[ProducesResponseType(typeof(MetaOnlyResponse), StatusCodes.Status500InternalServerError)]
|
[ProducesResponseType(StatusCodes.Status500InternalServerError)]
|
||||||
public async Task<ActionResult<WorkflowsResponse>> GetAsync(
|
public async Task<ActionResult<JsonElement>> GetAsync([FromRoute] int wisagId, CancellationToken cancellationToken)
|
||||||
[FromRoute] int wisagId,
|
|
||||||
CancellationToken cancellationToken)
|
|
||||||
{
|
{
|
||||||
var response = await sender.Send(new GetWorkflowsQuery(wisagId), cancellationToken);
|
return Ok(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
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
@@ -31,16 +31,12 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Folder Include="Controllers\" />
|
<None Include="Properties\PublishProfiles\IISProfileNet8.pubxml.user" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="Properties\PublishProfiles\IISProfileNet8.pubxml.user" />
|
<ProjectReference Include="..\..\core\DigitalData.TaskFlow.Application\DigitalData.TaskFlow.Application.csproj" />
|
||||||
</ItemGroup>
|
<ProjectReference Include="..\..\infrastructure\DigitalData.TaskFlow.Infrastructure\DigitalData.TaskFlow.Infrastructure.csproj" />
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\..\core\DigitalData.TaskFlow.Application\DigitalData.TaskFlow.Application.csproj" />
|
|
||||||
<ProjectReference Include="..\..\infrastructure\DigitalData.TaskFlow.Infrastructure\DigitalData.TaskFlow.Infrastructure.csproj" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,54 +1,55 @@
|
|||||||
using DigitalData.TaskFlow.API.Contracts.Common;
|
|
||||||
using DigitalData.TaskFlow.API.Options;
|
using DigitalData.TaskFlow.API.Options;
|
||||||
using DigitalData.TaskFlow.Domain.Common;
|
|
||||||
using DigitalData.TaskFlow.Domain.Enums;
|
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace DigitalData.TaskFlow.API.Middleware;
|
namespace DigitalData.TaskFlow.API.Middleware;
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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 static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
private readonly string? _configuredApiKey = apiKeyOptions.Value.Key;
|
|
||||||
|
|
||||||
/// <summary>
|
// Snapshot at startup; options are not expected to change at runtime.
|
||||||
/// Executes middleware logic.
|
private readonly string? _configuredKey = options.Value.Key;
|
||||||
/// </summary>
|
|
||||||
/// <param name="context">HTTP context.</param>
|
/// <inheritdoc />
|
||||||
public async Task InvokeAsync(HttpContext context)
|
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))
|
if (!context.Request.Path.StartsWithSegments("/api", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
await next(context);
|
await next(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context.Request.Headers.TryGetValue("X-Api-Key", out var values)
|
// Treat a missing or empty configured key as a misconfiguration — deny all traffic.
|
||||||
&& string.Equals(values.FirstOrDefault(), _configuredApiKey, StringComparison.Ordinal))
|
if (!string.IsNullOrWhiteSpace(_configuredKey)
|
||||||
|
&& context.Request.Headers.TryGetValue("X-Api-Key", out var provided)
|
||||||
|
&& string.Equals(provided, _configuredKey, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
await next(context);
|
await next(context);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
await WriteUnauthorizedAsync(context);
|
||||||
context.Response.ContentType = "application/json";
|
}
|
||||||
|
|
||||||
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 =
|
type = "https://tools.ietf.org/html/rfc7807",
|
||||||
[
|
title = "Unauthorized",
|
||||||
new MetaEntry
|
status = StatusCodes.Status401Unauthorized,
|
||||||
{
|
detail = "A valid API key must be supplied via the X-Api-Key header."
|
||||||
Code = (int)MetaCode.InvalidRequest,
|
|
||||||
Message = "Authentifizierung fehlgeschlagen"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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.Application.Common.Exceptions;
|
||||||
using DigitalData.TaskFlow.API.Contracts.Common;
|
|
||||||
using DigitalData.TaskFlow.Domain.Common;
|
|
||||||
using DigitalData.TaskFlow.Domain.Enums;
|
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace DigitalData.TaskFlow.API.Middleware;
|
namespace DigitalData.TaskFlow.API.Middleware;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles exceptions globally and returns ticket-compatible API envelopes.
|
/// Handles exceptions globally and returns RFC 7807 (Problem Details) compatible API envelopes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
public sealed class ExceptionHandlingMiddleware(RequestDelegate next, ILogger<ExceptionHandlingMiddleware> logger)
|
||||||
{
|
{
|
||||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes the middleware.
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="context">HTTP context.</param>
|
/// <param name="context"></param>
|
||||||
|
/// <returns></returns>
|
||||||
public async Task InvokeAsync(HttpContext context)
|
public async Task InvokeAsync(HttpContext context)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await next(context);
|
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;
|
var errors = ex.Errors
|
||||||
context.Response.ContentType = "application/json";
|
.GroupBy(e => e.PropertyName)
|
||||||
|
.ToDictionary(
|
||||||
|
g => g.Key,
|
||||||
|
g => g.Select(e => e.ErrorMessage).ToArray()
|
||||||
|
);
|
||||||
|
|
||||||
var response = new MetaOnlyResponse
|
var response = new
|
||||||
{
|
{
|
||||||
Meta =
|
type = "https://tools.ietf.org/html/rfc7807",
|
||||||
[
|
title = "Validation Error",
|
||||||
new MetaEntry
|
status = StatusCodes.Status400BadRequest,
|
||||||
{
|
errors
|
||||||
Code = (int)apiException.MetaCode,
|
|
||||||
Message = apiException.Message
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
context.Response.StatusCode = StatusCodes.Status400BadRequest;
|
||||||
|
context.Response.ContentType = "application/problem+json";
|
||||||
await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions));
|
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;
|
var response = new
|
||||||
context.Response.ContentType = "application/json";
|
|
||||||
|
|
||||||
var message = validationException.Errors.FirstOrDefault()?.ErrorMessage
|
|
||||||
?? "Ungultiger Request";
|
|
||||||
|
|
||||||
var response = new MetaOnlyResponse
|
|
||||||
{
|
{
|
||||||
Meta =
|
type = "https://tools.ietf.org/html/rfc7807",
|
||||||
[
|
title = "Bad Gateway",
|
||||||
new MetaEntry
|
status = StatusCodes.Status502BadGateway,
|
||||||
{
|
detail = ex.Message
|
||||||
Code = (int)MetaCode.InvalidRequest,
|
|
||||||
Message = message
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
context.Response.StatusCode = StatusCodes.Status502BadGateway;
|
||||||
|
context.Response.ContentType = "application/problem+json";
|
||||||
await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions));
|
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;
|
var response = new
|
||||||
context.Response.ContentType = "application/json";
|
|
||||||
|
|
||||||
var response = new MetaOnlyResponse
|
|
||||||
{
|
{
|
||||||
Meta =
|
type = "https://tools.ietf.org/html/rfc7807",
|
||||||
[
|
title = "Internal Server Error",
|
||||||
new MetaEntry
|
status = StatusCodes.Status500InternalServerError
|
||||||
{
|
|
||||||
Code = (int)MetaCode.InternalError,
|
|
||||||
Message = notSupportedException.Message
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
};
|
||||||
await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions));
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
logger.LogError(exception, "Unhandled exception.");
|
|
||||||
|
|
||||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||||
context.Response.ContentType = "application/json";
|
context.Response.ContentType = "application/problem+json";
|
||||||
|
|
||||||
var response = new MetaOnlyResponse
|
|
||||||
{
|
|
||||||
Meta =
|
|
||||||
[
|
|
||||||
new MetaEntry
|
|
||||||
{
|
|
||||||
Code = (int)MetaCode.InternalError,
|
|
||||||
Message = "Interner Fehler"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
};
|
|
||||||
await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions));
|
await context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ try
|
|||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddEndpointsApiExplorer();
|
builder.Services.AddEndpointsApiExplorer();
|
||||||
|
var apiKeyConfigured = !string.IsNullOrWhiteSpace(config[$"{ApiKeyAuthOptions.SectionName}:Key"]);
|
||||||
|
|
||||||
builder.Services.AddSwaggerGen(options =>
|
builder.Services.AddSwaggerGen(options =>
|
||||||
{
|
{
|
||||||
options.SwaggerDoc("v1", new OpenApiInfo
|
options.SwaggerDoc("v1", new OpenApiInfo
|
||||||
@@ -77,11 +79,11 @@ try
|
|||||||
Title = "DigitalData TaskFlow API",
|
Title = "DigitalData TaskFlow API",
|
||||||
Version = "v1",
|
Version = "v1",
|
||||||
Description = """
|
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
|
Contact = new OpenApiContact
|
||||||
{
|
{
|
||||||
@@ -90,6 +92,32 @@ try
|
|||||||
Email = "info-flow@digitaldata.works"
|
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
|
builder.Services
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
"windowsAuthentication": false,
|
"windowsAuthentication": false,
|
||||||
"anonymousAuthentication": true,
|
"anonymousAuthentication": true,
|
||||||
"iisExpress": {
|
"iisExpress": {
|
||||||
"applicationUrl": "http://localhost:3557",
|
"applicationUrl": "http://localhost:63206",
|
||||||
"sslPort": 44339
|
"sslPort": 44315
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"profiles": {
|
"profiles": {
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "swagger",
|
||||||
"applicationUrl": "http://localhost:5298",
|
"applicationUrl": "http://localhost:5062",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
"launchBrowser": true,
|
"launchBrowser": true,
|
||||||
"launchUrl": "swagger",
|
"launchUrl": "swagger",
|
||||||
"applicationUrl": "https://localhost:7050;http://localhost:5299",
|
"applicationUrl": "https://localhost:7131;http://localhost:5062",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
"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": {
|
"ApiKey": {
|
||||||
"Key": null
|
"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": {
|
"Swagger": {
|
||||||
"EnabledInProduction": false
|
"EnabledInProduction": false
|
||||||
},
|
},
|
||||||
"Database": {
|
|
||||||
"CommandTimeoutSeconds": 30,
|
|
||||||
"WorkflowListingObjectType": "Ferdinand"
|
|
||||||
},
|
|
||||||
"Application": {
|
"Application": {
|
||||||
"LogDirectory": "logs"
|
"LogDirectory": "logs"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user