SignalR FIX
All checks were successful
Build Docker Images / test (map[name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m26s
Build Docker Images / test (map[name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m24s
Build Docker Images / build-and-push (map[image_suffix:morska name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m41s
Build Docker Images / build-and-push (map[image_suffix:pedrollopl name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m38s
All checks were successful
Build Docker Images / test (map[name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m26s
Build Docker Images / test (map[name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m24s
Build Docker Images / build-and-push (map[image_suffix:morska name:Morska plugin_project:DiunaBI.Plugins.Morska]) (push) Successful in 1m41s
Build Docker Images / build-and-push (map[image_suffix:pedrollopl name:PedrolloPL plugin_project:DiunaBI.Plugins.PedrolloPL]) (push) Successful in 1m38s
This commit is contained in:
@@ -1,10 +1,34 @@
|
|||||||
# DiunaBI Project Context
|
# DiunaBI Project Context
|
||||||
|
|
||||||
> This file is auto-generated for Claude Code to quickly understand the project structure.
|
> This file is auto-generated for Claude Code to quickly understand the project structure.
|
||||||
> Last updated: 2025-12-05
|
> Last updated: 2025-12-06
|
||||||
|
|
||||||
## RECENT CHANGES (This Session)
|
## RECENT CHANGES (This Session)
|
||||||
|
|
||||||
|
**SignalR Authentication Token Flow Fix (Dec 6, 2025):**
|
||||||
|
- ✅ **TokenProvider Population** - Fixed `TokenProvider.Token` never being set with JWT, causing 401 Unauthorized on SignalR connections
|
||||||
|
- ✅ **AuthService Token Management** - Injected `TokenProvider` into `AuthService` and set token in 3 key places:
|
||||||
|
- `ValidateWithBackendAsync()` - on fresh Google login
|
||||||
|
- `CheckAuthenticationAsync()` - on session restore from localStorage
|
||||||
|
- `ClearAuthenticationAsync()` - clear token on logout
|
||||||
|
- ✅ **SignalR Initialization Timing** - Moved SignalR initialization from `MainLayout.OnInitializedAsync` to after authentication completes
|
||||||
|
- ✅ **Event-Driven Architecture** - `MainLayout` now subscribes to `AuthenticationStateChanged` event to initialize SignalR when user authenticates
|
||||||
|
- ✅ **Session Restore Support** - `CheckAuthenticationAsync()` now fires `AuthenticationStateChanged` event to initialize SignalR on page refresh
|
||||||
|
- Root cause: SignalR was initialized before authentication, so JWT token was empty during connection setup
|
||||||
|
- Solution: Initialize SignalR only after token is available via event subscription
|
||||||
|
- Files modified: [AuthService.cs](DiunaBI.UI.Shared/Services/AuthService.cs), [MainLayout.razor](DiunaBI.UI.Shared/Components/Layout/MainLayout.razor)
|
||||||
|
- Status: SignalR authentication working for both fresh login and restored sessions
|
||||||
|
|
||||||
|
**SignalR Authentication DI Fix (Dec 6, 2025):**
|
||||||
|
- ✅ **TokenProvider Registration** - Added missing `TokenProvider` service registration in DI container
|
||||||
|
- ✅ **EntityChangeHubService Scope Fix** - Changed from singleton to scoped to support user-specific JWT tokens
|
||||||
|
- ✅ **Bug Fix** - Resolved `InvalidOperationException` preventing app from starting after SignalR authentication was added
|
||||||
|
- Root cause: Singleton service (`EntityChangeHubService`) cannot depend on scoped service (`TokenProvider`) in DI
|
||||||
|
- Solution: Made `EntityChangeHubService` scoped so each user session has its own authenticated SignalR connection
|
||||||
|
- Files modified: [ServiceCollectionExtensions.cs](DiunaBI.UI.Shared/Extensions/ServiceCollectionExtensions.cs)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
**Security Audit & Hardening (Dec 5, 2025):**
|
**Security Audit & Hardening (Dec 5, 2025):**
|
||||||
- ✅ **JWT Token Validation** - Enabled issuer/audience validation in [Program.cs](DiunaBI.API/Program.cs), fixed config key mismatch in [JwtTokenService.cs](DiunaBI.API/Services/JwtTokenService.cs)
|
- ✅ **JWT Token Validation** - Enabled issuer/audience validation in [Program.cs](DiunaBI.API/Program.cs), fixed config key mismatch in [JwtTokenService.cs](DiunaBI.API/Services/JwtTokenService.cs)
|
||||||
- ✅ **API Key Security** - Created [ApiKeyAuthAttribute.cs](DiunaBI.API/Attributes/ApiKeyAuthAttribute.cs) with X-API-Key header auth, constant-time comparison
|
- ✅ **API Key Security** - Created [ApiKeyAuthAttribute.cs](DiunaBI.API/Attributes/ApiKeyAuthAttribute.cs) with X-API-Key header auth, constant-time comparison
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
@using DiunaBI.UI.Shared.Services
|
@using DiunaBI.UI.Shared.Services
|
||||||
@inject AppConfig AppConfig
|
@inject AppConfig AppConfig
|
||||||
@inject EntityChangeHubService HubService
|
@inject EntityChangeHubService HubService
|
||||||
|
@inject AuthService AuthService
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
<AuthGuard>
|
<AuthGuard>
|
||||||
<MudThemeProvider Theme="_theme"/>
|
<MudThemeProvider Theme="_theme"/>
|
||||||
@@ -55,11 +57,31 @@
|
|||||||
private bool _drawerOpen = true;
|
private bool _drawerOpen = true;
|
||||||
private DrawerVariant _drawerVariant = DrawerVariant.Persistent;
|
private DrawerVariant _drawerVariant = DrawerVariant.Persistent;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
// Initialize SignalR connection when layout loads
|
// Subscribe to authentication state changes
|
||||||
|
AuthService.AuthenticationStateChanged += OnAuthenticationStateChanged;
|
||||||
|
|
||||||
|
// If already authenticated (e.g., from restored session), initialize SignalR
|
||||||
|
if (AuthService.IsAuthenticated)
|
||||||
|
{
|
||||||
|
_ = HubService.InitializeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnAuthenticationStateChanged(bool isAuthenticated)
|
||||||
|
{
|
||||||
|
if (isAuthenticated)
|
||||||
|
{
|
||||||
|
Console.WriteLine("🔐 MainLayout: User authenticated, initializing SignalR...");
|
||||||
await HubService.InitializeAsync();
|
await HubService.InitializeAsync();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
AuthService.AuthenticationStateChanged -= OnAuthenticationStateChanged;
|
||||||
|
}
|
||||||
|
|
||||||
private MudTheme _theme = new MudTheme()
|
private MudTheme _theme = new MudTheme()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,16 +15,18 @@ public class AuthService
|
|||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
private readonly IJSRuntime _jsRuntime;
|
private readonly IJSRuntime _jsRuntime;
|
||||||
|
private readonly TokenProvider _tokenProvider;
|
||||||
private bool? _isAuthenticated;
|
private bool? _isAuthenticated;
|
||||||
private UserInfo? _userInfo = null;
|
private UserInfo? _userInfo = null;
|
||||||
private string? _apiToken;
|
private string? _apiToken;
|
||||||
|
|
||||||
public event Action<bool>? AuthenticationStateChanged;
|
public event Action<bool>? AuthenticationStateChanged;
|
||||||
|
|
||||||
public AuthService(HttpClient httpClient, IJSRuntime jsRuntime)
|
public AuthService(HttpClient httpClient, IJSRuntime jsRuntime, TokenProvider tokenProvider)
|
||||||
{
|
{
|
||||||
_httpClient = httpClient;
|
_httpClient = httpClient;
|
||||||
_jsRuntime = jsRuntime;
|
_jsRuntime = jsRuntime;
|
||||||
|
_tokenProvider = tokenProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsAuthenticated => _isAuthenticated ?? false;
|
public bool IsAuthenticated => _isAuthenticated ?? false;
|
||||||
@@ -44,6 +46,7 @@ public class AuthService
|
|||||||
if (result != null)
|
if (result != null)
|
||||||
{
|
{
|
||||||
_apiToken = result.Token;
|
_apiToken = result.Token;
|
||||||
|
_tokenProvider.Token = result.Token; // Set token for SignalR
|
||||||
_userInfo = new UserInfo
|
_userInfo = new UserInfo
|
||||||
{
|
{
|
||||||
Id = result.Id,
|
Id = result.Id,
|
||||||
@@ -104,6 +107,7 @@ public class AuthService
|
|||||||
if (_isAuthenticated.Value && !string.IsNullOrEmpty(userInfoJson))
|
if (_isAuthenticated.Value && !string.IsNullOrEmpty(userInfoJson))
|
||||||
{
|
{
|
||||||
_apiToken = token;
|
_apiToken = token;
|
||||||
|
_tokenProvider.Token = token; // Set token for SignalR
|
||||||
_userInfo = JsonSerializer.Deserialize<UserInfo>(userInfoJson);
|
_userInfo = JsonSerializer.Deserialize<UserInfo>(userInfoJson);
|
||||||
|
|
||||||
// Restore header
|
// Restore header
|
||||||
@@ -111,6 +115,9 @@ public class AuthService
|
|||||||
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken);
|
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiToken);
|
||||||
|
|
||||||
Console.WriteLine($"✅ Session restored: {_userInfo?.Email}");
|
Console.WriteLine($"✅ Session restored: {_userInfo?.Email}");
|
||||||
|
|
||||||
|
// Notify that authentication state changed (for SignalR initialization)
|
||||||
|
AuthenticationStateChanged?.Invoke(true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -139,6 +146,7 @@ public class AuthService
|
|||||||
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "user_info");
|
await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", "user_info");
|
||||||
|
|
||||||
_apiToken = null;
|
_apiToken = null;
|
||||||
|
_tokenProvider.Token = null; // Clear token for SignalR
|
||||||
_isAuthenticated = false;
|
_isAuthenticated = false;
|
||||||
_userInfo = null;
|
_userInfo = null;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user