### Description This PR implements **Issue #34: [UI/UX] Implement Hybrid Metadata Verification Form in Ingestion Modal**. ### Key Changes - **Metadata Verification State**: Introduced a new state in `BookIngestionModal.razor` allowing users to edit `Title` and `Author` before final ingestion. - **Cover Image Preview**: Added a high-fidelity cover preview with a CSS-based glowing placeholder fallback for books without embedded covers. - **Ingestion Pipeline**: - Implemented `IngestEbookCommand` and `IngestEbookCommandHandler`. - Added `IBookStorageService` and its implementation for managing EPUB and cover file storage. - Exposed `POST /api/library/ingest` Minimal API endpoint with `.DisableAntiforgery()` to handle client-side JSON uploads. - **Stability Fixes**: - Resolved DI validation errors in the WASM client by providing a dummy `IBookStorageService` registration. - Adjusted Kestrel request limits to handle large EPUB payloads (up to 100MB). - Corrected middleware ordering to ensure Antiforgery works correctly with Authentication. ### Verification - Solution builds successfully. - Manual verification of modal state transitions and API ingestion logic. Closes #34. --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #41 Reviewed-by: Marek Jaisński <jasins.marek@gmail.com> Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
This commit was merged in pull request #41.
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
using NexusReader.Web.Components;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using NexusReader.Application;
|
||||
using NexusReader.Infrastructure;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
using NexusReader.Application.Queries.User;
|
||||
using NexusReader.Application.Commands.Library;
|
||||
using MediatR;
|
||||
using NexusReader.Web.Client.Services;
|
||||
using NexusReader.UI.Shared.Services;
|
||||
@@ -48,13 +50,23 @@ builder.Services.AddScoped<IReaderInteractionService, ReaderInteractionService>(
|
||||
builder.Services.AddScoped<KnowledgeCoordinator>();
|
||||
builder.Services.AddScoped<ISyncService, SyncService>();
|
||||
|
||||
builder.Services.AddHttpClient("NexusAPI", client =>
|
||||
builder.Services.AddHttpClient("NexusAPI", (sp, client) =>
|
||||
{
|
||||
client.BaseAddress = new Uri(builder.Configuration["ApiBaseUrl"] ?? "http://localhost:5000");
|
||||
var configuration = sp.GetRequiredService<IConfiguration>();
|
||||
var apiBaseUrl = configuration["ApiBaseUrl"];
|
||||
if (!string.IsNullOrEmpty(apiBaseUrl))
|
||||
{
|
||||
client.BaseAddress = new Uri(apiBaseUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For local development/Interactive Server, we use the current base address
|
||||
var nav = sp.GetRequiredService<NavigationManager>();
|
||||
client.BaseAddress = new Uri(nav.BaseUri);
|
||||
}
|
||||
});
|
||||
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<IIdentityService, NexusReader.Web.Services.ServerIdentityService>();
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
|
||||
@@ -220,9 +232,9 @@ if (!app.Environment.IsDevelopment())
|
||||
app.UseHttpsRedirection();
|
||||
}
|
||||
|
||||
app.UseAntiforgery();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.UseAntiforgery();
|
||||
app.MapStaticAssets();
|
||||
app.MapHub<NexusReader.Infrastructure.RealTime.SyncHub>("/synchub");
|
||||
|
||||
@@ -281,6 +293,30 @@ knowledgeApi.MapDelete("/", async (IKnowledgeService knowledgeService) =>
|
||||
return Results.BadRequest(errorMsg);
|
||||
});
|
||||
|
||||
app.MapPost("/api/library/ingest", async ([FromBody] IngestEbookRequest request, ClaimsPrincipal user, IMediator mediator) =>
|
||||
{
|
||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (string.IsNullOrEmpty(userId)) return Results.Unauthorized();
|
||||
|
||||
var epubData = Convert.FromBase64String(request.EpubDataBase64);
|
||||
byte[]? coverData = !string.IsNullOrEmpty(request.CoverImageBase64)
|
||||
? Convert.FromBase64String(request.CoverImageBase64)
|
||||
: null;
|
||||
|
||||
var command = new IngestEbookCommand(
|
||||
request.Title,
|
||||
request.AuthorName,
|
||||
coverData,
|
||||
epubData,
|
||||
userId
|
||||
);
|
||||
|
||||
var result = await mediator.Send(command);
|
||||
if (result.IsSuccess) return Results.Ok(new { Id = result.Value });
|
||||
|
||||
return Results.BadRequest(result.Errors.FirstOrDefault()?.Message ?? "Ingestion failed");
|
||||
}).RequireAuthorization().DisableAntiforgery();
|
||||
|
||||
app.MapPost("/api/StripeWebhook", async (
|
||||
HttpContext context,
|
||||
UserManager<NexusUser> userManager,
|
||||
|
||||
Reference in New Issue
Block a user