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
@@ -1,9 +1,10 @@
using NexusReader.Domain.Entities;
using FluentResults;
namespace NexusReader.Application.Abstractions.Services;
public interface IBillingService
{
Task<bool> HandleSubscriptionUpdatedAsync(string customerEmail, string stripeProductId);
Task<bool> HandleSubscriptionDeletedAsync(string customerEmail);
Task<Result> HandleSubscriptionUpdatedAsync(string customerEmail, string stripeProductId);
Task<Result> HandleSubscriptionDeletedAsync(string customerEmail);
}
@@ -0,0 +1,10 @@
using FluentResults;
using NexusReader.Application.Queries.Reader;
using System.IO;
namespace NexusReader.Application.Abstractions.Services;
public interface IEpubMetadataExtractor
{
Task<Result<LocalEpubMetadata>> ExtractMetadataAsync(Stream epubStream);
}
@@ -3,7 +3,7 @@ using NexusReader.Application.Queries.Reader;
namespace NexusReader.Application.Abstractions.Services;
public interface IEpubService
public interface IEpubReader
{
Task<Result<ReaderPageViewModel>> GetEpubContentAsync(int chapterIndex, string? userId = null);
}
@@ -0,0 +1,14 @@
using FluentResults;
using NexusReader.Application.DTOs.User;
namespace NexusReader.Application.Abstractions.Services;
public interface IIdentityService
{
event Func<Task>? OnStateInvalidated;
Task<Result> RegisterAsync(string email, string password);
Task<Result> LoginAsync(string email, string password, bool rememberMe = false);
Task<Result> LogoutAsync();
Task<Result<UserProfileDto>> GetProfileAsync();
Task<Result> RefreshTokenAsync();
}
@@ -4,13 +4,13 @@ namespace NexusReader.Application.Abstractions.Services;
public interface INativeStorageService
{
Result SaveString(string key, string value);
Result<string?> GetString(string key);
Result SaveBool(string key, bool value);
Result<bool> GetBool(string key, bool defaultValue = false);
Result Remove(string key);
Task<Result> SaveStringAsync(string key, string value);
Task<Result<string?>> GetStringAsync(string key);
Task<Result> SaveBoolAsync(string key, bool value);
Task<Result<bool>> GetBoolAsync(string key, bool defaultValue = false);
Task<Result> RemoveAsync(string key);
Task<Result> SaveSecureString(string key, string value);
Task<Result<string?>> GetSecureString(string key);
Result RemoveSecure(string key);
Task<Result> RemoveSecureAsync(string key);
}
@@ -0,0 +1,8 @@
namespace NexusReader.Application.Constants;
public static class PlanConstants
{
public const string DefaultPlanName = "Free";
public const int DefaultTokenLimit = 1000;
public const string DefaultActivityLabel = "Brak aktywności";
}
@@ -0,0 +1,10 @@
namespace NexusReader.Application.Constants;
public static class StorageKeys
{
public const string AuthToken = "nexus_auth_token";
public const string RefreshToken = "nexus_refresh_token";
public const string UserEmail = "nexus_user_email";
public const string UserTenant = "nexus_user_tenant";
public const string UserRoles = "nexus_user_roles";
}
@@ -1,3 +1,5 @@
using NexusReader.Application.Constants;
namespace NexusReader.Application.DTOs.User;
public record UserProfileDto
@@ -17,6 +19,13 @@ public record UserProfileDto
/// Summary of the last read book.
/// </summary>
public LastReadBookDto? LastReadBook { get; init; }
public string[] Roles { get; init; } = Array.Empty<string>();
// Helper properties for UI compatibility
public string CurrentPlan => Plan?.Name ?? PlanConstants.DefaultPlanName;
public int AITokenLimit => Plan?.AITokenLimit ?? PlanConstants.DefaultTokenLimit;
public string LastReadBookTitle => LastReadBook?.Title ?? PlanConstants.DefaultActivityLabel;
}
public record LastReadBookDto
@@ -1,7 +1,8 @@
using Mapster;
using MapsterMapper;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;
using NexusReader.Domain.Entities;
using NexusReader.Application.DTOs.User;
namespace NexusReader.Application.Mappings;
@@ -11,8 +12,8 @@ public static class MappingConfig
{
var config = TypeAdapterConfig.GlobalSettings;
// Manual registration for AOT (or use Source Generator)
// config.NewConfig<Source, Destination>();
config.NewConfig<NexusUser, UserProfileDto>();
// Roles are mapped manually in queries due to Identity structure
services.AddSingleton(config);
services.AddScoped<IMapper, ServiceMapper>();
@@ -6,9 +6,9 @@ namespace NexusReader.Application.Queries.Reader;
internal sealed class GetReaderPageQueryHandler : IQueryHandler<GetReaderPageQuery, ReaderPageViewModel>
{
private readonly IEpubService _epubService;
private readonly IEpubReader _epubService;
public GetReaderPageQueryHandler(IEpubService epubService)
public GetReaderPageQueryHandler(IEpubReader epubService)
{
_epubService = epubService;
}
@@ -0,0 +1,7 @@
namespace NexusReader.Application.Queries.Reader;
public record LocalEpubMetadata(
string Title,
string Author,
byte[]? CoverImage = null
);
@@ -48,7 +48,11 @@ public class GetUserProfileQueryHandler : IRequestHandler<GetUserProfileQuery, R
Progress = e.Progress,
LastChapter = e.LastChapter ?? "Rozpoczynanie...",
LastChapterIndex = e.LastChapterIndex
}).FirstOrDefault()
}).FirstOrDefault(),
Roles = dbContext.UserRoles
.Where(ur => ur.UserId == u.Id)
.Join(dbContext.Roles, ur => ur.RoleId, r => r.Id, (ur, r) => r.Name!)
.ToArray()
})
.FirstOrDefaultAsync(cancellationToken);