feat(ui): Hub Navigation, Profile Dashboard and Auth Stability Fixes (#31)

This PR implements the Hub Navigation system and the Profile Dashboard, while resolving critical session synchronization issues.

### Key Changes
- **Hub Navigation**: Introduced `MainHubLayout` with a premium glassmorphism sidebar, providing access to Dashboard, Library, Concepts Map, and Profile.
- **Profile Dashboard**: Implemented a high-fidelity Profile page (#27) with learning metrics, AI token usage tracking, and system rank visualization.
- **Stability Fixes**:
    - Resolved an infinite network loop on the `/profile` page by implementing request deduplication and in-memory caching in `IdentityService`.
    - Added environment-aware guards to prevent illegal JavaScript interop calls during server-side prerendering.
    - Implemented automatic session invalidation on `401 Unauthorized` responses to handle stale authentication states gracefully.
- **Reader Integration**: Added a "Return to Dashboard" option in the reader toolbar (#26).

Closes #26
Closes #27

Reviewed-on: #31
Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Co-committed-by: Marek Jasiński <jasins.marek@gmail.com>
This commit was merged in pull request #31.
This commit is contained in:
2026-05-10 17:36:35 +00:00
committed by Marek Jaisński
parent 34794db209
commit 2e23a032d3
56 changed files with 4292 additions and 481 deletions
@@ -0,0 +1,68 @@
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.UI.Shared.Services;
namespace NexusReader.Web.New.Services;
public class ServerIdentityService : IIdentityService
{
private readonly UserManager<NexusUser> _userManager;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IMediator _mediator;
public event Func<Task>? OnStateInvalidated;
public ServerIdentityService(
UserManager<NexusUser> userManager,
IHttpContextAccessor httpContextAccessor,
IMediator mediator)
{
_userManager = userManager;
_httpContextAccessor = httpContextAccessor;
_mediator = mediator;
}
public Task<Result> LoginAsync(string email, string password, bool rememberMe = false)
=> throw new NotSupportedException("Use standard Identity endpoints for login on server.");
public Task<Result> LogoutAsync()
=> throw new NotSupportedException("Use standard Identity endpoints for logout on server.");
public Task<Result> RegisterAsync(string email, string password)
=> throw new NotSupportedException("Use standard Identity endpoints for registration on server.");
public Task<Result> RefreshTokenAsync() => Task.FromResult(Result.Ok());
public async Task<Result<UserProfile>> 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);
var dto = result.Value;
// Map DTO to UI record
var profile = new UserProfile(
dto.Email,
dto.AITokensUsed,
dto.TenantId,
dto.Plan,
dto.AverageQuizScore,
dto.LastReadBook
);
return Result.Ok(profile);
}
}