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:
Joseph Doherty
2026-05-28 09:37:45 -04:00
parent 6d87ee3c3b
commit 7b0b9c7365
1531 changed files with 11180 additions and 11054 deletions
@@ -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();
}
}