using DXPSL.Common; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.SignalR; namespace DXPSL.Hubs; // Real-time push for PO/ASN/Invoice status (Part 2/Part 3 of the DXP plan — "Universal Inbox"). // Caller identity is reconstructed ONLY from the validated JWT the ASP.NET Core JWT bearer // middleware already attached to HttpContext.User (see Program.cs's OnMessageReceived hook that // lets SignalR's WebSocket transport carry the token via ?access_token=, since browsers cannot // set a custom Authorization header on the transport handshake) — never trusted from a hub // method parameter, same rule CLAUDE.md states for every other Hub in this repo. // // [Authorize] is required here (unlike CLAUDE.md's CollabHub example, which relies on the // internal LoginDTO-header trust model with no ASP.NET auth middleware in play at all) — DXPHub // sits behind real JWT bearer middleware, and without this attribute SignalR would accept an // unauthenticated connection (Context.User would just be an empty ClaimsPrincipal, silently // landing in the "dxp:party:0" group instead of being rejected). [Authorize] public class DXPHub : Hub { private readonly ILogger _logger; public DXPHub(ILogger logger) => _logger = logger; public override async Task OnConnectedAsync() { var caller = DXPCallerContext.FromClaims(Context.User?.Claims ?? Enumerable.Empty()); await Groups.AddToGroupAsync(Context.ConnectionId, PartyGroup(caller.DxpPartyId)); _logger.LogInformation("DXPHub connected: {ConnectionId} party {DxpPartyId}", Context.ConnectionId, caller.DxpPartyId); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var caller = DXPCallerContext.FromClaims(Context.User?.Claims ?? Enumerable.Empty()); await Groups.RemoveFromGroupAsync(Context.ConnectionId, PartyGroup(caller.DxpPartyId)); if (exception is not null) _logger.LogWarning(exception, "DXPHub disconnected with error: {ConnectionId}", Context.ConnectionId); await base.OnDisconnectedAsync(exception); } // Group naming convention (Part 2 of the DXP plan): dxp:party:{dxpPartyId} — a party's // relevant notifications span every tenant it's linked to, so the group is keyed on the // global DxpPartyId, not per-tenant. public static string PartyGroup(int dxpPartyId) => $"dxp:party:{dxpPartyId}"; }