diff --git a/src/NexusReader.UI.Shared/Components/AuthenticationStatePersister.razor b/src/NexusReader.UI.Shared/Components/AuthenticationStatePersister.razor
new file mode 100644
index 0000000..22f87fb
--- /dev/null
+++ b/src/NexusReader.UI.Shared/Components/AuthenticationStatePersister.razor
@@ -0,0 +1,41 @@
+@using Microsoft.AspNetCore.Components.Authorization
+@inject PersistentComponentState ApplicationState
+@inject AuthenticationStateProvider AuthenticationStateProvider
+@implements IDisposable
+
+@code {
+ private PersistingComponentStateSubscription _subscription;
+
+ protected override void OnInitialized()
+ {
+ _subscription = ApplicationState.RegisterOnPersisting(PersistAuthenticationStateAsync);
+ }
+
+ private async Task PersistAuthenticationStateAsync()
+ {
+ var authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
+ var principal = authenticationState.User;
+
+ if (principal.Identity?.IsAuthenticated == true)
+ {
+ var email = principal.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? principal.Identity.Name;
+ var tenantId = principal.FindFirst("TenantId")?.Value ?? "global";
+ var roles = string.Join(",", principal.FindAll(System.Security.Claims.ClaimTypes.Role).Select(c => c.Value));
+
+ if (email != null)
+ {
+ ApplicationState.PersistAsJson("UserInfo", new UserInfo
+ {
+ Email = email,
+ TenantId = tenantId,
+ Roles = roles
+ });
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ _subscription.Dispose();
+ }
+}
diff --git a/src/NexusReader.UI.Shared/Pages/Account/Login.razor b/src/NexusReader.UI.Shared/Pages/Account/Login.razor
index 9484478..703382a 100644
--- a/src/NexusReader.UI.Shared/Pages/Account/Login.razor
+++ b/src/NexusReader.UI.Shared/Pages/Account/Login.razor
@@ -102,13 +102,21 @@
+
@code {
+ [CascadingParameter]
+ private Task? AuthStateTask { get; set; }
+
[Parameter]
[SupplyParameterFromQuery(Name = "error")]
public string? ErrorCode { get; set; }
+ [Parameter]
+ [SupplyParameterFromQuery(Name = "returnUrl")]
+ public string? ReturnUrl { get; set; }
+
private LoginModel _loginModel = new();
private string? _errorMessage;
private bool _isSubmitting;
@@ -116,7 +124,7 @@
private bool _allowRegistration;
private bool _allowPasswordReset;
- protected override void OnInitialized()
+ protected override async Task OnInitializedAsync()
{
_allowRegistration = Configuration.GetValue("Features:AllowRegistration") ?? true;
_allowPasswordReset = Configuration.GetValue("Features:AllowPasswordReset") ?? true;
@@ -134,6 +142,15 @@
_ => "Wystąpił nieoczekiwany błąd podczas logowania."
};
}
+
+ if (AuthStateTask != null)
+ {
+ var authState = await AuthStateTask;
+ if (authState.User.Identity?.IsAuthenticated == true)
+ {
+ NavigationManager.NavigateTo(string.IsNullOrEmpty(ReturnUrl) ? "/" : ReturnUrl);
+ }
+ }
}
private async Task HandleLogin()
diff --git a/src/NexusReader.UI.Shared/Routes.razor b/src/NexusReader.UI.Shared/Routes.razor
index 644ac84..1459eed 100644
--- a/src/NexusReader.UI.Shared/Routes.razor
+++ b/src/NexusReader.UI.Shared/Routes.razor
@@ -1,3 +1,5 @@
+
+
diff --git a/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs b/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs
index 7835422..c5f4f94 100644
--- a/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs
+++ b/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs
@@ -4,20 +4,24 @@ using Microsoft.AspNetCore.Components.Authorization;
using NexusReader.Application.Abstractions.Services;
using NexusReader.Application.Constants;
+using Microsoft.AspNetCore.Components;
+
namespace NexusReader.UI.Shared.Services;
public class NexusAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly INativeStorageService _storageService;
+ private readonly PersistentComponentState _persistentState;
// SECURITY NOTE: We currently store roles in local storage to persist state across refreshes.
// In a production SaaS environment, consider using ProtectedBrowserStorage (Blazor Server)
// or encrypted storage/JWT claims validation to prevent client-side role tampering.
private const string TokenKey = StorageKeys.AuthToken;
- public NexusAuthenticationStateProvider(INativeStorageService storageService)
+ public NexusAuthenticationStateProvider(INativeStorageService storageService, PersistentComponentState persistentState)
{
_storageService = storageService;
+ _persistentState = persistentState;
}
public void ClearCache()
@@ -34,6 +38,18 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
{
if (_cachedState != null) return _cachedState;
+ // 0. Hydrate state from SSR if available in PersistentComponentState
+ if (_persistentState.TryTakeFromJson("UserInfo", out var userInfo) && userInfo != null)
+ {
+ // Save to local storage for subsequent client-only transitions/refreshes
+ await _storageService.SaveSecureString(StorageKeys.UserEmail, userInfo.Email);
+ await _storageService.SaveSecureString(StorageKeys.UserTenant, userInfo.TenantId);
+ await _storageService.SaveSecureString(StorageKeys.UserRoles, userInfo.Roles);
+
+ _cachedState = CreateState(userInfo.Email, userInfo.TenantId, "FederatedHydration", userInfo.Roles);
+ return _cachedState;
+ }
+
var tokenResult = await _storageService.GetSecureString(TokenKey);
var token = tokenResult.IsSuccess ? tokenResult.Value : null;
@@ -116,3 +132,10 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(guest)));
}
}
+
+public class UserInfo
+{
+ public string Email { get; set; } = string.Empty;
+ public string TenantId { get; set; } = string.Empty;
+ public string Roles { get; set; } = string.Empty;
+}