using System.Text.Json;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace ScadaLink.InboundAPI;
///
/// WP-1: POST /api/{methodName} endpoint registration.
/// WP-2: Method routing and parameter validation.
/// WP-3: Script execution on central.
/// WP-5: Error handling — 401, 403, 400, 500.
///
public static class EndpointExtensions
{
public static IEndpointRouteBuilder MapInboundAPI(this IEndpointRouteBuilder endpoints)
{
endpoints.MapPost("/api/{methodName}", HandleInboundApiRequest);
return endpoints;
}
private static async Task HandleInboundApiRequest(
HttpContext httpContext,
string methodName)
{
var logger = httpContext.RequestServices.GetRequiredService>();
var validator = httpContext.RequestServices.GetRequiredService();
var executor = httpContext.RequestServices.GetRequiredService();
var routeHelper = httpContext.RequestServices.GetRequiredService();
var options = httpContext.RequestServices.GetRequiredService>().Value;
// WP-1: Extract and validate API key
var apiKeyValue = httpContext.Request.Headers["X-API-Key"].FirstOrDefault();
var validationResult = await validator.ValidateAsync(apiKeyValue, methodName, httpContext.RequestAborted);
if (!validationResult.IsValid)
{
// WP-5: Failures-only logging
logger.LogWarning(
"Inbound API auth failure for method {Method}: {Error} (status {StatusCode})",
methodName, validationResult.ErrorMessage, validationResult.StatusCode);
return Results.Json(
new { error = validationResult.ErrorMessage },
statusCode: validationResult.StatusCode);
}
var method = validationResult.Method!;
// WP-2: Deserialize and validate parameters
JsonElement? body = null;
try
{
if (httpContext.Request.ContentLength > 0 || httpContext.Request.ContentType?.Contains("json") == true)
{
using var doc = await JsonDocument.ParseAsync(
httpContext.Request.Body, cancellationToken: httpContext.RequestAborted);
body = doc.RootElement.Clone();
}
}
catch (JsonException)
{
return Results.Json(
new { error = "Invalid JSON in request body" },
statusCode: 400);
}
var paramResult = ParameterValidator.Validate(body, method.ParameterDefinitions);
if (!paramResult.IsValid)
{
return Results.Json(
new { error = paramResult.ErrorMessage },
statusCode: 400);
}
// WP-3: Execute the method's script
var timeout = method.TimeoutSeconds > 0
? TimeSpan.FromSeconds(method.TimeoutSeconds)
: options.DefaultMethodTimeout;
var scriptResult = await executor.ExecuteAsync(
method, paramResult.Parameters, routeHelper, timeout, httpContext.RequestAborted);
if (!scriptResult.Success)
{
// WP-5: 500 for script failures, safe error message
logger.LogWarning(
"Inbound API script failure for method {Method}: {Error}",
methodName, scriptResult.ErrorMessage);
return Results.Json(
new { error = scriptResult.ErrorMessage ?? "Internal server error" },
statusCode: 500);
}
// Return the script result as JSON
if (scriptResult.ResultJson != null)
{
return Results.Text(scriptResult.ResultJson, "application/json", statusCode: 200);
}
return Results.Ok();
}
}