using System.Security.Claims; using System.Text.Json; 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 /// read-only facade that dispatches the /// to the central ManagementActor through the in-process /// (the same Ask seam the HTTP /management /// endpoint uses). The actor walks the full inheritance chain and returns the /// effective member set + staleness; none of that is re-implemented here. Mirrors /// 's read path. /// public sealed class TemplateInheritanceQueryService : ITemplateInheritanceQueryService { /// /// camelCase + case-insensitive, matching ManagementActor.SerializeResult's /// options. is produced with those settings, /// so the deserializer must mirror them to bind every property. /// private static readonly JsonSerializerOptions ResultDeserializerOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; 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 TemplateInheritanceQueryService( 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 ResolveAsync( int templateId, CancellationToken cancellationToken = default) { var response = await SendAsync(new GetResolvedTemplateMembersCommand(templateId), cancellationToken); if (response is ManagementSuccess success) { return JsonSerializer.Deserialize( success.JsonData, ResultDeserializerOptions); } // Read path: log + return null so the editor falls back to its stored-row view. _logger.LogWarning( "GetResolvedTemplateMembers failed for template {TemplateId}: {Response}", templateId, DescribeFailure(response)); return null; } /// /// Wraps in a for the /// current principal and Asks the ManagementActor. Transport faults (timeout, /// actor not yet started) become a synthetic so the /// caller handles 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 (mirrors the /// claim set the HTTP endpoint constructs). /// private async Task BuildAuthenticatedUserAsync() { var state = await _auth.GetAuthenticationStateAsync(); 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.", }; }