using ZB.MOM.WW.ScadaBridge.CentralUI.Auth; using System.Security.Claims; using Akka.Actor; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Logging; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.ManagementService; using ZB.MOM.WW.ScadaBridge.Security; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; /// /// Default implementation — a thin facade that /// dispatches the to the central /// ManagementActor through the in-process /// (the same Ask seam the HTTP /management endpoint uses). The actor authorizes /// the command against the supplied (Designer-gated) and /// runs every move guard server-side — the target site must exist, the target must not /// already own a same-named connection, no instance binding may reference the /// connection, and no name-based native-alarm-source reference may be orphaned. None of /// that is re-implemented here; a guard failure returns as a classified error. /// /// /// Mirrors : the current Blazor principal is projected /// to an so the server's role gate runs against the real /// identity, and the three management response shapes plus any transport fault collapse /// into a typed . /// public sealed class DataConnectionMoveService : IDataConnectionMoveService { private static readonly TimeSpan AskTimeout = TimeSpan.FromSeconds(30); private readonly ManagementActorHolder _holder; private readonly AuthenticationStateProvider _auth; private readonly ILogger _logger; /// /// Initializes a new instance of the . /// /// Holder for the central ManagementActor reference. /// Authentication state provider used to project the current principal. /// Logger instance. public DataConnectionMoveService( ManagementActorHolder holder, AuthenticationStateProvider auth, ILogger logger) { _holder = holder ?? throw new ArgumentNullException(nameof(holder)); _auth = auth ?? throw new ArgumentNullException(nameof(auth)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// public async Task MoveAsync( int connectionId, int targetSiteId, CancellationToken cancellationToken = default) { var response = await SendAsync( new MoveDataConnectionCommand(connectionId, targetSiteId), cancellationToken); return response switch { ManagementSuccess => DataConnectionMoveResult.Ok(), ManagementUnauthorized unauthorized => DataConnectionMoveResult.Fail(unauthorized.Message), ManagementError error => DataConnectionMoveResult.Fail(error.Error), _ => DataConnectionMoveResult.Fail(DescribeFailure(response)), }; } /// /// Wraps in a for the /// current principal and Asks the ManagementActor. Transport faults (timeout, /// actor not yet started, cancellation→propagated) become a synthetic /// so callers handle one response shape. /// private async Task SendAsync(object command, CancellationToken cancellationToken) { var actor = _holder.ActorRef; if (actor is null) { return new ManagementError( string.Empty, "Management service is not ready.", "SERVICE_UNAVAILABLE"); } var user = await BuildAuthenticatedUserAsync(); var envelope = new ManagementEnvelope(user, command, Guid.NewGuid().ToString("N")); try { return await actor.Ask(envelope, AskTimeout, cancellationToken); } catch (OperationCanceledException) { // Caller-initiated cancel (e.g. circuit teardown) — propagate cleanly. throw; } catch (Exception ex) { _logger.LogError(ex, "ManagementActor Ask failed for {Command}", command.GetType().Name); return new ManagementError(string.Empty, ex.Message, "TRANSPORT_ERROR"); } } /// /// Projects the current Blazor to the /// the actor authorizes against — username, /// display name, role claims, and the permitted-site scope claims (mirrors the /// claim set the HTTP endpoint constructs). /// private async Task BuildAuthenticatedUserAsync() { var state = await _auth.GetAuthoritativeStateAsync(); var principal = state.User; var username = principal.FindFirst(JwtTokenService.UsernameClaimType)?.Value ?? "unknown"; var displayName = principal.FindFirst(JwtTokenService.DisplayNameClaimType)?.Value ?? username; var roles = principal.FindAll(JwtTokenService.RoleClaimType).Select(c => c.Value).ToArray(); var permittedSiteIds = principal.FindAll(JwtTokenService.SiteIdClaimType).Select(c => c.Value).ToArray(); return new AuthenticatedUser(username, displayName, roles, permittedSiteIds); } /// Renders a fallback description for an unexpected/failure response. private static string DescribeFailure(object response) => response switch { ManagementUnauthorized unauthorized => unauthorized.Message, ManagementError error => error.Error, _ => "Unexpected response from the management service.", }; }