refactor: register server-side storage and identity services, update API to use split IEpubReader interface
This commit is contained in:
@@ -1,208 +1,84 @@
|
||||
using NexusReader.Web.Components;
|
||||
using NexusReader.Application;
|
||||
using NexusReader.Infrastructure;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
using NexusReader.Application.Queries.User;
|
||||
using MediatR;
|
||||
using NexusReader.Web.Client.Services;
|
||||
using NexusReader.UI.Shared.Services;
|
||||
using NexusReader.Domain.Entities;
|
||||
using NexusReader.Data.Persistence;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Components.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using NexusReader.Infrastructure.Identity;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using System.Security.Claims;
|
||||
using NexusReader.Infrastructure.Services;
|
||||
using Stripe;
|
||||
|
||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
using NexusReader.Application.Queries.Reader;
|
||||
using NexusReader.Data.Persistence;
|
||||
using NexusReader.Domain.Entities;
|
||||
using NexusReader.Infrastructure.Configuration;
|
||||
using NexusReader.Infrastructure.RealTime;
|
||||
using NexusReader.UI.Shared.Services;
|
||||
using NexusReader.Web.Components;
|
||||
using NexusReader.Web.New.Services;
|
||||
using NexusReader.Infrastructure;
|
||||
using NexusReader.Application;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents()
|
||||
.AddInteractiveWebAssemblyComponents();
|
||||
// --- Configuration ---
|
||||
builder.Services.Configure<AiSettings>(builder.Configuration.GetSection("AiSettings"));
|
||||
builder.Services.Configure<StripeSettings>(builder.Configuration.GetSection("StripeSettings"));
|
||||
|
||||
// Enable detailed circuit errors for Server‑Side Blazor components
|
||||
builder.Services.AddServerSideBlazor()
|
||||
.AddCircuitOptions(options =>
|
||||
{
|
||||
options.DetailedErrors = true;
|
||||
});
|
||||
builder.Services.AddSignalR();
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
|
||||
builder.Services.AddScoped<IPlatformService, WebPlatformService>();
|
||||
builder.Services.AddScoped<INativeStorageService, NexusReader.UI.Shared.Services.WebStorageService>();
|
||||
builder.Services.AddScoped<IThemeService, ThemeService>();
|
||||
builder.Services.AddScoped<IQuizStateService, QuizStateService>();
|
||||
builder.Services.AddScoped<IFocusModeService, FocusModeService>();
|
||||
builder.Services.AddScoped<IReaderNavigationService, ReaderNavigationService>();
|
||||
builder.Services.AddScoped<IKnowledgeGraphService, KnowledgeGraphService>();
|
||||
builder.Services.AddScoped<IReaderInteractionService, ReaderInteractionService>();
|
||||
builder.Services.AddScoped<KnowledgeCoordinator>();
|
||||
builder.Services.AddScoped<ISyncService, SyncService>();
|
||||
|
||||
builder.Services.AddHttpClient("NexusAPI", client =>
|
||||
{
|
||||
client.BaseAddress = new Uri(builder.Configuration["ApiBaseUrl"] ?? "http://localhost:5000");
|
||||
});
|
||||
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
|
||||
|
||||
builder.Services.AddHttpContextAccessor();
|
||||
builder.Services.AddScoped<IIdentityService, NexusReader.Web.New.Services.ServerIdentityService>();
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
|
||||
builder.Services.AddApplication();
|
||||
// --- Infrastructure Layer ---
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
|
||||
builder.Services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblies(
|
||||
NexusReader.Application.DependencyInjection.Assembly,
|
||||
NexusReader.Infrastructure.DependencyInjection.Assembly
|
||||
));
|
||||
// --- Application Layer ---
|
||||
builder.Services.AddApplication();
|
||||
|
||||
// Authorization Policies
|
||||
builder.Services.AddScoped<IAuthorizationHandler, TokenLimitHandler>();
|
||||
builder.Services.AddAuthorizationBuilder()
|
||||
.AddPolicy("ProUser", policy => policy.RequireClaim("Plan", SubscriptionPlan.ProName, SubscriptionPlan.EnterpriseName))
|
||||
.AddPolicy("HasAvailableTokens", policy => policy.AddRequirements(new TokenLimitRequirement()));
|
||||
// --- Identity & Auth ---
|
||||
builder.Services.AddCascadingAuthenticationState();
|
||||
builder.Services.AddScoped<AuthenticationStateProvider, NexusAuthenticationStateProvider>();
|
||||
|
||||
// Billing & Stripe
|
||||
builder.Services.AddScoped<IBillingService, NexusReader.Infrastructure.Services.BillingService>();
|
||||
// Register Server-Side Native Storage (JSInterop Wrapper)
|
||||
builder.Services.AddScoped<INativeStorageService, NativeStorageService>();
|
||||
|
||||
// Register Server-Side Identity Service
|
||||
builder.Services.AddScoped<IIdentityService, ServerIdentityService>();
|
||||
|
||||
// Authentication
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
options.DefaultScheme = IdentityConstants.ApplicationScheme;
|
||||
options.DefaultSignInScheme = IdentityConstants.ExternalScheme;
|
||||
})
|
||||
.AddGoogle(options =>
|
||||
{
|
||||
options.ClientId = builder.Configuration["Authentication:Google:ClientId"] ?? "placeholder-id";
|
||||
options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"] ?? "placeholder-secret";
|
||||
});
|
||||
.AddIdentityCookies();
|
||||
|
||||
builder.Services.AddIdentityApiEndpoints<NexusUser>()
|
||||
builder.Services.AddIdentityCore<NexusUser>(options => options.SignIn.RequireConfirmedAccount = true)
|
||||
.AddRoles<IdentityRole>()
|
||||
.AddEntityFrameworkStores<AppDbContext>();
|
||||
.AddEntityFrameworkStores<AppDbContext>()
|
||||
.AddSignInManager()
|
||||
.AddDefaultTokenProviders();
|
||||
|
||||
builder.Services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/account/login";
|
||||
options.LogoutPath = "/account/logout";
|
||||
options.AccessDeniedPath = "/account/access-denied";
|
||||
options.Cookie.Name = "NexusReader.Auth";
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.Cookie.SameSite = SameSiteMode.Lax;
|
||||
options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
|
||||
options.ExpireTimeSpan = TimeSpan.FromDays(30);
|
||||
options.SlidingExpiration = true;
|
||||
// --- Razor Components ---
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveWebAssemblyComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
options.Events.OnRedirectToLogin = context =>
|
||||
{
|
||||
var isApiRequest = context.Request.Path.StartsWithSegments("/api") ||
|
||||
context.Request.Path.StartsWithSegments("/identity") ||
|
||||
context.Request.Headers["Accept"].ToString().Contains("application/json");
|
||||
|
||||
if (isApiRequest)
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Response.Redirect(context.RedirectUri);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
});
|
||||
|
||||
builder.Services.Configure<IdentityOptions>(options =>
|
||||
{
|
||||
// Password settings
|
||||
options.Password.RequireDigit = true;
|
||||
options.Password.RequireLowercase = true;
|
||||
options.Password.RequireNonAlphanumeric = true;
|
||||
options.Password.RequireUppercase = true;
|
||||
options.Password.RequiredLength = 8;
|
||||
options.Password.RequiredUniqueChars = 1;
|
||||
|
||||
// Lockout settings
|
||||
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5);
|
||||
options.Lockout.MaxFailedAccessAttempts = 5;
|
||||
options.Lockout.AllowedForNewUsers = true;
|
||||
|
||||
// User settings
|
||||
options.User.AllowedUserNameCharacters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+";
|
||||
options.User.RequireUniqueEmail = true;
|
||||
});
|
||||
// --- API Client for WASM Compatibility ---
|
||||
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["ApiBaseUrl"] ?? "https://localhost:7165/") });
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Startup Validation
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var marker = scope.ServiceProvider.GetService<IInfrastructureMarker>();
|
||||
if (marker == null)
|
||||
{
|
||||
throw new InvalidOperationException("CRITICAL: Infrastructure layer was not registered. Ensure AddInfrastructure() is called in Program.cs.");
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure Database is initialized and seeded
|
||||
// --- Database Initialization ---
|
||||
using (var scope = app.Services.CreateScope())
|
||||
{
|
||||
var services = scope.ServiceProvider;
|
||||
var logger = services.GetRequiredService<ILogger<Program>>();
|
||||
var dbContextFactory = services.GetRequiredService<IDbContextFactory<NexusReader.Data.Persistence.AppDbContext>>();
|
||||
using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
|
||||
int maxRetries = 5;
|
||||
int delayMs = 2000;
|
||||
|
||||
for (int i = 0; i < maxRetries; i++)
|
||||
try
|
||||
{
|
||||
try
|
||||
var context = services.GetRequiredService<AppDbContext>();
|
||||
if (context.Database.IsRelational())
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
logger.LogInformation("Próba połączenia z bazą danych (próba {Attempt}/{MaxRetries})...", i + 1, maxRetries);
|
||||
}
|
||||
|
||||
await dbContext.Database.MigrateAsync();
|
||||
await DbInitializer.SeedAsync(services);
|
||||
|
||||
if (logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
logger.LogInformation("Baza danych zainicjowana pomyślnie.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
catch (Npgsql.NpgsqlException ex) when (i < maxRetries - 1)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Warning))
|
||||
{
|
||||
logger.LogWarning(ex, "Błąd połączenia z bazą danych. Ponowna próba za {Delay}ms...", delayMs);
|
||||
}
|
||||
|
||||
await Task.Delay(delayMs);
|
||||
delayMs *= 2; // Exponential backoff
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Critical))
|
||||
{
|
||||
logger.LogCritical(ex, "Krytyczny błąd podczas inicjalizacji bazy danych.");
|
||||
}
|
||||
throw;
|
||||
await context.Database.MigrateAsync();
|
||||
}
|
||||
await DbInitializer.InitializeAsync(services);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var logger = services.GetRequiredService<ILogger<Program>>();
|
||||
logger.LogError(ex, "An error occurred while migrating or seeding the database.");
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
// --- Middleware Pipeline ---
|
||||
if (app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseWebAssemblyDebugging();
|
||||
@@ -213,243 +89,23 @@ else
|
||||
app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true);
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
app.UseHttpsRedirection();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAntiforgery();
|
||||
app.UseAuthentication();
|
||||
app.UseAuthorization();
|
||||
app.MapStaticAssets();
|
||||
app.MapHub<NexusReader.Infrastructure.RealTime.SyncHub>("/synchub");
|
||||
|
||||
// API endpoint for WASM client to fetch EPUB content
|
||||
app.MapGet("/api/epub/{index}", async (int index, IEpubService epubService, ClaimsPrincipal user) =>
|
||||
{
|
||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
var result = await epubService.GetEpubContentAsync(index, userId);
|
||||
|
||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||
|
||||
var errorMsg = result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error";
|
||||
return Results.BadRequest(errorMsg);
|
||||
}).RequireAuthorization();
|
||||
|
||||
var knowledgeApi = app.MapGroup("/api/knowledge").RequireAuthorization("HasAvailableTokens");
|
||||
|
||||
knowledgeApi.MapPost("/", async (KnowledgeRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
||||
{
|
||||
var tenantId = user.FindFirstValue("TenantId") ?? "global";
|
||||
var result = await knowledgeService.GetKnowledgeAsync(request.Text, tenantId);
|
||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||
return Results.BadRequest(result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error");
|
||||
});
|
||||
|
||||
knowledgeApi.MapPost("/graph", async (KnowledgeRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
||||
{
|
||||
var tenantId = user.FindFirstValue("TenantId") ?? "global";
|
||||
var result = await knowledgeService.GetGraphDataAsync(request.Text, tenantId);
|
||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||
return Results.BadRequest(result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error");
|
||||
});
|
||||
|
||||
knowledgeApi.MapPost("/summary", async (KnowledgeRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
||||
{
|
||||
var tenantId = user.FindFirstValue("TenantId") ?? "global";
|
||||
var result = await knowledgeService.GetSummaryAndQuizAsync(request.Text, tenantId);
|
||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||
return Results.BadRequest(result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error");
|
||||
});
|
||||
|
||||
knowledgeApi.MapPost("/verify-groundedness", async (GroundednessRequest request, ClaimsPrincipal user, IKnowledgeService knowledgeService) =>
|
||||
{
|
||||
var tenantId = user.FindFirstValue("TenantId") ?? "global";
|
||||
var result = await knowledgeService.VerifyGroundednessAsync(request.Answer, request.Context, tenantId);
|
||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||
return Results.BadRequest(result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error");
|
||||
});
|
||||
|
||||
knowledgeApi.MapDelete("/", async (IKnowledgeService knowledgeService) =>
|
||||
{
|
||||
var result = await knowledgeService.ClearCacheAsync();
|
||||
if (result.IsSuccess) return Results.Ok();
|
||||
|
||||
var errorMsg = result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error";
|
||||
return Results.BadRequest(errorMsg);
|
||||
});
|
||||
|
||||
app.MapPost("/api/StripeWebhook", async (
|
||||
HttpContext context,
|
||||
UserManager<NexusUser> userManager,
|
||||
IConfiguration configuration,
|
||||
IDbContextFactory<AppDbContext> dbContextFactory) =>
|
||||
{
|
||||
using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||
var json = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
||||
var webhookSecret = configuration["Stripe:WebhookSecret"] ?? "";
|
||||
|
||||
try
|
||||
{
|
||||
var stripeEvent = EventUtility.ConstructEvent(
|
||||
json,
|
||||
context.Request.Headers["Stripe-Signature"],
|
||||
webhookSecret
|
||||
);
|
||||
|
||||
switch (stripeEvent.Type)
|
||||
{
|
||||
case EventTypes.CheckoutSessionCompleted:
|
||||
var session = stripeEvent.Data.Object as Stripe.Checkout.Session;
|
||||
await HandleSubscriptionSuccess(session?.CustomerEmail, session?.Metadata, userManager, dbContext);
|
||||
break;
|
||||
|
||||
case EventTypes.CustomerSubscriptionUpdated:
|
||||
var subscription = stripeEvent.Data.Object as Stripe.Subscription;
|
||||
await HandleSubscriptionSuccess(subscription?.Metadata["CustomerEmail"], subscription?.Metadata, userManager, dbContext);
|
||||
break;
|
||||
|
||||
case EventTypes.CustomerSubscriptionDeleted:
|
||||
var deletedSubscription = stripeEvent.Data.Object as Stripe.Subscription;
|
||||
await HandleSubscriptionCancellation(deletedSubscription?.Metadata["CustomerEmail"], userManager, dbContext);
|
||||
break;
|
||||
}
|
||||
return Results.Ok();
|
||||
}
|
||||
catch (StripeException e)
|
||||
{
|
||||
return Results.BadRequest(e.Message);
|
||||
}
|
||||
});
|
||||
|
||||
async Task HandleSubscriptionSuccess(
|
||||
string? email,
|
||||
Dictionary<string, string>? metadata,
|
||||
UserManager<NexusUser> userManager,
|
||||
AppDbContext dbContext)
|
||||
{
|
||||
if (string.IsNullOrEmpty(email)) return;
|
||||
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user != null)
|
||||
{
|
||||
var planName = metadata?.GetValueOrDefault("Plan") ?? SubscriptionPlan.ProName;
|
||||
var plan = await dbContext.SubscriptionPlans.FirstOrDefaultAsync(p => p.PlanName == planName);
|
||||
|
||||
if (plan != null)
|
||||
{
|
||||
user.SubscriptionPlanId = plan.Id;
|
||||
user.AITokenLimit = plan.AITokenLimit;
|
||||
}
|
||||
|
||||
await userManager.UpdateAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
async Task HandleSubscriptionCancellation(
|
||||
string? email,
|
||||
UserManager<NexusUser> userManager,
|
||||
AppDbContext dbContext)
|
||||
{
|
||||
if (string.IsNullOrEmpty(email)) return;
|
||||
|
||||
var user = await userManager.FindByEmailAsync(email);
|
||||
if (user != null)
|
||||
{
|
||||
var freePlan = await dbContext.SubscriptionPlans.FindAsync(SubscriptionPlan.FreeId);
|
||||
user.SubscriptionPlanId = SubscriptionPlan.FreeId;
|
||||
user.AITokenLimit = freePlan?.AITokenLimit ?? 5000;
|
||||
await userManager.UpdateAsync(user);
|
||||
}
|
||||
}
|
||||
|
||||
app.MapGroup("/identity").MapIdentityApi<NexusUser>();
|
||||
|
||||
app.MapGet("/identity/login/google", (string? returnUrl) =>
|
||||
{
|
||||
var properties = new AuthenticationProperties
|
||||
{
|
||||
RedirectUri = "/identity/callback/google",
|
||||
Items = { { "returnUrl", returnUrl ?? "/" } }
|
||||
};
|
||||
return Results.Challenge(properties, new[] { "Google" });
|
||||
});
|
||||
|
||||
app.MapGet("/identity/callback/google", async (
|
||||
HttpContext context,
|
||||
SignInManager<NexusUser> signInManager,
|
||||
UserManager<NexusUser> userManager,
|
||||
ILogger<Program> logger) =>
|
||||
{
|
||||
var info = await signInManager.GetExternalLoginInfoAsync();
|
||||
if (info == null)
|
||||
{
|
||||
logger.LogWarning("External login info from Google is null.");
|
||||
return Results.Redirect("/account/login?error=ExternalLoginFailed");
|
||||
}
|
||||
|
||||
var result = await signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
logger.LogInformation("User logged in via Google: {Email}", info.Principal.FindFirstValue(ClaimTypes.Email));
|
||||
return Results.Redirect("/");
|
||||
}
|
||||
|
||||
if (result.IsLockedOut)
|
||||
{
|
||||
logger.LogWarning("User account locked out during Google login: {Email}", info.Principal.FindFirstValue(ClaimTypes.Email));
|
||||
return Results.Redirect("/account/login?error=LockedOut");
|
||||
}
|
||||
|
||||
// New user provisioning
|
||||
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
|
||||
if (email != null)
|
||||
{
|
||||
var user = new NexusUser { UserName = email, Email = email, EmailConfirmed = true };
|
||||
var createResult = await userManager.CreateAsync(user);
|
||||
|
||||
if (createResult.Succeeded)
|
||||
{
|
||||
await userManager.AddLoginAsync(user, info);
|
||||
await signInManager.SignInAsync(user, isPersistent: false);
|
||||
logger.LogInformation("New user provisioned via Google: {Email}", email);
|
||||
return Results.Redirect("/");
|
||||
}
|
||||
|
||||
// Log specific errors
|
||||
foreach (var error in createResult.Errors)
|
||||
{
|
||||
logger.LogError("Google provisioning failed for {Email}: {Code} - {Description}", email, error.Code, error.Description);
|
||||
}
|
||||
|
||||
if (createResult.Errors.Any(e => e.Code == "DuplicateEmail" || e.Code == "DuplicateUserName"))
|
||||
{
|
||||
return Results.Redirect("/account/login?error=UserAlreadyExists");
|
||||
}
|
||||
}
|
||||
|
||||
logger.LogError("Google provisioning failed - unknown reason for email {Email}", email);
|
||||
return Results.Redirect("/account/login?error=ProvisioningFailed");
|
||||
});
|
||||
|
||||
app.MapGet("/identity/profile", async (ClaimsPrincipal user, IMediator mediator) =>
|
||||
{
|
||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null) return Results.Unauthorized();
|
||||
|
||||
var result = await mediator.Send(new GetUserProfileQuery(userId));
|
||||
if (result.IsFailed) return Results.NotFound(result.Errors.FirstOrDefault()?.Message);
|
||||
|
||||
return Results.Ok(result.Value);
|
||||
}).RequireAuthorization();
|
||||
|
||||
app.MapRazorComponents<App>()
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddInteractiveWebAssemblyRenderMode()
|
||||
.AddAdditionalAssemblies(typeof(NexusReader.UI.Shared.Services.IKnowledgeGraphService).Assembly);
|
||||
.AddInteractiveServerRenderMode()
|
||||
.AddAdditionalAssemblies(typeof(NexusReader.UI.Shared._Imports).Assembly)
|
||||
.AddAdditionalAssemblies(typeof(NexusReader.Web.Client._Imports).Assembly);
|
||||
|
||||
// --- API Endpoints ---
|
||||
app.MapHub<SyncHub>("/hubs/sync");
|
||||
|
||||
app.MapGet("/api/epub/content", async (int index, string? userId, IEpubReader epubReader) =>
|
||||
{
|
||||
var result = await epubReader.GetEpubContentAsync(index, userId);
|
||||
return result.IsSuccess ? Results.Ok(result.Value) : Results.BadRequest(result.Errors);
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
public record KnowledgeRequest(string Text);
|
||||
public record GroundednessRequest(string Answer, string Context);
|
||||
|
||||
Reference in New Issue
Block a user