using System; using System.Linq; using System.Threading.Tasks; using GB5Shared.DTO.Framework.Login; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace FrameworkSL.Hubs.GOP { // ============================================================ // GopExecutionHub — server-push hub for real-time GOP execution // status updates. // // Architecture: // • Server-push only: no client-initiated methods. // • GopExecutionPipeline (BackgroundService) injects // IHubContext and // calls Clients.Group(...).ReceiveExecutionUpdate(...) after // each pipeline step completes. // // Group convention: // "gop:client:{clientId}" — all connected clients for a tenant // receive all execution updates for that tenant. // // LoginDTO: // ALWAYS reconstructed from Context.GetHttpContext() HTTP headers. // NEVER accepted from client payload. // // Registration (Program.cs): // app.MapHub("/hubs/gop-execution"); // ============================================================ public class GopExecutionHub : Hub { private readonly ILogger _Logger; public GopExecutionHub(ILogger logger) { _Logger = logger; } // ── Group naming convention ─────────────────────────────────────────────── /// Tenant-scoped group: all connected clients for a given tenant. public static string TenantGroup(int clientId) => $"gop:client:{clientId}"; // ── Connection lifecycle ────────────────────────────────────────────────── public override async Task OnConnectedAsync() { var login = GetLoginDTO(); var group = TenantGroup(login.ClientId); await Groups.AddToGroupAsync(Context.ConnectionId, group); _Logger.LogInformation( "GopExecutionHub connected: {ConnectionId} tenant {ClientId} user {UserId}", Context.ConnectionId, login.ClientId, login.UserId); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var login = GetLoginDTO(); var group = TenantGroup(login.ClientId); await Groups.RemoveFromGroupAsync(Context.ConnectionId, group); if (exception is not null) _Logger.LogWarning(exception, "GopExecutionHub disconnected with error: {ConnectionId}", Context.ConnectionId); await base.OnDisconnectedAsync(exception); } // ── Helper ──────────────────────────────────────────────────────────────── // ALWAYS reconstruct LoginDTO from verified HTTP context — never from client payload. private LoginDTO GetLoginDTO() { var http = Context.GetHttpContext() ?? throw new InvalidOperationException("Hub invoked outside HTTP context."); var raw = http.Request.Headers["Login"].FirstOrDefault() ?? throw new InvalidOperationException("Login header is missing."); return System.Text.Json.JsonSerializer.Deserialize(raw) ?? throw new InvalidOperationException("Login header contains invalid JSON."); } } }