Initial commit
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace PowderCoating.Web.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR hub that delivers real-time notifications to authenticated users.
|
||||
/// Each user is placed into a company-scoped SignalR group ("company-{companyId}") on connect so
|
||||
/// that server-side code can broadcast to an entire tenant without iterating individual connections.
|
||||
/// SuperAdmin users are also placed in the "superadmin" group for platform-wide alerts.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class NotificationHub : Hub
|
||||
{
|
||||
private readonly ILogger<NotificationHub> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="NotificationHub"/> with the required logger.
|
||||
/// </summary>
|
||||
public NotificationHub(ILogger<NotificationHub> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the SignalR framework when a client establishes a connection.
|
||||
/// Reads the CompanyId claim from the authenticated user and adds the connection to the
|
||||
/// corresponding company group. SuperAdmins are additionally added to the "superadmin" group.
|
||||
/// Errors are caught and logged rather than re-thrown so that a group-join failure does not
|
||||
/// abort the connection handshake for the client.
|
||||
/// </summary>
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var companyId = Context.User?.FindFirst("CompanyId")?.Value;
|
||||
if (!string.IsNullOrEmpty(companyId))
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"company-{companyId}");
|
||||
|
||||
if (Context.User?.IsInRole("SuperAdmin") == true)
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, "superadmin");
|
||||
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in NotificationHub.OnConnectedAsync for connection {ConnectionId}", Context.ConnectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the SignalR framework when a client disconnects, either cleanly or due to an error.
|
||||
/// Unexpected disconnects (non-null exception) are logged at Warning level so they appear in
|
||||
/// monitoring without spamming the error log during normal browser refreshes.
|
||||
/// </summary>
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
if (exception != null)
|
||||
_logger.LogWarning(exception, "NotificationHub client disconnected with error: {ConnectionId}", Context.ConnectionId);
|
||||
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace PowderCoating.Web.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// SignalR hub that delivers real-time shop floor updates to TV displays and worker devices.
|
||||
/// Each connection is placed in a company-scoped group ("shop-{companyId}") so that job status
|
||||
/// changes, batch assignments, and oven updates can be broadcast to all shop floor screens for
|
||||
/// a tenant without leaking data to other companies.
|
||||
/// </summary>
|
||||
[Authorize]
|
||||
public class ShopHub : Hub
|
||||
{
|
||||
private readonly ILogger<ShopHub> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of <see cref="ShopHub"/> with the required logger.
|
||||
/// </summary>
|
||||
public ShopHub(ILogger<ShopHub> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the SignalR framework when a shop floor client connects.
|
||||
/// Reads the CompanyId claim and adds the connection to the company-scoped shop group so that
|
||||
/// server-side broadcasts reach all screens for that company only.
|
||||
/// Errors are swallowed after logging to prevent a failed group join from dropping the connection.
|
||||
/// </summary>
|
||||
public override async Task OnConnectedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var companyId = Context.User?.FindFirst("CompanyId")?.Value;
|
||||
if (!string.IsNullOrEmpty(companyId))
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, $"shop-{companyId}");
|
||||
|
||||
await base.OnConnectedAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error in ShopHub.OnConnectedAsync for connection {ConnectionId}", Context.ConnectionId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the SignalR framework when a shop floor client disconnects.
|
||||
/// Unexpected disconnects (e.g., TV display losing network) are logged at Warning level so
|
||||
/// they can be correlated with shop floor connectivity issues without flooding the error log.
|
||||
/// </summary>
|
||||
public override async Task OnDisconnectedAsync(Exception? exception)
|
||||
{
|
||||
if (exception != null)
|
||||
_logger.LogWarning(exception, "ShopHub client disconnected with error: {ConnectionId}", Context.ConnectionId);
|
||||
|
||||
await base.OnDisconnectedAsync(exception);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user