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
@@ -0,0 +1,121 @@
using System.Security.Claims;
using FluentResults;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using NexusReader.Application.DTOs.User;
using NexusReader.Data.Persistence;
using NexusReader.Domain.Entities;
using NexusReader.Application.Queries.User;
using MediatR;
using NexusReader.Application.Constants;
using NexusReader.Application.Abstractions.Services;
namespace NexusReader.Web.Services;
public class ServerIdentityService : IIdentityService
{
private readonly UserManager<NexusUser> _userManager;
private readonly SignInManager<NexusUser> _signInManager;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMediator _mediator;
private readonly INativeStorageService _storageService;
public event Func<Task>? OnStateInvalidated;
public ServerIdentityService(
UserManager<NexusUser> userManager,
SignInManager<NexusUser> signInManager,
IHttpContextAccessor httpContextAccessor,
IMediator mediator,
INativeStorageService storageService)
{
_userManager = userManager;
_signInManager = signInManager;
_httpContextAccessor = httpContextAccessor;
_mediator = mediator;
_storageService = storageService;
}
public async Task<Result> LoginAsync(string email, string password, bool rememberMe = false)
{
try
{
var user = await _userManager.FindByEmailAsync(email);
if (user == null) return Result.Fail("Nieprawidłowy e-mail lub hasło.");
// Check if account is locked
if (await _userManager.IsLockedOutAsync(user)) return Result.Fail("Konto zostało zablokowane.");
// Check password
var isCorrect = await _userManager.CheckPasswordAsync(user, password);
if (!isCorrect)
{
await _userManager.AccessFailedAsync(user);
return Result.Fail("Nieprawidłowy e-mail lub hasło.");
}
// Reset access failed count on success
await _userManager.ResetAccessFailedCountAsync(user);
// In Blazor Interactive Server, we cannot use PasswordSignInAsync directly
// because headers are read-only once the circuit is established.
// We return success here to indicate credentials are valid.
// The UI will then perform a POST redirect to /account/login-form to set cookies.
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(new Error($"Błąd podczas weryfikacji poświadczeń: {ex.Message}").CausedBy(ex));
}
}
public async Task<Result> LogoutAsync()
{
// Logout via SignalR is also problematic for cookie clearing.
// The UI should redirect to /account/logout-form
return Result.Ok();
}
public async Task<Result> RegisterAsync(string email, string password)
{
try
{
var user = new NexusUser
{
UserName = email,
Email = email,
SubscriptionPlanId = SubscriptionPlan.FreeId,
TenantId = "global"
};
var result = await _userManager.CreateAsync(user, password);
if (result.Succeeded)
{
// Similar to Login, we return success but don't sign in here.
return Result.Ok();
}
return Result.Fail(result.Errors.Select(e => e.Description).FirstOrDefault() ?? "Rejestracja nie powiodła się.");
}
catch (Exception ex)
{
return Result.Fail(new Error($"Błąd podczas rejestracji na serwerze: {ex.Message}").CausedBy(ex));
}
}
public Task<Result> RefreshTokenAsync() => Task.FromResult(Result.Ok());
public async Task<Result<UserProfileDto>> GetProfileAsync()
{
var user = _httpContextAccessor.HttpContext?.User;
if (user == null || !user.Identity?.IsAuthenticated == true) return Result.Fail("Not authenticated.");
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null) return Result.Fail("User ID not found.");
var result = await _mediator.Send(new GetUserProfileQuery(userId));
if (result.IsFailed) return Result.Fail(result.Errors);
return Result.Ok(result.Value);
}
}