using NexusReader.Web.Components; using NexusReader.Application; using NexusReader.Infrastructure; using NexusReader.Application.Abstractions.Services; using NexusReader.Web.Client.Services; using NexusReader.Web.New.Services; using NexusReader.UI.Shared.Services; using NexusReader.Domain.Entities; using NexusReader.Infrastructure.Persistence; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Authorization; using NexusReader.Infrastructure.Identity; using Microsoft.AspNetCore.Authentication; using System.Security.Claims; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents() .AddInteractiveWebAssemblyComponents(); builder.Services.AddControllers(); // Enable detailed circuit errors for Server‑Side Blazor components builder.Services.AddServerSideBlazor() .AddCircuitOptions(options => { options.DetailedErrors = true; }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHttpClient("NexusAPI", client => { client.BaseAddress = new Uri(builder.Configuration["ApiBaseUrl"] ?? "http://localhost:5000"); }); builder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("NexusAPI")); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(sp => sp.GetRequiredService()); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddApplication(); builder.Services.AddInfrastructure(builder.Configuration); // Authorization Policies builder.Services.AddScoped(); builder.Services.AddAuthorization(options => { options.AddPolicy("ProUser", policy => policy.RequireClaim("Plan", "Pro", "Enterprise")); options.AddPolicy("HasAvailableTokens", policy => policy.AddRequirements(new TokenLimitRequirement())); }); // 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"; }); builder.Services.AddIdentityApiEndpoints() .AddEntityFrameworkStores(); builder.Services.ConfigureApplicationCookie(options => { options.Cookie.HttpOnly = true; options.ExpireTimeSpan = TimeSpan.FromDays(30); options.SlidingExpiration = true; }); builder.Services.Configure(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; }); var app = builder.Build(); // Ensure Database is initialized using (var scope = app.Services.CreateScope()) { var dbContext = scope.ServiceProvider.GetRequiredService(); await dbContext.Database.MigrateAsync(); } // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseWebAssemblyDebugging(); } else { app.UseExceptionHandler("/Error", createScopeForErrors: true); app.UseHsts(); } app.UseStatusCodePagesWithReExecute("/not-found", createScopeForStatusCodePages: true); if (!app.Environment.IsDevelopment()) { app.UseHttpsRedirection(); } app.UseAntiforgery(); app.UseAuthentication(); app.UseAuthorization(); app.MapStaticAssets(); app.MapControllers(); // API endpoint for WASM client to fetch EPUB content app.MapGet("/api/epub/{index}", async (int index, IEpubService epubService) => { var result = await epubService.GetEpubContentAsync(index); if (result.IsSuccess) return Results.Ok(result.Value); var errorMsg = result.Errors.FirstOrDefault()?.Message ?? "Unknown server error"; return Results.BadRequest(errorMsg); }); app.MapPost("/api/knowledge", async (KnowledgeRequest request, IKnowledgeService knowledgeService) => { var result = await knowledgeService.GetKnowledgeAsync(request.Text); if (result.IsSuccess) return Results.Ok(result.Value); return Results.BadRequest(result.Errors.FirstOrDefault()?.Message ?? "Unknown server error"); }); app.MapPost("/api/knowledge/graph", async (KnowledgeRequest request, IKnowledgeService knowledgeService) => { var result = await knowledgeService.GetGraphDataAsync(request.Text); if (result.IsSuccess) return Results.Ok(result.Value); return Results.BadRequest(result.Errors.FirstOrDefault()?.Message ?? "Unknown server error"); }); app.MapPost("/api/knowledge/summary", async (KnowledgeRequest request, IKnowledgeService knowledgeService) => { var result = await knowledgeService.GetSummaryAndQuizAsync(request.Text); if (result.IsSuccess) return Results.Ok(result.Value); return Results.BadRequest(result.Errors.FirstOrDefault()?.Message ?? "Unknown server error"); }); app.MapDelete("/api/knowledge", async (IKnowledgeService knowledgeService) => { var result = await knowledgeService.ClearCacheAsync(); if (result.IsSuccess) return Results.Ok(); var errorMsg = result.Errors.FirstOrDefault()?.Message ?? "Unknown server error"; return Results.BadRequest(errorMsg); }); app.MapGroup("/identity").MapIdentityApi(); 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 signInManager, UserManager userManager) => { var info = await signInManager.GetExternalLoginInfoAsync(); if (info == null) return Results.Redirect("/account/login?error=ExternalLoginFailed"); var result = await signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false); if (result.Succeeded) { return Results.Redirect("/"); } // 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); return Results.Redirect("/"); } } return Results.Redirect("/account/login?error=ProvisioningFailed"); }); app.MapGet("/identity/profile", async (ClaimsPrincipal user, UserManager userManager) => { var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); if (userId == null) return Results.Unauthorized(); var nexusUser = await userManager.FindByIdAsync(userId); if (nexusUser == null) return Results.NotFound(); return Results.Ok(new { nexusUser.Email, nexusUser.AITokenLimit, nexusUser.AITokensUsed, nexusUser.CurrentPlan, nexusUser.TenantId }); }).RequireAuthorization(); app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(typeof(NexusReader.UI.Shared.Services.IKnowledgeGraphService).Assembly); app.Run(); public record KnowledgeRequest(string Text);