fix(di): resolve client-side WASM dependency injection validation errors on login
This commit is contained in:
+5
-5
@@ -15,7 +15,7 @@
|
||||
|
||||
@if (_isLoading)
|
||||
{
|
||||
<div class="loading-state" role="status" aria-label="Ładowanie rekomendacji">
|
||||
<div @key='"loading"' class="loading-state" role="status" aria-label="Ładowanie rekomendacji">
|
||||
<div class="spinner-ring">
|
||||
<div class="spinner-track"></div>
|
||||
<div class="spinner-head"></div>
|
||||
@@ -25,24 +25,24 @@
|
||||
}
|
||||
else if (_hasError)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<div @key='"error"' class="empty-state">
|
||||
<NexusIcon Name="alert-circle" Size="32" />
|
||||
<p>Nie udało się załadować rekomendacji.</p>
|
||||
</div>
|
||||
}
|
||||
else if (_recommendations is null || _recommendations.Count == 0)
|
||||
{
|
||||
<div class="empty-state">
|
||||
<div @key='"empty"' class="empty-state">
|
||||
<NexusIcon Name="book-open" Size="32" />
|
||||
<p>Zacznij czytać, aby odkryć powiązane tytuły.</p>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="recommendations-list" role="list">
|
||||
<ul @key='"list"' class="recommendations-list" role="list">
|
||||
@foreach (var rec in _recommendations)
|
||||
{
|
||||
<li class="recommendation-item @(rec.IsPremiumUpsell ? "premium" : "owned")"
|
||||
<li @key="rec.TargetBookId" class="recommendation-item @(rec.IsPremiumUpsell ? "premium" : "owned")"
|
||||
role="listitem">
|
||||
<div class="rec-content">
|
||||
<div class="rec-meta">
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<section class="current-reading-card glass-panel">
|
||||
@if (Book != null)
|
||||
{
|
||||
<div class="card-layout">
|
||||
<div @key='"current-reading-book"' class="card-layout">
|
||||
<div class="book-cover">
|
||||
<img src="@(Book.CoverUrl ?? "https://via.placeholder.com/120x180?text=No+Cover")" alt="@Book.Title" />
|
||||
</div>
|
||||
@@ -51,7 +51,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="empty-state">
|
||||
<div @key='"current-reading-empty"' class="empty-state">
|
||||
<div class="empty-icon">
|
||||
<NexusIcon Name="book-open" Size="48" />
|
||||
</div>
|
||||
|
||||
@@ -6,13 +6,13 @@
|
||||
|
||||
@if (!_isFullyLoaded)
|
||||
{
|
||||
<div class="app-preloader" style="backdrop-filter: blur(15px); background: rgba(18, 18, 18, 0.95); z-index: 100000; color: #ffffff;">
|
||||
<div @key='"preloader"' class="app-preloader" style="backdrop-filter: blur(15px); background: rgba(18, 18, 18, 0.95); z-index: 100000; color: #ffffff;">
|
||||
<div class="preloader-spinner"></div>
|
||||
<div class="preloader-text" style="color: #ffffff;">Synchronizing Secure Session...</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="hub-container @(_isMobileMenuOpen ? "mobile-menu-open" : "")">
|
||||
<div @key='"hub-container"' class="hub-container @(_isMobileMenuOpen ? "mobile-menu-open" : "")">
|
||||
<AuthorizeView>
|
||||
<Authorized>
|
||||
<!-- Mobile Sticky Top-bar -->
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
<span>lub</span>
|
||||
</div>
|
||||
|
||||
<EditForm Model="@_loginModel" OnValidSubmit="HandleLogin" class="auth-form">
|
||||
<EditForm FormName="login-form" Model="@_loginModel" OnValidSubmit="HandleLogin" class="auth-form">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="field-group">
|
||||
@@ -98,7 +98,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="nexusLoginForm" method="post" action="/account/login-form" style="display:none">
|
||||
<form @formname="hidden-login-form" id="nexusLoginForm" method="post" action="/account/login-form" style="display:none">
|
||||
<input type="hidden" name="email" value="@_loginModel.Email" />
|
||||
<input type="hidden" name="password" value="@_loginModel.Password" />
|
||||
<input type="hidden" name="rememberMe" value="@(_loginModel.RememberMe ? "true" : "false")" />
|
||||
@@ -117,7 +117,10 @@
|
||||
[SupplyParameterFromQuery(Name = "returnUrl")]
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
private LoginModel _loginModel = new();
|
||||
#pragma warning disable BL0008
|
||||
[SupplyParameterFromForm(FormName = "login-form")]
|
||||
private LoginModel _loginModel { get; set; } = new();
|
||||
#pragma warning restore BL0008
|
||||
private string? _errorMessage;
|
||||
private bool _isSubmitting;
|
||||
private bool _showPassword;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<p class="auth-subtitle">Utwórz nowe konto</p>
|
||||
</div>
|
||||
|
||||
<EditForm Model="@_registerModel" OnValidSubmit="HandleRegister" class="auth-form">
|
||||
<EditForm FormName="register-form" Model="@_registerModel" OnValidSubmit="HandleRegister" class="auth-form">
|
||||
<DataAnnotationsValidator />
|
||||
|
||||
<div class="field-group">
|
||||
@@ -71,14 +71,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form id="nexusLoginForm" method="post" action="/account/login-form" style="display:none">
|
||||
<form @formname="hidden-register-login-form" id="nexusLoginForm" method="post" action="/account/login-form" style="display:none">
|
||||
<input type="hidden" name="email" value="@_registerModel.Email" />
|
||||
<input type="hidden" name="password" value="@_registerModel.Password" />
|
||||
<input type="hidden" name="rememberMe" value="false" />
|
||||
</form>
|
||||
|
||||
@code {
|
||||
private RegisterModel _registerModel = new();
|
||||
#pragma warning disable BL0008
|
||||
[SupplyParameterFromForm(FormName = "register-form")]
|
||||
private RegisterModel _registerModel { get; set; } = new();
|
||||
#pragma warning restore BL0008
|
||||
private string? _errorMessage;
|
||||
private bool _isSubmitting;
|
||||
|
||||
|
||||
@@ -61,24 +61,28 @@
|
||||
|
||||
@if (_profile?.MappedConcepts != null && _profile.MappedConcepts.Any())
|
||||
{
|
||||
<div @key='"satellite-concepts-container"' style="display: contents;">
|
||||
@for (int i = 0; i < _profile.MappedConcepts.Count; i++)
|
||||
{
|
||||
var concept = _profile.MappedConcepts[i];
|
||||
var angle = i * (360.0 / _profile.MappedConcepts.Count);
|
||||
var dist = 65;
|
||||
<div class="graph-node satellite"
|
||||
<div @key="concept.Id" class="graph-node satellite"
|
||||
style="--angle: @(angle)deg; --dist: @(dist)px;"
|
||||
title="[@concept.Type] @concept.Content"
|
||||
@onmouseover="() => SetHoveredConcept(concept)"
|
||||
@onmouseout="ClearHoveredConcept">
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="graph-node satellite" style="--angle: 0deg; --dist: 60px;"></div>
|
||||
<div class="graph-node satellite" style="--angle: 120deg; --dist: 50px;"></div>
|
||||
<div class="graph-node satellite" style="--angle: 240deg; --dist: 70px;"></div>
|
||||
<div @key='"satellite-placeholders-container"' style="display: contents;">
|
||||
<div @key='"satellite-placeholder-0"' class="graph-node satellite" style="--angle: 0deg; --dist: 60px;"></div>
|
||||
<div @key='"satellite-placeholder-1"' class="graph-node satellite" style="--angle: 120deg; --dist: 50px;"></div>
|
||||
<div @key='"satellite-placeholder-2"' class="graph-node satellite" style="--angle: 240deg; --dist: 70px;"></div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="active-node-label">
|
||||
@@ -111,10 +115,10 @@
|
||||
<div class="quiz-preview">
|
||||
@if (_profile?.RecentQuizzes != null && _profile.RecentQuizzes.Any())
|
||||
{
|
||||
<div class="quiz-history-list">
|
||||
<div @key='"quiz-history-list"' class="quiz-history-list">
|
||||
@foreach (var quiz in _profile.RecentQuizzes)
|
||||
{
|
||||
<div class="quiz-history-item">
|
||||
<div @key="quiz.Id" class="quiz-history-item">
|
||||
<div class="quiz-item-header">
|
||||
<span class="quiz-topic">@quiz.Topic</span>
|
||||
<span class="quiz-score badge @(quiz.Percentage >= 80 ? "badge-success" : quiz.Percentage >= 50 ? "badge-warning" : "badge-danger")">
|
||||
@@ -130,7 +134,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="empty-quiz-state">
|
||||
<div @key='"empty-quiz-state"' class="empty-quiz-state">
|
||||
<p class="question">Brak rozwiązanych quizów</p>
|
||||
<p class="sub-text">Rozwiązuj quizy w trakcie czytania książek, aby śledzić swoje postępy.</p>
|
||||
</div>
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
|
||||
|
||||
/* Global Semantic Theme Mapping */
|
||||
:root {
|
||||
--nexus-primary: var(--nexus-neon);
|
||||
--nexus-primary-glow: var(--nexus-neon-glow);
|
||||
--nexus-primary-hover: #00e688;
|
||||
|
||||
@@ -62,6 +62,10 @@ builder.Services.AddSingleton<IQuizResultRepository>(new ThrowingQuizResultRepos
|
||||
builder.Services.AddSingleton<IConceptsMapReadRepository>(new ThrowingConceptsMapReadRepository());
|
||||
builder.Services.AddSingleton<ISyncBroadcaster>(new ThrowingSyncBroadcaster());
|
||||
builder.Services.AddSingleton<IEpubExtractor>(new ThrowingEpubExtractor());
|
||||
builder.Services.AddSingleton<IUserLibraryStore>(new ThrowingUserLibraryStore());
|
||||
builder.Services.AddSingleton<IVectorSearchStore>(new ThrowingVectorSearchStore());
|
||||
builder.Services.Configure<NexusReader.Application.Common.RagMonetizationOptions>(builder.Configuration.GetSection(NexusReader.Application.Common.RagMonetizationOptions.SectionName));
|
||||
builder.Services.AddSingleton<IChatClient>(new ThrowingChatClient());
|
||||
|
||||
builder.Services.AddApplication();
|
||||
builder.Services.AddScoped<IEpubReader, WasmEpubReader>();
|
||||
@@ -135,3 +139,37 @@ public class ThrowingEpubExtractor : IEpubExtractor
|
||||
=> throw new NotSupportedException("EPUB text extraction is not supported in the WASM client.");
|
||||
}
|
||||
|
||||
public class ThrowingUserLibraryStore : IUserLibraryStore
|
||||
{
|
||||
public Task<List<Guid>> GetOwnedBookIdsAsync(string userId, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("UserLibrary operations are not supported in the WASM client.");
|
||||
|
||||
public Task<Dictionary<Guid, string>> GetBookTitlesAsync(List<Guid> bookIds, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("UserLibrary operations are not supported in the WASM client.");
|
||||
}
|
||||
|
||||
public class ThrowingVectorSearchStore : IVectorSearchStore
|
||||
{
|
||||
public Task<List<VectorChunk>> SearchGlobalAsync(string queryText, string tenantId, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("VectorSearch operations are not supported in the WASM client.");
|
||||
|
||||
public Task<List<VectorChunk>> SearchLocalAsync(string queryText, string tenantId, List<Guid> whitelistedBookIds, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("VectorSearch operations are not supported in the WASM client.");
|
||||
|
||||
public Task<List<VectorChunk>> SearchGlobalExcludeAsync(string queryText, string tenantId, Guid excludeBookId, int limit, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("VectorSearch operations are not supported in the WASM client.");
|
||||
}
|
||||
|
||||
public class ThrowingChatClient : IChatClient
|
||||
{
|
||||
public void Dispose() { }
|
||||
|
||||
public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("Chat operations are not supported in the WASM client.");
|
||||
|
||||
public IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(IEnumerable<ChatMessage> chatMessages, ChatOptions? options = null, CancellationToken cancellationToken = default)
|
||||
=> throw new NotSupportedException("Chat operations are not supported in the WASM client.");
|
||||
|
||||
public object? GetService(Type serviceType, object? serviceKey = null) => null;
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<IPlatformService, WebPlatformService>();
|
||||
builder.Services.AddScoped<INativeStorageService, NexusReader.Web.Services.NativeStorageService>();
|
||||
builder.Services.AddScoped<IUserPreferenceStore, NexusReader.Web.Services.ServerUserPreferenceStore>();
|
||||
builder.Services.AddScoped<IThemeService, NexusReader.Web.Services.ServerThemeService>();
|
||||
builder.Services.AddScoped<IRecommendationService, NexusReader.Web.Services.ServerRecommendationService>();
|
||||
// Feature settings (avoiding direct raw IConfiguration injection in client pages)
|
||||
var featureSettings = builder.Configuration.GetSection("Features").Get<FeatureSettings>() ?? new FeatureSettings();
|
||||
builder.Services.AddSingleton(featureSettings);
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentResults;
|
||||
using MediatR;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using NexusReader.Application.Queries.Recommendations;
|
||||
using NexusReader.UI.Shared.Services;
|
||||
|
||||
namespace NexusReader.Web.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Server-side implementation of <see cref="IRecommendationService"/> that executes
|
||||
/// the MediatR query directly inside the Web Server's request context.
|
||||
/// </summary>
|
||||
public sealed class ServerRecommendationService : IRecommendationService
|
||||
{
|
||||
private readonly IMediator _mediator;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
|
||||
public ServerRecommendationService(IMediator mediator, IHttpContextAccessor httpContextAccessor)
|
||||
{
|
||||
_mediator = mediator;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
public async Task<List<RecommendationDto>?> GetRecommendationsAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var httpContext = _httpContextAccessor.HttpContext;
|
||||
if (httpContext?.User == null)
|
||||
{
|
||||
return new List<RecommendationDto>();
|
||||
}
|
||||
|
||||
var userId = httpContext.User.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
return new List<RecommendationDto>();
|
||||
}
|
||||
|
||||
var result = await _mediator.Send(new GetContextualRecommendationsQuery(userId), cancellationToken);
|
||||
if (result.IsSuccess && result.Value != null)
|
||||
{
|
||||
return result.Value.Recommendations;
|
||||
}
|
||||
|
||||
return new List<RecommendationDto>();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using NexusReader.Domain.Enums;
|
||||
using NexusReader.UI.Shared.Services;
|
||||
|
||||
namespace NexusReader.Web.Services;
|
||||
|
||||
public sealed class ServerThemeService : IThemeService
|
||||
{
|
||||
public ThemeMode Mode => ThemeMode.System;
|
||||
public bool IsLightMode => false;
|
||||
|
||||
// Explicit event implementation to avoid CS0067 warning about unused events on the server
|
||||
public event Action<ThemeMode>? OnThemeChanged
|
||||
{
|
||||
add { }
|
||||
remove { }
|
||||
}
|
||||
|
||||
public Task InitializeAsync() => Task.CompletedTask;
|
||||
public Task SetThemeAsync(ThemeMode mode) => Task.CompletedTask;
|
||||
public Task ToggleTheme() => Task.CompletedTask;
|
||||
}
|
||||
Reference in New Issue
Block a user