using Microsoft.AspNetCore.SignalR.Client; using Blazored.LocalStorage; namespace FrontOffice.Main.Utilities; /// /// Service for managing SignalR connection and handling token-related notifications from BFF. /// This service connects to the BFF SignalR Hub and notifies the application when token refresh is needed. /// public class TokenNotificationService : IAsyncDisposable { private readonly ILocalStorageService _localStorage; private readonly IConfiguration _configuration; private readonly ILogger _logger; private HubConnection? _hubConnection; /// /// Event fired when a token revoked notification is received from server /// public event Func? OnTokenRevoked; /// /// Event fired when a force refresh token notification is received from server /// public event Func? OnForceRefreshToken; /// /// Event fired when a broadcast message is received from server /// public event Func? OnBroadcastMessage; /// /// Event fired when connection state changes /// public event Action? OnConnectionStateChanged; public TokenNotificationService( ILocalStorageService localStorage, IConfiguration configuration, ILogger logger) { _localStorage = localStorage; _configuration = configuration; _logger = logger; } /// /// Gets the current connection state /// public HubConnectionState ConnectionState => _hubConnection?.State ?? HubConnectionState.Disconnected; /// /// Connect to the SignalR Hub with the user's authentication token /// public async Task ConnectAsync() { if (_hubConnection?.State == HubConnectionState.Connected) { _logger.LogWarning("Already connected to SignalR Hub"); return; } try { var token = await _localStorage.GetItemAsync("authToken"); if (string.IsNullOrEmpty(token)) { _logger.LogWarning("No auth token found, cannot connect to SignalR Hub"); return; } var gwUrl = _configuration["GwUrl"]?.TrimEnd('/') ?? "https://localhost:5002"; var hubPath = _configuration["SignalR:HubPath"] ?? "/hubs/token-relay"; var hubUrl = $"{gwUrl}{hubPath}"; _logger.LogInformation("Connecting to SignalR Hub at {HubUrl}", hubUrl); _hubConnection = new HubConnectionBuilder() .WithUrl(hubUrl, options => { options.AccessTokenProvider = () => Task.FromResult(token); }) .WithAutomaticReconnect(new[] { TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30) }) .Build(); RegisterEventHandlers(); await _hubConnection.StartAsync(); _logger.LogInformation("Successfully connected to SignalR Hub"); OnConnectionStateChanged?.Invoke(HubConnectionState.Connected); } catch (Exception ex) { _logger.LogError(ex, "Failed to connect to SignalR Hub"); OnConnectionStateChanged?.Invoke(HubConnectionState.Disconnected); } } /// /// Disconnect from the SignalR Hub /// public async Task DisconnectAsync() { if (_hubConnection != null) { await _hubConnection.StopAsync(); _logger.LogInformation("Disconnected from SignalR Hub"); OnConnectionStateChanged?.Invoke(HubConnectionState.Disconnected); } } private void RegisterEventHandlers() { if (_hubConnection == null) return; // Handle TokenRevoked event _hubConnection.On("TokenRevoked", async args => { _logger.LogInformation("Received TokenRevoked notification. UserId: {UserId}, Reason: {Reason}", args.UserId, args.Reason); if (OnTokenRevoked != null) { await OnTokenRevoked.Invoke(args); } }); // Handle ForceRefreshToken event _hubConnection.On("ForceRefreshToken", async args => { _logger.LogInformation("Received ForceRefreshToken notification. UserId: {UserId}", args.UserId); if (OnForceRefreshToken != null) { await OnForceRefreshToken.Invoke(args); } }); // Handle BroadcastMessage event _hubConnection.On("BroadcastMessage", async args => { _logger.LogInformation("Received BroadcastMessage: {Message}", args.Message); if (OnBroadcastMessage != null) { await OnBroadcastMessage.Invoke(args); } }); // Handle connection state changes _hubConnection.Reconnecting += error => { _logger.LogWarning(error, "SignalR connection lost. Attempting to reconnect..."); OnConnectionStateChanged?.Invoke(HubConnectionState.Reconnecting); return Task.CompletedTask; }; _hubConnection.Reconnected += connectionId => { _logger.LogInformation("SignalR reconnected. ConnectionId: {ConnectionId}", connectionId); OnConnectionStateChanged?.Invoke(HubConnectionState.Connected); return Task.CompletedTask; }; _hubConnection.Closed += async error => { _logger.LogWarning(error, "SignalR connection closed"); OnConnectionStateChanged?.Invoke(HubConnectionState.Disconnected); await Task.CompletedTask; }; } public async ValueTask DisposeAsync() { if (_hubConnection != null) { await _hubConnection.DisposeAsync(); } } } /// /// Event arguments for token revoked notification /// public class TokenRevokedEventArgs { public long UserId { get; set; } public string Reason { get; set; } = string.Empty; public DateTime Timestamp { get; set; } } /// /// Event arguments for force refresh token notification /// public class ForceRefreshEventArgs { public long UserId { get; set; } public DateTime Timestamp { get; set; } } /// /// Event arguments for broadcast message notification /// public class BroadcastMessageEventArgs { public string Message { get; set; } = string.Empty; public DateTime Timestamp { get; set; } }