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="email" value="@_loginModel.Email" />
|
||||||
<input type="hidden" name="password" value="@_loginModel.Password" />
|
<input type="hidden" name="password" value="@_loginModel.Password" />
|
||||||
<input type="hidden" name="rememberMe" value="@(_loginModel.RememberMe ? "true" : "false")" />
|
<input type="hidden" name="rememberMe" value="@(_loginModel.RememberMe ? "true" : "false")" />
|
||||||
|
<input type="hidden" name="returnUrl" value="@ReturnUrl" />
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
[CascadingParameter]
|
||||||
|
private Task<AuthenticationState>? AuthStateTask { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
[SupplyParameterFromQuery(Name = "error")]
|
[SupplyParameterFromQuery(Name = "error")]
|
||||||
public string? ErrorCode { get; set; }
|
public string? ErrorCode { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
[SupplyParameterFromQuery(Name = "returnUrl")]
|
||||||
|
public string? ReturnUrl { get; set; }
|
||||||
|
|
||||||
private LoginModel _loginModel = new();
|
private LoginModel _loginModel = new();
|
||||||
private string? _errorMessage;
|
private string? _errorMessage;
|
||||||
private bool _isSubmitting;
|
private bool _isSubmitting;
|
||||||
@@ -116,7 +124,7 @@
|
|||||||
private bool _allowRegistration;
|
private bool _allowRegistration;
|
||||||
private bool _allowPasswordReset;
|
private bool _allowPasswordReset;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
_allowRegistration = Configuration.GetValue<bool?>("Features:AllowRegistration") ?? true;
|
_allowRegistration = Configuration.GetValue<bool?>("Features:AllowRegistration") ?? true;
|
||||||
_allowPasswordReset = Configuration.GetValue<bool?>("Features:AllowPasswordReset") ?? true;
|
_allowPasswordReset = Configuration.GetValue<bool?>("Features:AllowPasswordReset") ?? true;
|
||||||
@@ -134,6 +142,15 @@
|
|||||||
_ => "Wystąpił nieoczekiwany błąd podczas logowania."
|
_ => "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()
|
private async Task HandleLogin()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
<AuthenticationStatePersister />
|
||||||
|
|
||||||
<ErrorBoundary @ref="_errorBoundary">
|
<ErrorBoundary @ref="_errorBoundary">
|
||||||
<ChildContent>
|
<ChildContent>
|
||||||
<Router AppAssembly="@typeof(Routes).Assembly">
|
<Router AppAssembly="@typeof(Routes).Assembly">
|
||||||
|
|||||||
@@ -4,20 +4,24 @@ using Microsoft.AspNetCore.Components.Authorization;
|
|||||||
using NexusReader.Application.Abstractions.Services;
|
using NexusReader.Application.Abstractions.Services;
|
||||||
using NexusReader.Application.Constants;
|
using NexusReader.Application.Constants;
|
||||||
|
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
namespace NexusReader.UI.Shared.Services;
|
namespace NexusReader.UI.Shared.Services;
|
||||||
|
|
||||||
public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
||||||
{
|
{
|
||||||
private readonly INativeStorageService _storageService;
|
private readonly INativeStorageService _storageService;
|
||||||
|
private readonly PersistentComponentState _persistentState;
|
||||||
|
|
||||||
// SECURITY NOTE: We currently store roles in local storage to persist state across refreshes.
|
// SECURITY NOTE: We currently store roles in local storage to persist state across refreshes.
|
||||||
// In a production SaaS environment, consider using ProtectedBrowserStorage (Blazor Server)
|
// In a production SaaS environment, consider using ProtectedBrowserStorage (Blazor Server)
|
||||||
// or encrypted storage/JWT claims validation to prevent client-side role tampering.
|
// or encrypted storage/JWT claims validation to prevent client-side role tampering.
|
||||||
private const string TokenKey = StorageKeys.AuthToken;
|
private const string TokenKey = StorageKeys.AuthToken;
|
||||||
|
|
||||||
public NexusAuthenticationStateProvider(INativeStorageService storageService)
|
public NexusAuthenticationStateProvider(INativeStorageService storageService, PersistentComponentState persistentState)
|
||||||
{
|
{
|
||||||
_storageService = storageService;
|
_storageService = storageService;
|
||||||
|
_persistentState = persistentState;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ClearCache()
|
public void ClearCache()
|
||||||
@@ -34,6 +38,18 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
{
|
{
|
||||||
if (_cachedState != null) return _cachedState;
|
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 tokenResult = await _storageService.GetSecureString(TokenKey);
|
||||||
var token = tokenResult.IsSuccess ? tokenResult.Value : null;
|
var token = tokenResult.IsSuccess ? tokenResult.Value : null;
|
||||||
|
|
||||||
@@ -116,3 +132,10 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(guest)));
|
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