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(); } }