using GB5Shared.DTO.Framework.Login; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace FBCKSL.Hubs; public class FeedbackHub : Hub { private readonly ILogger _logger; public FeedbackHub(ILogger logger) => _logger = logger; public override async Task OnConnectedAsync() { var login = GetLoginDTO(); await Groups.AddToGroupAsync(Context.ConnectionId, UserGroup(login)); await Groups.AddToGroupAsync(Context.ConnectionId, TenantGroup(login)); _logger.LogInformation("FeedbackHub connected: {ConnectionId} user {UserId}", Context.ConnectionId, login.UserId); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var login = GetLoginDTO(); await Groups.RemoveFromGroupAsync(Context.ConnectionId, UserGroup(login)); await Groups.RemoveFromGroupAsync(Context.ConnectionId, TenantGroup(login)); if (exception is not null) _logger.LogWarning(exception, "FeedbackHub disconnected with error: {ConnectionId}", Context.ConnectionId); await base.OnDisconnectedAsync(exception); } private LoginDTO GetLoginDTO() { var http = Context.GetHttpContext() ?? throw new InvalidOperationException("FeedbackHub invoked outside HTTP context."); var raw = http.Request.Headers["Login"].FirstOrDefault() ?? throw new InvalidOperationException("Login header missing."); return System.Text.Json.JsonSerializer.Deserialize(raw) ?? throw new InvalidOperationException("Login header is invalid JSON."); } // Group per user (targeted push: memo sent to recipient, recognition to recipient) internal static string UserGroup(LoginDTO login) => $"fbck:user:{login.UserId}"; // Group per tenant (public recognition wall broadcast) internal static string TenantGroup(LoginDTO login) => $"fbck:client:{login.ClientId}"; }