refactor: rename ScadaLink → ZB.MOM.WW.ScadaBridge (code + projects + namespaces)
Solution + 23 src projects + 26 test projects renamed; folders, csproj, namespaces, and ScadaLinkDbContext/ScadaBridgeDbContext class updated. ActorSystem "scadalink" → "scadabridge", Akka seed-node URLs migrated. SQL roles/logins, LDAP domains, CLI command name, and CLI config dir (~/.scadalink → ~/.scadabridge) also renamed. Build green; 5 Host.Tests fail awaiting SQL login rename in next commit. Pre-existing StaleTagMonitor timing flakes unchanged. Rename script committed at tools/rename-to-scadabridge.sh.
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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;
|
||||
using ZB.MOM.WW.ScadaBridge.InboundAPI.Middleware;
|
||||
|
||||
namespace ZB.MOM.WW.ScadaBridge.InboundAPI;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class EndpointExtensions
|
||||
{
|
||||
/// <summary>Registers the <c>POST /api/{methodName}</c> inbound API endpoint with the active-node gate and body-size filter applied.</summary>
|
||||
/// <param name="endpoints">The route builder to add the endpoint to.</param>
|
||||
public static IEndpointRouteBuilder MapInboundAPI(this IEndpointRouteBuilder endpoints)
|
||||
{
|
||||
endpoints.MapPost("/api/{methodName}", HandleInboundApiRequest)
|
||||
// InboundAPI-006 / InboundAPI-008: active-node gating + request body
|
||||
// size cap are enforced by the endpoint filter before the handler runs.
|
||||
.AddEndpointFilter<InboundApiEndpointFilter>();
|
||||
return endpoints;
|
||||
}
|
||||
|
||||
private static async Task<IResult> HandleInboundApiRequest(
|
||||
HttpContext httpContext,
|
||||
string methodName)
|
||||
{
|
||||
var logger = httpContext.RequestServices.GetRequiredService<ILogger<ApiKeyValidator>>();
|
||||
var validator = httpContext.RequestServices.GetRequiredService<ApiKeyValidator>();
|
||||
var executor = httpContext.RequestServices.GetRequiredService<InboundScriptExecutor>();
|
||||
var routeHelper = httpContext.RequestServices.GetRequiredService<RouteHelper>();
|
||||
var options = httpContext.RequestServices.GetRequiredService<IOptions<InboundApiOptions>>().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!;
|
||||
|
||||
// Audit Log (#23 M4 Bundle D): publish the resolved API key name so
|
||||
// AuditWriteMiddleware can populate AuditEvent.Actor in its finally
|
||||
// block. Done AFTER validation succeeded — auth failures leave the
|
||||
// slot empty and the middleware records the row with Actor=null.
|
||||
httpContext.Items[AuditWriteMiddleware.AuditActorItemKey] =
|
||||
validationResult.ApiKey!.Name;
|
||||
|
||||
// WP-2: Deserialize and validate parameters
|
||||
JsonElement? body = null;
|
||||
try
|
||||
{
|
||||
// InboundAPI-020: the content-type sniff must be case-insensitive — a
|
||||
// request with `application/JSON` or `Application/Json` is still JSON
|
||||
// and must enter the body-parsing path. The previous case-sensitive
|
||||
// `Contains("json")` silently skipped JSON deserialization for any
|
||||
// capitalised value, leaving `body = null` and surfacing required
|
||||
// parameters as 400 "missing" even though the caller sent a valid body.
|
||||
if (httpContext.Request.ContentLength > 0
|
||||
|| httpContext.Request.ContentType?.Contains("json", StringComparison.OrdinalIgnoreCase) == 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;
|
||||
|
||||
// Audit Log #23 (ParentExecutionId): the inbound request's per-request
|
||||
// ExecutionId was minted early by AuditWriteMiddleware and stashed on
|
||||
// HttpContext.Items. Thread it into the executor so a routed
|
||||
// Route.To(...).Call(...) carries it as RouteToCallRequest.ParentExecutionId
|
||||
// — the spawned site script execution points back at this inbound request.
|
||||
var parentExecutionId =
|
||||
httpContext.Items.TryGetValue(
|
||||
AuditWriteMiddleware.InboundExecutionIdItemKey, out var stashedExecutionId)
|
||||
&& stashedExecutionId is Guid inboundExecutionId
|
||||
? inboundExecutionId
|
||||
: (Guid?)null;
|
||||
|
||||
var scriptResult = await executor.ExecuteAsync(
|
||||
method, paramResult.Parameters, routeHelper, timeout,
|
||||
httpContext.RequestAborted, parentExecutionId);
|
||||
|
||||
if (!scriptResult.Success)
|
||||
{
|
||||
// InboundAPI-004: a client-aborted request is not a script failure.
|
||||
// Do not pollute the failure log (reserved for genuine script errors)
|
||||
// and do not attempt to write a 500 body to an already-gone connection.
|
||||
if (httpContext.RequestAborted.IsCancellationRequested)
|
||||
{
|
||||
return Results.Empty;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user