using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs; /// /// SignalR hub for per-session MxEvent push. Clients call /// to join the group for a specific /// session; the dashboard's MxEvent broadcaster (a future hook on /// EventStreamService) sends messages to session:{id}. /// /// /// The publisher side is intentionally a follow-up. Today the dashboard's /// per-session event view is fed by the snapshot hub, which carries the /// rolling recent-events list. Once a dedicated MxEvent broadcaster /// lands, this hub's group convention is what it will publish to. /// [Authorize(Policy = DashboardAuthenticationDefaults.HubClientsPolicy)] public sealed class EventsHub : Hub { /// Method name used to push individual MxEvent values to clients. public const string EventMessage = "MxEvent"; /// Computes the SignalR group name for a given session id. /// The session id. /// The group name for the session. public static string GroupName(string sessionId) => $"session:{sessionId}"; /// /// Subscribes the calling SignalR connection to the per-session events /// group, so that events broadcast by /// for that session reach this /// client. /// /// /// Server-038: in v1 the hub-level /// (HubClientsPolicy) only checks that the caller carries one of /// the dashboard roles (Admin or Viewer); both roles may subscribe to /// any session id they choose. This is acceptable today because (a) the /// dashboard's per-session views show non-secret session metadata that /// any authenticated dashboard user can already see, and (b) value /// logging in the source gRPC stream is gated by the same redaction /// policy that protects logs. The per-session ACL that gates the gRPC /// StreamEvents RPC is intentionally not yet mirrored here. /// TODO(per-session-acl): once a role/scope is introduced that scopes a /// Viewer to a specific session or tenant, add a session-access check /// at this seam — either inline (consult the per-user allowed-session /// set on Context.User claims / Context.Items) or via a /// dedicated authorization policy applied to the hub method itself. /// /// Session id to subscribe the caller to. /// A task that represents the asynchronous operation. public Task SubscribeSession(string sessionId) { if (string.IsNullOrWhiteSpace(sessionId)) { return Task.CompletedTask; } return Groups.AddToGroupAsync(Context.ConnectionId, GroupName(sessionId)); } /// Unsubscribes the calling SignalR connection from the per-session events group. /// Session id to unsubscribe the caller from. /// A task representing the unsubscription operation. public Task UnsubscribeSession(string sessionId) { if (string.IsNullOrWhiteSpace(sessionId)) { return Task.CompletedTask; } return Groups.RemoveFromGroupAsync(Context.ConnectionId, GroupName(sessionId)); } }