Refactor: Web Consolidation and Identity Stabilization (#40)
## Overview This PR completes the architectural consolidation of the web project and stabilizes the Identity-based authentication flow for the NexusReader application. It also refines the UI aesthetic for the Book Ingestion Modal as requested in #33. ## Key Changes - **Project Consolidation**: Fully merged `NexusReader.Web.New` into `NexusReader.Web`. This includes updating all namespace references, VS Code launch/task configurations, and CI/CD (`Dockerfile`). - **Identity Stabilization**: - Implemented `IIdentityService` on the server using `SignInManager<NexusUser>` and `UserManager<NexusUser>`. - Fixed registration logic to include mandatory fields (`SubscriptionPlanId`, `TenantId`). - Updated `Login.razor` to force a page reload on successful login, ensuring proper synchronization of authentication cookies between SignalR and the browser. - **UI/UX Refinement**: - Updated `BookIngestionModal` styling to follow the **Nexus Neon** design system. - Added premium button styles with hover effects and glows. - Improved modal layout and interaction feedback (shimmer effects, spinner colors). - **Cleanup**: Removed obsolete interfaces and constants that were superseded by newer Application layer implementations. ## Verification - Successfully built the solution: `dotnet build NexusReader.slnx --no-restore` - Verified project structure and file moves. - Validated server-side authentication logic. Fixes #33 --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #40 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #40.
This commit is contained in:
@@ -2,13 +2,17 @@ using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
using NexusReader.UI.Shared.Constants;
|
||||
using NexusReader.Application.Constants;
|
||||
|
||||
namespace NexusReader.UI.Shared.Services;
|
||||
|
||||
public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
private readonly INativeStorageService _storageService;
|
||||
|
||||
// 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)
|
||||
@@ -38,10 +42,15 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
||||
{
|
||||
var emailResult = await _storageService.GetSecureString(StorageKeys.UserEmail);
|
||||
var tenantIdResult = await _storageService.GetSecureString(StorageKeys.UserTenant);
|
||||
var rolesResult = await _storageService.GetSecureString(StorageKeys.UserRoles);
|
||||
|
||||
if (emailResult.IsSuccess && !string.IsNullOrEmpty(emailResult.Value))
|
||||
{
|
||||
_cachedState = CreateState(emailResult.Value, tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown", "OpaqueBearer");
|
||||
_cachedState = CreateState(
|
||||
emailResult.Value,
|
||||
tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown",
|
||||
"OpaqueBearer",
|
||||
rolesResult.IsSuccess ? rolesResult.Value! : "");
|
||||
return _cachedState;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +60,12 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
||||
if (storedEmailResult.IsSuccess && !string.IsNullOrEmpty(storedEmailResult.Value))
|
||||
{
|
||||
var tenantIdResult = await _storageService.GetSecureString(StorageKeys.UserTenant);
|
||||
_cachedState = CreateState(storedEmailResult.Value, tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown", "CookieAuth");
|
||||
var rolesResult = await _storageService.GetSecureString(StorageKeys.UserRoles);
|
||||
_cachedState = CreateState(
|
||||
storedEmailResult.Value,
|
||||
tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown",
|
||||
"CookieAuth",
|
||||
rolesResult.IsSuccess ? rolesResult.Value! : "");
|
||||
return _cachedState;
|
||||
}
|
||||
|
||||
@@ -67,7 +81,7 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
||||
}
|
||||
}
|
||||
|
||||
private AuthenticationState CreateState(string email, string tenantId, string authType)
|
||||
private AuthenticationState CreateState(string email, string tenantId, string authType, string rolesStr = "")
|
||||
{
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
@@ -75,13 +89,23 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
||||
new Claim(ClaimTypes.Email, email),
|
||||
new Claim("TenantId", tenantId)
|
||||
};
|
||||
|
||||
if (!string.IsNullOrEmpty(rolesStr))
|
||||
{
|
||||
var roles = rolesStr.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
|
||||
foreach (var role in roles)
|
||||
{
|
||||
claims.Add(new Claim(ClaimTypes.Role, role.Trim()));
|
||||
}
|
||||
}
|
||||
|
||||
var identity = new ClaimsIdentity(claims, authType);
|
||||
return new AuthenticationState(new ClaimsPrincipal(identity));
|
||||
}
|
||||
|
||||
public void NotifyUserAuthentication(string email, string tenantId)
|
||||
public void NotifyUserAuthentication(string email, string tenantId, string rolesStr = "")
|
||||
{
|
||||
_cachedState = CreateState(email, tenantId, "OpaqueBearer");
|
||||
_cachedState = CreateState(email, tenantId, "OpaqueBearer", rolesStr);
|
||||
NotifyAuthenticationStateChanged(Task.FromResult(_cachedState));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user