using ZB.MOM.WW.ScadaBridge.CentralUI.Auth; using Microsoft.AspNetCore.Components.Authorization; using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management; using ZB.MOM.WW.ScadaBridge.Commons.Serialization; using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections; 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, serializes the endpoint config, /// and translates transport exceptions into a typed /// . /// /// /// Site-side actors (SiteCommunicationActor + DataConnectionManagerActor) /// do not unwrap the central trust envelope, so the role check MUST run here — never /// on the site (mirrors ). Transport failures collapse into /// a or /// result so the editor can show an inline outcome rather than throwing. /// public sealed class EndpointVerificationService : IEndpointVerificationService { 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 EndpointVerificationService(CommunicationService communication, AuthenticationStateProvider auth) { _communication = communication ?? throw new ArgumentNullException(nameof(communication)); _auth = auth ?? throw new ArgumentNullException(nameof(auth)); } /// public async Task VerifyAsync( string siteIdentifier, string connectionName, string protocol, OpcUaEndpointConfig config, 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.GetAuthoritativeStateAsync(); if (!state.User.HasClaim(JwtTokenService.RoleClaimType, Roles.Designer)) { return new VerifyEndpointResult( false, VerifyFailureKind.ServerError, "Not authorized.", null); } var configJson = OpcUaEndpointConfigSerializer.Serialize(config); var command = new VerifyEndpointCommand(connectionName, protocol, configJson); try { return await _communication.VerifyEndpointAsync(siteIdentifier, command, cancellationToken); } catch (TimeoutException ex) { // Akka Ask timed out — the site (or its OPC UA connect probe) didn't answer // within CommunicationOptions.QueryTimeout. Surface as a typed Timeout // failure so the editor can render the outcome inline. return new VerifyEndpointResult( false, VerifyFailureKind.Timeout, ex.Message, null); } catch (OperationCanceledException) { // Caller-initiated cancel — propagate so Blazor can drop the response // cleanly. Distinct from Timeout (which the editor renders inline). throw; } catch (Exception ex) { // Any other transport / serialization failure: keep the editor alive and // show the failure inline rather than crashing the form. return new VerifyEndpointResult( false, VerifyFailureKind.ServerError, ex.Message, null); } } }