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:
2026-05-11 19:16:30 +00:00
committed by Marek Jaisński
parent f433e3c74a
commit fe5ff81c98
61 changed files with 1092 additions and 312 deletions
@@ -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));
}