feat: Mobile-First Layout Redesign & D3.js Graph Stabilization #58
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -102,13 +102,21 @@
|
||||
<input type="hidden" name="email" value="@_loginModel.Email" />
|
||||
<input type="hidden" name="password" value="@_loginModel.Password" />
|
||||
<input type="hidden" name="rememberMe" value="@(_loginModel.RememberMe ? "true" : "false")" />
|
||||
<input type="hidden" name="returnUrl" value="@ReturnUrl" />
|
||||
</form>
|
||||
|
||||
@code {
|
||||
[CascadingParameter]
|
||||
private Task<AuthenticationState>? 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<bool?>("Features:AllowRegistration") ?? true;
|
||||
_allowPasswordReset = Configuration.GetValue<bool?>("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()
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<AuthenticationStatePersister />
|
||||
|
||||
<ErrorBoundary @ref="_errorBoundary">
|
||||
<ChildContent>
|
||||
<Router AppAssembly="@typeof(Routes).Assembly">
|
||||
|
||||
@@ -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>("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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user