Refactor: Web Consolidation and Identity Stabilization #40

Merged
mjasin merged 37 commits from feature/issue-33 into develop 2026-05-11 19:16:31 +00:00
Showing only changes of commit 38043cbda3 - Show all commits
@@ -1,21 +1,16 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Pgvector.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.AI; using Microsoft.Extensions.Configuration;
using GeminiDotnet; using Microsoft.Extensions.DependencyInjection;
using GeminiDotnet.Extensions.AI;
using NexusReader.Data.Persistence;
using NexusReader.Application.Abstractions.Services; using NexusReader.Application.Abstractions.Services;
using NexusReader.Data.Persistence;
using NexusReader.Infrastructure.Services; using NexusReader.Infrastructure.Services;
using NexusReader.Infrastructure.Configuration;
using Polly;
using Polly.Retry;
using NexusReader.Domain.Entities;
using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity;
using NexusReader.Domain.Entities;
using NexusReader.Infrastructure.Identity;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using NexusReader.Application.Security.Authorization; using NexusReader.Application.Commands.Sync;
using NexusReader.Infrastructure.Handlers;
using MediatR;
namespace NexusReader.Infrastructure; namespace NexusReader.Infrastructure;
@@ -23,72 +18,54 @@ public static class DependencyInjection
{ {
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration) public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
{ {
var pgConnectionString = configuration.GetConnectionString("PostgresConnection"); var connectionString = configuration.GetConnectionString("DefaultConnection")
if (!string.IsNullOrEmpty(pgConnectionString)) ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
{
services.AddDbContextFactory<AppDbContext>(options =>
options.UseNpgsql(pgConnectionString, x => x.UseVector()));
}
else
{
var sqliteConnectionString = configuration.GetConnectionString("SqliteConnection") ?? "Data Source=nexus.db";
services.AddDbContextFactory<AppDbContext>(options =>
options.UseSqlite(sqliteConnectionString));
}
services.Configure<AiSettings>(configuration.GetSection(AiSettings.SectionName)); // Register DB Context Factory for multi-threaded/asynchronous safety in Blazor
services.Configure<StripeSettings>(configuration.GetSection(StripeSettings.SectionName)); services.AddDbContextFactory<AppDbContext>(options =>
var aiSettings = configuration.GetSection(AiSettings.SectionName).Get<AiSettings>() ?? new AiSettings();
Console.WriteLine($"[Infrastructure] AI Configured: Model={aiSettings.Model}, KeyPresent={!string.IsNullOrWhiteSpace(aiSettings.ApiKey) && aiSettings.ApiKey != "PLACEHOLDER"}");
if (string.IsNullOrWhiteSpace(aiSettings.ApiKey) || aiSettings.ApiKey == "PLACEHOLDER")
{ {
Console.WriteLine("[Infrastructure] WARNING: AI API Key is missing or placeholder!"); if (connectionString.Contains("Host="))
}
services.AddResiliencePipeline("ai-retry", builder =>
{
builder.AddRetry(new RetryStrategyOptions
{ {
ShouldHandle = new PredicateBuilder().Handle<Exception>(ex => options.UseNpgsql(connectionString, o => o.UseVector());
ex.Message.Contains("429") || ex.Message.Contains("Too Many Requests") || ex.Message.Contains("quota")), }
BackoffType = DelayBackoffType.Exponential, else
UseJitter = true, {
MaxRetryAttempts = aiSettings.RetryAttempts, options.UseSqlite(connectionString);
Delay = TimeSpan.FromSeconds(2) }
});
}); });
services.AddChatClient(new GeminiChatClient(new GeminiClientOptions // Register Scoped Context for traditional usage
{ services.AddDbContext<AppDbContext>(options =>
ApiKey = aiSettings.ApiKey,
ModelId = aiSettings.Model
}));
services.AddEmbeddingGenerator(new GeminiEmbeddingGenerator(new GeminiClientOptions
{ {
ApiKey = aiSettings.ApiKey, if (connectionString.Contains("Host="))
ModelId = aiSettings.EmbeddingModel ?? "text-embedding-004" {
})); options.UseNpgsql(connectionString, o => o.UseVector());
}
else
{
options.UseSqlite(connectionString);
}
});
// Identity Configuration
services.AddAuthorization(options =>
{
options.AddPolicy("TokenLimitPolicy", policy =>
policy.Requirements.Add(new TokenLimitRequirement()));
});
services.AddScoped<IAuthorizationHandler, TokenLimitHandler>();
// Services
services.AddScoped<IEpubReader, EpubReaderService>();
services.AddScoped<IEpubMetadataExtractor, EpubMetadataExtractor>();
services.AddScoped<IKnowledgeService, KnowledgeService>(); services.AddScoped<IKnowledgeService, KnowledgeService>();
services.AddTransient<IEpubService, EpubService>(); services.AddScoped<IBillingService, BillingService>();
services.AddSingleton<PromptRegistry>();
services.AddAuthorizationCore(options => // Handlers (MediatR)
{ services.AddScoped<IRequestHandler<UpdateReadingProgressCommand, FluentResults.Result>, UpdateReadingProgressCommandHandler>();
options.AddPolicy("ProUser", policy => policy.Requirements.Add(new ProUserRequirement()));
});
services.AddScoped<IAuthorizationHandler, ProUserHandler>();
services.AddScoped<IInfrastructureMarker, InfrastructureMarker>();
return services; return services;
} }
public static System.Reflection.Assembly Assembly => typeof(DependencyInjection).Assembly;
} }
public interface IInfrastructureMarker { }
internal class InfrastructureMarker : IInfrastructureMarker { }