using Microsoft.AspNetCore.Components.Authorization; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.Communication; using ZB.MOM.WW.ScadaBridge.Security; namespace ZB.MOM.WW.ScadaBridge.CentralUI.Services; /// /// Default implementation — a thin facade over /// that enforces the /// CentralUI-side Design-role trust boundary and translates transport /// exceptions into a typed result. /// /// /// Site-side actors (SiteCommunicationActor + DeploymentManagerActor) /// do not unwrap the central trust envelope, so the role check MUST run here — /// never on the site. Transport failures collapse into Timeout or /// ServerError so the dialog can show an inline banner while leaving the /// manual node-id paste field usable. /// public sealed class BrowseService : IBrowseService { private readonly CommunicationService _communication; private readonly AuthenticationStateProvider _auth; /// /// Initializes a new instance of the . /// /// Central-side cluster communication service. /// Authentication state provider used for the Design-role guard. public BrowseService(CommunicationService communication, AuthenticationStateProvider auth) { _communication = communication ?? throw new ArgumentNullException(nameof(communication)); _auth = auth ?? throw new ArgumentNullException(nameof(auth)); } /// public async Task BrowseChildrenAsync( string siteId, string connectionName, string? parentNodeId, CancellationToken cancellationToken = default) { // CentralUI-side role guard — sites don't enforce envelope-level roles, // so the Designer check must happen here before any cross-cluster traffic. var state = await _auth.GetAuthenticationStateAsync(); if (!state.User.HasClaim(JwtTokenService.RoleClaimType, Roles.Designer)) { return new BrowseNodeResult( Array.Empty(), Truncated: false, new BrowseFailure(BrowseFailureKind.ServerError, "Not authorized.")); } try { return await _communication.BrowseNodeAsync( siteId, new BrowseNodeCommand(connectionName, parentNodeId), cancellationToken); } catch (TimeoutException ex) { // Akka Ask timed out — the site (or its OPC UA session) didn't answer // within CommunicationOptions.QueryTimeout. Surface as a typed // Timeout failure so the dialog can render an inline banner. return new BrowseNodeResult( Array.Empty(), Truncated: false, new BrowseFailure(BrowseFailureKind.Timeout, ex.Message)); } catch (OperationCanceledException) { // Caller-initiated cancel — propagate so Blazor can drop the response // cleanly. Distinct from Timeout (which the dialog renders inline). throw; } catch (Exception ex) { // Any other transport / serialization failure: keep the dialog // alive and let the user fall back to manual node-id paste. return new BrowseNodeResult( Array.Empty(), Truncated: false, new BrowseFailure(BrowseFailureKind.ServerError, ex.Message)); } } }