Files
Nexus.Reader/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs
T
Antigravity fe5ff81c98 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>
2026-05-11 19:16:30 +00:00

119 lines
4.8 KiB
C#

using System.Security.Claims;
using System.Text.Json;
using Microsoft.AspNetCore.Components.Authorization;
using NexusReader.Application.Abstractions.Services;
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)
{
_storageService = storageService;
}
public void ClearCache()
{
_cachedState = null;
NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
}
private AuthenticationState? _cachedState;
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
try
{
if (_cachedState != null) return _cachedState;
var tokenResult = await _storageService.GetSecureString(TokenKey);
var token = tokenResult.IsSuccess ? tokenResult.Value : null;
// 1. Try Token-based auth
if (!string.IsNullOrWhiteSpace(token))
{
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",
rolesResult.IsSuccess ? rolesResult.Value! : "");
return _cachedState;
}
}
// 2. Try Cookie-based auth indicators
var storedEmailResult = await _storageService.GetSecureString(StorageKeys.UserEmail);
if (storedEmailResult.IsSuccess && !string.IsNullOrEmpty(storedEmailResult.Value))
{
var tenantIdResult = await _storageService.GetSecureString(StorageKeys.UserTenant);
var rolesResult = await _storageService.GetSecureString(StorageKeys.UserRoles);
_cachedState = CreateState(
storedEmailResult.Value,
tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown",
"CookieAuth",
rolesResult.IsSuccess ? rolesResult.Value! : "");
return _cachedState;
}
// 3. Fallback: If we have no local info, we might still have a cookie (e.g. after refresh or Google login).
// We should return anonymous for now but trigger a background check if we're in WASM.
// Wait! In WASM, the first GetAuthenticationStateAsync is awaited.
// We can do a quick check here if it's the first time.
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
catch (Exception)
{
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
}
}
private AuthenticationState CreateState(string email, string tenantId, string authType, string rolesStr = "")
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, email),
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, string rolesStr = "")
{
_cachedState = CreateState(email, tenantId, "OpaqueBearer", rolesStr);
NotifyAuthenticationStateChanged(Task.FromResult(_cachedState));
}
public void NotifyUserLogout()
{
_cachedState = null;
var guest = new ClaimsPrincipal(new ClaimsIdentity());
NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(guest)));
}
}