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:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user