fix: migrate to IDbContextFactory and remove direct AppDbContext from DI (#11)
Reviewed-on: #11 Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Co-committed-by: Marek Jasiński <jasins.marek@gmail.com>
This commit was merged in pull request #11.
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
<Project Path="src/NexusReader.Infrastructure.Mobile/NexusReader.Infrastructure.Mobile.csproj" />
|
<Project Path="src/NexusReader.Infrastructure.Mobile/NexusReader.Infrastructure.Mobile.csproj" />
|
||||||
<Project Path="src/NexusReader.Web.Client/NexusReader.Web.Client.csproj" />
|
<Project Path="src/NexusReader.Web.Client/NexusReader.Web.Client.csproj" />
|
||||||
<Project Path="src/NexusReader.UI.Shared/NexusReader.UI.Shared.csproj" />
|
<Project Path="src/NexusReader.UI.Shared/NexusReader.UI.Shared.csproj" />
|
||||||
|
<Project Path="src/NexusReader.Data/NexusReader.Data.csproj" />
|
||||||
<Project Path="src/NexusReader.Maui/NexusReader.Maui.csproj" />
|
<Project Path="src/NexusReader.Maui/NexusReader.Maui.csproj" />
|
||||||
</Folder>
|
</Folder>
|
||||||
<Folder Name="/src/NexusReader.Web.New/">
|
<Folder Name="/src/NexusReader.Web.New/">
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using NexusReader.Infrastructure.Persistence;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using NexusReader.Domain.Entities;
|
|
||||||
using System;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
var configuration = new ConfigurationBuilder()
|
|
||||||
.AddJsonFile("src/NexusReader.Web.New/appsettings.json")
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
var services = new ServiceCollection();
|
|
||||||
var pgConnectionString = configuration.GetConnectionString("PostgresConnection");
|
|
||||||
if (!string.IsNullOrEmpty(pgConnectionString))
|
|
||||||
{
|
|
||||||
services.AddDbContext<AppDbContext>(options => options.UseNpgsql(pgConnectionString));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
services.AddDbContext<AppDbContext>(options => options.UseSqlite(configuration.GetConnectionString("SqliteConnection")));
|
|
||||||
}
|
|
||||||
|
|
||||||
var serviceProvider = services.BuildServiceProvider();
|
|
||||||
using var scope = serviceProvider.CreateScope();
|
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var user = await dbContext.Users.FirstOrDefaultAsync(u => u.Email == "admin@nexus.com");
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
Console.WriteLine("User admin@nexus.com NOT FOUND in database.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Console.WriteLine($"User found: {user.Email}, Id: {user.Id}, EmailConfirmed: {user.EmailConfirmed}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Error accessing database: {ex.Message}");
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
public class Program
|
|
||||||
{
|
|
||||||
public static void Main()
|
|
||||||
{
|
|
||||||
string input1 = "Hello \n World";
|
|
||||||
string input2 = "Hello World";
|
|
||||||
|
|
||||||
string norm1 = Normalize(input1);
|
|
||||||
string norm2 = Normalize(input2);
|
|
||||||
|
|
||||||
Console.WriteLine($"Input 1: '{input1}' -> Normalized: '{norm1}'");
|
|
||||||
Console.WriteLine($"Input 2: '{input2}' -> Normalized: '{norm2}'");
|
|
||||||
Console.WriteLine($"Match: {norm1 == norm2}");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static string Normalize(string input)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(input)) return string.Empty;
|
|
||||||
var normalized = Regex.Replace(input.Trim(), @"\s+", " ");
|
|
||||||
return normalized.ToLowerInvariant();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using NexusReader.Domain.Entities;
|
|
||||||
|
|
||||||
namespace NexusReader.Application.Abstractions.Persistence;
|
|
||||||
|
|
||||||
public interface IApplicationDbContext
|
|
||||||
{
|
|
||||||
DbSet<SemanticKnowledgeCache> SemanticKnowledgeCache { get; }
|
|
||||||
DbSet<KnowledgeUnit> KnowledgeUnits { get; }
|
|
||||||
DbSet<KnowledgeUnitLink> KnowledgeUnitLinks { get; }
|
|
||||||
DbSet<Ebook> Ebooks { get; }
|
|
||||||
DbSet<QuizResult> QuizResults { get; }
|
|
||||||
|
|
||||||
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
|
||||||
}
|
|
||||||
@@ -5,5 +5,6 @@ public record SubscriptionPlanDto
|
|||||||
public int Id { get; init; }
|
public int Id { get; init; }
|
||||||
public string Name { get; init; } = string.Empty;
|
public string Name { get; init; } = string.Empty;
|
||||||
public int AITokenLimit { get; init; }
|
public int AITokenLimit { get; init; }
|
||||||
|
public bool IsUnlimitedTokens { get; init; }
|
||||||
public decimal MonthlyPrice { get; init; }
|
public decimal MonthlyPrice { get; init; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\NexusReader.Domain\NexusReader.Domain.csproj" />
|
<ProjectReference Include="..\NexusReader.Domain\NexusReader.Domain.csproj" />
|
||||||
|
<ProjectReference Include="..\NexusReader.Data\NexusReader.Data.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@@ -13,7 +14,7 @@
|
|||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.Extensions.AI" Version="10.5.0" />
|
<PackageReference Include="Microsoft.Extensions.AI" Version="10.5.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="10.0.7" />
|
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="10.0.7" />
|
||||||
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.2.1" />
|
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ using MediatR;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using NexusReader.Application.DTOs.AI;
|
using NexusReader.Application.DTOs.AI;
|
||||||
using NexusReader.Application.Abstractions.Persistence;
|
|
||||||
|
using NexusReader.Data.Persistence;
|
||||||
using Pgvector;
|
using Pgvector;
|
||||||
using Pgvector.EntityFrameworkCore;
|
using Pgvector.EntityFrameworkCore;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -16,14 +17,14 @@ public record SearchLibrarySemanticallyQuery(string QueryText, string TenantId,
|
|||||||
|
|
||||||
public class SearchLibrarySemanticallyQueryHandler : IRequestHandler<SearchLibrarySemanticallyQuery, Result<List<SemanticSearchResultDto>>>
|
public class SearchLibrarySemanticallyQueryHandler : IRequestHandler<SearchLibrarySemanticallyQuery, Result<List<SemanticSearchResultDto>>>
|
||||||
{
|
{
|
||||||
private readonly IApplicationDbContext _dbContext;
|
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||||
private readonly IEmbeddingGenerator<string, Embedding<float>> _embeddingGenerator;
|
private readonly IEmbeddingGenerator<string, Embedding<float>> _embeddingGenerator;
|
||||||
|
|
||||||
public SearchLibrarySemanticallyQueryHandler(
|
public SearchLibrarySemanticallyQueryHandler(
|
||||||
IApplicationDbContext dbContext,
|
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||||
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator)
|
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator)
|
||||||
{
|
{
|
||||||
_dbContext = dbContext;
|
_dbContextFactory = dbContextFactory;
|
||||||
_embeddingGenerator = embeddingGenerator;
|
_embeddingGenerator = embeddingGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,6 +35,7 @@ public class SearchLibrarySemanticallyQueryHandler : IRequestHandler<SearchLibra
|
|||||||
return Result.Fail("Query text cannot be empty.");
|
return Result.Fail("Query text cannot be empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
using var dbContext = _dbContextFactory.CreateDbContext();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 1. Generate embedding for user query
|
// 1. Generate embedding for user query
|
||||||
@@ -41,7 +43,7 @@ public class SearchLibrarySemanticallyQueryHandler : IRequestHandler<SearchLibra
|
|||||||
var queryVector = new Vector(embeddingResponse.First().Vector.ToArray());
|
var queryVector = new Vector(embeddingResponse.First().Vector.ToArray());
|
||||||
|
|
||||||
// 2. Perform Cosine Similarity Search on Knowledge Units
|
// 2. Perform Cosine Similarity Search on Knowledge Units
|
||||||
var candidates = await _dbContext.KnowledgeUnits
|
var candidates = await dbContext.KnowledgeUnits
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(x => (x.TenantId == request.TenantId || x.TenantId == "global") && x.Vector != null)
|
.Where(x => (x.TenantId == request.TenantId || x.TenantId == "global") && x.Vector != null)
|
||||||
.OrderBy(x => x.Vector!.CosineDistance(queryVector))
|
.OrderBy(x => x.Vector!.CosineDistance(queryVector))
|
||||||
@@ -51,7 +53,7 @@ public class SearchLibrarySemanticallyQueryHandler : IRequestHandler<SearchLibra
|
|||||||
if (!candidates.Any())
|
if (!candidates.Any())
|
||||||
{
|
{
|
||||||
// Fallback to legacy cache if no granular units found
|
// Fallback to legacy cache if no granular units found
|
||||||
var legacyResults = await _dbContext.SemanticKnowledgeCache
|
var legacyResults = await dbContext.SemanticKnowledgeCache
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(x => x.TenantId == request.TenantId && x.Vector != null)
|
.Where(x => x.TenantId == request.TenantId && x.Vector != null)
|
||||||
.OrderBy(x => x.Vector!.CosineDistance(queryVector))
|
.OrderBy(x => x.Vector!.CosineDistance(queryVector))
|
||||||
@@ -68,13 +70,13 @@ public class SearchLibrarySemanticallyQueryHandler : IRequestHandler<SearchLibra
|
|||||||
|
|
||||||
// 3. Graph Expansion: Pull related units (e.g. Definitions, Next steps)
|
// 3. Graph Expansion: Pull related units (e.g. Definitions, Next steps)
|
||||||
var candidateIds = candidates.Select(c => c.Id).ToList();
|
var candidateIds = candidates.Select(c => c.Id).ToList();
|
||||||
var links = await _dbContext.KnowledgeUnitLinks
|
var links = await dbContext.KnowledgeUnitLinks
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(l => candidateIds.Contains(l.SourceUnitId) && (l.RelationType == "Defines" || l.RelationType == "Next"))
|
.Where(l => candidateIds.Contains(l.SourceUnitId) && (l.RelationType == "Defines" || l.RelationType == "Next"))
|
||||||
.ToListAsync(cancellationToken);
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
var relatedIds = links.Select(l => l.TargetUnitId).Distinct().ToList();
|
var relatedIds = links.Select(l => l.TargetUnitId).Distinct().ToList();
|
||||||
var relatedUnits = await _dbContext.KnowledgeUnits
|
var relatedUnits = await dbContext.KnowledgeUnits
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Where(u => relatedIds.Contains(u.Id))
|
.Where(u => relatedIds.Contains(u.Id))
|
||||||
.ToDictionaryAsync(u => u.Id, cancellationToken);
|
.ToDictionaryAsync(u => u.Id, cancellationToken);
|
||||||
|
|||||||
@@ -2,16 +2,19 @@ using System.Security.Claims;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using NexusReader.Domain.Entities;
|
using NexusReader.Domain.Entities;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
using NexusReader.Data.Persistence;
|
||||||
|
|
||||||
namespace NexusReader.Application.Security.Authorization;
|
namespace NexusReader.Application.Security.Authorization;
|
||||||
|
|
||||||
public class ProUserHandler : AuthorizationHandler<ProUserRequirement>
|
public class ProUserHandler : AuthorizationHandler<ProUserRequirement>
|
||||||
{
|
{
|
||||||
private readonly UserManager<NexusUser> _userManager;
|
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||||
|
|
||||||
public ProUserHandler(UserManager<NexusUser> userManager)
|
public ProUserHandler(IDbContextFactory<AppDbContext> dbContextFactory)
|
||||||
{
|
{
|
||||||
_userManager = userManager;
|
_dbContextFactory = dbContextFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task HandleRequirementAsync(
|
protected override async Task HandleRequirementAsync(
|
||||||
@@ -24,14 +27,18 @@ public class ProUserHandler : AuthorizationHandler<ProUserRequirement>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await _userManager.FindByIdAsync(userId);
|
using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var user = await db.Users
|
||||||
|
.Include(u => u.SubscriptionPlan)
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == userId);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rule 1: Explicit Pro plan
|
// Rule 1: Unlimited access
|
||||||
if (user.SubscriptionPlanId == SubscriptionPlan.ProId)
|
if (user.SubscriptionPlan?.IsUnlimitedTokens == true)
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+2
-2
@@ -4,12 +4,12 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Migrations
|
namespace NexusReader.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDbContext))]
|
[DbContext(typeof(AppDbContext))]
|
||||||
[Migration("20260428184727_InitialPostgres")]
|
[Migration("20260428184727_InitialPostgres")]
|
||||||
+1
-1
@@ -4,7 +4,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Migrations
|
namespace NexusReader.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class InitialPostgres : Migration
|
public partial class InitialPostgres : Migration
|
||||||
+2
-2
@@ -4,12 +4,12 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Migrations
|
namespace NexusReader.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDbContext))]
|
[DbContext(typeof(AppDbContext))]
|
||||||
[Migration("20260428185239_IncreaseHashLength")]
|
[Migration("20260428185239_IncreaseHashLength")]
|
||||||
+1
-1
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Migrations
|
namespace NexusReader.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class IncreaseHashLength : Migration
|
public partial class IncreaseHashLength : Migration
|
||||||
+2
-2
@@ -4,12 +4,12 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Migrations
|
namespace NexusReader.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDbContext))]
|
[DbContext(typeof(AppDbContext))]
|
||||||
[Migration("20260429080302_AddQuizResults")]
|
[Migration("20260429080302_AddQuizResults")]
|
||||||
+1
-1
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Migrations;
|
|||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Migrations
|
namespace NexusReader.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class AddQuizResults : Migration
|
public partial class AddQuizResults : Migration
|
||||||
+2
-2
@@ -4,13 +4,13 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Migrations;
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
using Pgvector;
|
using Pgvector;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Migrations
|
namespace NexusReader.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDbContext))]
|
[DbContext(typeof(AppDbContext))]
|
||||||
[Migration("20260503175906_FinalNormalizedSubscriptionArchitecture")]
|
[Migration("20260503175906_FinalNormalizedSubscriptionArchitecture")]
|
||||||
+1
-1
@@ -7,7 +7,7 @@ using Pgvector;
|
|||||||
|
|
||||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Migrations
|
namespace NexusReader.Data.Migrations
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public partial class FinalNormalizedSubscriptionArchitecture : Migration
|
public partial class FinalNormalizedSubscriptionArchitecture : Migration
|
||||||
Generated
+659
@@ -0,0 +1,659 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
|
using NexusReader.Data.Persistence;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
using Pgvector;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NexusReader.Data.Migrations
|
||||||
|
{
|
||||||
|
[DbContext(typeof(AppDbContext))]
|
||||||
|
[Migration("20260506184227_UpdateSubscriptionPlanIsUnlimitedTokens")]
|
||||||
|
partial class UpdateSubscriptionPlanIsUnlimitedTokens
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void BuildTargetModel(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
#pragma warning disable 612, 618
|
||||||
|
modelBuilder
|
||||||
|
.HasAnnotation("ProductVersion", "10.0.7")
|
||||||
|
.HasAnnotation("Relational:MaxIdentifierLength", 63);
|
||||||
|
|
||||||
|
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "vector");
|
||||||
|
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRole", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("RoleNameIndex");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetRoleClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("ClaimType")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ClaimValue")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserClaims", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderKey")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ProviderDisplayName")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("LoginProvider", "ProviderKey");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserLogins", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RoleId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "RoleId");
|
||||||
|
|
||||||
|
b.HasIndex("RoleId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserRoles", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("LoginProvider")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("Value")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("UserId", "LoginProvider", "Name");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUserTokens", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.Ebook", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("AddedDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Author")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<string>("CoverUrl")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("FilePath")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastReadDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("TenantId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Title")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Ebooks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnit", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Content")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("MetadataJson")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("SourceId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("TenantId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Vector>("Vector")
|
||||||
|
.HasColumnType("vector(768)");
|
||||||
|
|
||||||
|
b.Property<string>("Version")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SourceId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.ToTable("KnowledgeUnits");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnitLink", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<string>("RelationType")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("SourceUnitId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("TargetUnitId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("SourceUnitId");
|
||||||
|
|
||||||
|
b.HasIndex("TargetUnitId");
|
||||||
|
|
||||||
|
b.ToTable("KnowledgeUnitLinks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.NexusUser", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("Id")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("AITokenLimit")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("AITokensUsed")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<int>("AccessFailedCount")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("ConcurrencyStamp")
|
||||||
|
.IsConcurrencyToken()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("DisplayName")
|
||||||
|
.HasMaxLength(100)
|
||||||
|
.HasColumnType("character varying(100)");
|
||||||
|
|
||||||
|
b.Property<string>("Email")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<bool>("EmailConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastAiActionDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<DateTime?>("LastReadAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("LastReadPageId")
|
||||||
|
.HasMaxLength(255)
|
||||||
|
.HasColumnType("character varying(255)");
|
||||||
|
|
||||||
|
b.Property<bool>("LockoutEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<DateTimeOffset?>("LockoutEnd")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedEmail")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("NormalizedUserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.Property<string>("PasswordHash")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PhoneNumber")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<bool>("PhoneNumberConfirmed")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("SecurityStamp")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("SubscriptionPlanId")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer")
|
||||||
|
.HasDefaultValue(1);
|
||||||
|
|
||||||
|
b.Property<string>("TenantId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<bool>("TwoFactorEnabled")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("UserName")
|
||||||
|
.HasMaxLength(256)
|
||||||
|
.HasColumnType("character varying(256)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedEmail")
|
||||||
|
.HasDatabaseName("EmailIndex");
|
||||||
|
|
||||||
|
b.HasIndex("NormalizedUserName")
|
||||||
|
.IsUnique()
|
||||||
|
.HasDatabaseName("UserNameIndex");
|
||||||
|
|
||||||
|
b.HasIndex("SubscriptionPlanId");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.ToTable("AspNetUsers", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.QuizResult", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CompletedDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Score")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("TenantId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<string>("Topic")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<int>("TotalQuestions")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("QuizResults");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.SemanticKnowledgeCache", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("ContentHash")
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreatedAt")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("JsonData")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("ModelId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("OriginalText")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("PromptVersion")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(10)
|
||||||
|
.HasColumnType("character varying(10)");
|
||||||
|
|
||||||
|
b.Property<string>("TenantId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(128)
|
||||||
|
.HasColumnType("character varying(128)");
|
||||||
|
|
||||||
|
b.Property<Vector>("Vector")
|
||||||
|
.HasColumnType("vector(1536)");
|
||||||
|
|
||||||
|
b.HasKey("ContentHash");
|
||||||
|
|
||||||
|
b.HasIndex("ContentHash")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
|
b.ToTable("SemanticKnowledgeCache");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.SubscriptionPlan", b =>
|
||||||
|
{
|
||||||
|
b.Property<int>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
|
b.Property<int>("AITokenLimit")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUnlimitedTokens")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<decimal>("MonthlyPrice")
|
||||||
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
|
b.Property<string>("PlanName")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("StripeProductId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("PlanName")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
|
b.ToTable("SubscriptionPlans");
|
||||||
|
|
||||||
|
b.HasData(
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
AITokenLimit = 5000,
|
||||||
|
IsUnlimitedTokens = false,
|
||||||
|
MonthlyPrice = 0m,
|
||||||
|
PlanName = "Free",
|
||||||
|
StripeProductId = "prod_Free789"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 2,
|
||||||
|
AITokenLimit = 10000,
|
||||||
|
IsUnlimitedTokens = false,
|
||||||
|
MonthlyPrice = 9.99m,
|
||||||
|
PlanName = "Basic",
|
||||||
|
StripeProductId = "prod_basic_placeholder"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 3,
|
||||||
|
AITokenLimit = 50000,
|
||||||
|
IsUnlimitedTokens = false,
|
||||||
|
MonthlyPrice = 19.99m,
|
||||||
|
PlanName = "Pro",
|
||||||
|
StripeProductId = "prod_pro_placeholder"
|
||||||
|
},
|
||||||
|
new
|
||||||
|
{
|
||||||
|
Id = 4,
|
||||||
|
AITokenLimit = 1000000000,
|
||||||
|
IsUnlimitedTokens = true,
|
||||||
|
MonthlyPrice = 99.99m,
|
||||||
|
PlanName = "Enterprise",
|
||||||
|
StripeProductId = "prod_enterprise_placeholder"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NexusReader.Domain.Entities.NexusUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NexusReader.Domain.Entities.NexusUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Microsoft.AspNetCore.Identity.IdentityRole", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("RoleId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("NexusReader.Domain.Entities.NexusUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken<string>", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NexusReader.Domain.Entities.NexusUser", null)
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.Ebook", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NexusReader.Domain.Entities.NexusUser", "User")
|
||||||
|
.WithMany("Ebooks")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnitLink", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NexusReader.Domain.Entities.KnowledgeUnit", "SourceUnit")
|
||||||
|
.WithMany("OutgoingLinks")
|
||||||
|
.HasForeignKey("SourceUnitId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.HasOne("NexusReader.Domain.Entities.KnowledgeUnit", "TargetUnit")
|
||||||
|
.WithMany("IncomingLinks")
|
||||||
|
.HasForeignKey("TargetUnitId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("SourceUnit");
|
||||||
|
|
||||||
|
b.Navigation("TargetUnit");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.NexusUser", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NexusReader.Domain.Entities.SubscriptionPlan", "SubscriptionPlan")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("SubscriptionPlanId")
|
||||||
|
.OnDelete(DeleteBehavior.Restrict)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("SubscriptionPlan");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.QuizResult", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("NexusReader.Domain.Entities.NexusUser", "User")
|
||||||
|
.WithMany("QuizResults")
|
||||||
|
.HasForeignKey("UserId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("User");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnit", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("IncomingLinks");
|
||||||
|
|
||||||
|
b.Navigation("OutgoingLinks");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("NexusReader.Domain.Entities.NexusUser", b =>
|
||||||
|
{
|
||||||
|
b.Navigation("Ebooks");
|
||||||
|
|
||||||
|
b.Navigation("QuizResults");
|
||||||
|
});
|
||||||
|
#pragma warning restore 612, 618
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace NexusReader.Data.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class UpdateSubscriptionPlanIsUnlimitedTokens : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<bool>(
|
||||||
|
name: "IsUnlimitedTokens",
|
||||||
|
table: "SubscriptionPlans",
|
||||||
|
type: "boolean",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: false);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "SubscriptionPlans",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
columns: new[] { "AITokenLimit", "IsUnlimitedTokens", "StripeProductId" },
|
||||||
|
values: new object[] { 5000, false, "prod_Free789" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "SubscriptionPlans",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 2,
|
||||||
|
column: "IsUnlimitedTokens",
|
||||||
|
value: false);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "SubscriptionPlans",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 3,
|
||||||
|
column: "IsUnlimitedTokens",
|
||||||
|
value: false);
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "SubscriptionPlans",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 4,
|
||||||
|
columns: new[] { "AITokenLimit", "IsUnlimitedTokens" },
|
||||||
|
values: new object[] { 1000000000, true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "IsUnlimitedTokens",
|
||||||
|
table: "SubscriptionPlans");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "SubscriptionPlans",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 1,
|
||||||
|
columns: new[] { "AITokenLimit", "StripeProductId" },
|
||||||
|
values: new object[] { 1000, "" });
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "SubscriptionPlans",
|
||||||
|
keyColumn: "Id",
|
||||||
|
keyValue: 4,
|
||||||
|
column: "AITokenLimit",
|
||||||
|
value: 500000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+18
-11
@@ -3,13 +3,13 @@ using System;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
using Pgvector;
|
using Pgvector;
|
||||||
|
|
||||||
#nullable disable
|
#nullable disable
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Migrations
|
namespace NexusReader.Data.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(AppDbContext))]
|
[DbContext(typeof(AppDbContext))]
|
||||||
partial class AppDbContextModelSnapshot : ModelSnapshot
|
partial class AppDbContextModelSnapshot : ModelSnapshot
|
||||||
@@ -200,7 +200,7 @@ namespace NexusReader.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("Ebooks");
|
b.ToTable("Ebooks", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnit", b =>
|
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnit", b =>
|
||||||
@@ -246,7 +246,7 @@ namespace NexusReader.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasIndex("TenantId");
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
b.ToTable("KnowledgeUnits");
|
b.ToTable("KnowledgeUnits", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnitLink", b =>
|
modelBuilder.Entity("NexusReader.Domain.Entities.KnowledgeUnitLink", b =>
|
||||||
@@ -278,7 +278,7 @@ namespace NexusReader.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasIndex("TargetUnitId");
|
b.HasIndex("TargetUnitId");
|
||||||
|
|
||||||
b.ToTable("KnowledgeUnitLinks");
|
b.ToTable("KnowledgeUnitLinks", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NexusReader.Domain.Entities.NexusUser", b =>
|
modelBuilder.Entity("NexusReader.Domain.Entities.NexusUser", b =>
|
||||||
@@ -413,7 +413,7 @@ namespace NexusReader.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasIndex("UserId");
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
b.ToTable("QuizResults");
|
b.ToTable("QuizResults", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NexusReader.Domain.Entities.SemanticKnowledgeCache", b =>
|
modelBuilder.Entity("NexusReader.Domain.Entities.SemanticKnowledgeCache", b =>
|
||||||
@@ -458,7 +458,7 @@ namespace NexusReader.Infrastructure.Migrations
|
|||||||
|
|
||||||
b.HasIndex("TenantId");
|
b.HasIndex("TenantId");
|
||||||
|
|
||||||
b.ToTable("SemanticKnowledgeCache");
|
b.ToTable("SemanticKnowledgeCache", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("NexusReader.Domain.Entities.SubscriptionPlan", b =>
|
modelBuilder.Entity("NexusReader.Domain.Entities.SubscriptionPlan", b =>
|
||||||
@@ -472,6 +472,9 @@ namespace NexusReader.Infrastructure.Migrations
|
|||||||
b.Property<int>("AITokenLimit")
|
b.Property<int>("AITokenLimit")
|
||||||
.HasColumnType("integer");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<bool>("IsUnlimitedTokens")
|
||||||
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
b.Property<decimal>("MonthlyPrice")
|
b.Property<decimal>("MonthlyPrice")
|
||||||
.HasColumnType("numeric");
|
.HasColumnType("numeric");
|
||||||
|
|
||||||
@@ -490,21 +493,23 @@ namespace NexusReader.Infrastructure.Migrations
|
|||||||
b.HasIndex("PlanName")
|
b.HasIndex("PlanName")
|
||||||
.IsUnique();
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("SubscriptionPlans");
|
b.ToTable("SubscriptionPlans", (string)null);
|
||||||
|
|
||||||
b.HasData(
|
b.HasData(
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = 1,
|
Id = 1,
|
||||||
AITokenLimit = 1000,
|
AITokenLimit = 5000,
|
||||||
|
IsUnlimitedTokens = false,
|
||||||
MonthlyPrice = 0m,
|
MonthlyPrice = 0m,
|
||||||
PlanName = "Free",
|
PlanName = "Free",
|
||||||
StripeProductId = ""
|
StripeProductId = "prod_Free789"
|
||||||
},
|
},
|
||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = 2,
|
Id = 2,
|
||||||
AITokenLimit = 10000,
|
AITokenLimit = 10000,
|
||||||
|
IsUnlimitedTokens = false,
|
||||||
MonthlyPrice = 9.99m,
|
MonthlyPrice = 9.99m,
|
||||||
PlanName = "Basic",
|
PlanName = "Basic",
|
||||||
StripeProductId = "prod_basic_placeholder"
|
StripeProductId = "prod_basic_placeholder"
|
||||||
@@ -513,6 +518,7 @@ namespace NexusReader.Infrastructure.Migrations
|
|||||||
{
|
{
|
||||||
Id = 3,
|
Id = 3,
|
||||||
AITokenLimit = 50000,
|
AITokenLimit = 50000,
|
||||||
|
IsUnlimitedTokens = false,
|
||||||
MonthlyPrice = 19.99m,
|
MonthlyPrice = 19.99m,
|
||||||
PlanName = "Pro",
|
PlanName = "Pro",
|
||||||
StripeProductId = "prod_pro_placeholder"
|
StripeProductId = "prod_pro_placeholder"
|
||||||
@@ -520,7 +526,8 @@ namespace NexusReader.Infrastructure.Migrations
|
|||||||
new
|
new
|
||||||
{
|
{
|
||||||
Id = 4,
|
Id = 4,
|
||||||
AITokenLimit = 500000,
|
AITokenLimit = 1000000000,
|
||||||
|
IsUnlimitedTokens = true,
|
||||||
MonthlyPrice = 99.99m,
|
MonthlyPrice = 99.99m,
|
||||||
PlanName = "Enterprise",
|
PlanName = "Enterprise",
|
||||||
StripeProductId = "prod_enterprise_placeholder"
|
StripeProductId = "prod_enterprise_placeholder"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="10.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="10.0.7" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="10.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\NexusReader.Domain\NexusReader.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
+16
-9
@@ -1,16 +1,23 @@
|
|||||||
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NexusReader.Domain.Entities;
|
using NexusReader.Domain.Entities;
|
||||||
using NexusReader.Application.Abstractions.Persistence;
|
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Persistence;
|
|
||||||
|
|
||||||
public class AppDbContext : IdentityDbContext<NexusUser>, IApplicationDbContext
|
namespace NexusReader.Data.Persistence;
|
||||||
|
|
||||||
|
public class AppDbContext : IdentityDbContext<NexusUser>
|
||||||
{
|
{
|
||||||
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
|
||||||
|
{
|
||||||
|
base.OnConfiguring(optionsBuilder);
|
||||||
|
// Suppress the pending model changes warning to avoid runtime exceptions in some environments
|
||||||
|
optionsBuilder.ConfigureWarnings(w => w.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.RelationalEventId.PendingModelChangesWarning));
|
||||||
|
}
|
||||||
|
|
||||||
public DbSet<SemanticKnowledgeCache> SemanticKnowledgeCache => Set<SemanticKnowledgeCache>();
|
public DbSet<SemanticKnowledgeCache> SemanticKnowledgeCache => Set<SemanticKnowledgeCache>();
|
||||||
public DbSet<KnowledgeUnit> KnowledgeUnits => Set<KnowledgeUnit>();
|
public DbSet<KnowledgeUnit> KnowledgeUnits => Set<KnowledgeUnit>();
|
||||||
public DbSet<KnowledgeUnitLink> KnowledgeUnitLinks => Set<KnowledgeUnitLink>();
|
public DbSet<KnowledgeUnitLink> KnowledgeUnitLinks => Set<KnowledgeUnitLink>();
|
||||||
@@ -20,6 +27,8 @@ public class AppDbContext : IdentityDbContext<NexusUser>, IApplicationDbContext
|
|||||||
|
|
||||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
{
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
|
||||||
modelBuilder.HasPostgresExtension("vector");
|
modelBuilder.HasPostgresExtension("vector");
|
||||||
|
|
||||||
modelBuilder.Entity<NexusUser>(entity =>
|
modelBuilder.Entity<NexusUser>(entity =>
|
||||||
@@ -38,8 +47,6 @@ public class AppDbContext : IdentityDbContext<NexusUser>, IApplicationDbContext
|
|||||||
.HasDefaultValue(1);
|
.HasDefaultValue(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
base.OnModelCreating(modelBuilder);
|
|
||||||
|
|
||||||
modelBuilder.Entity<SubscriptionPlan>(entity =>
|
modelBuilder.Entity<SubscriptionPlan>(entity =>
|
||||||
{
|
{
|
||||||
entity.HasIndex(p => p.PlanName).IsUnique();
|
entity.HasIndex(p => p.PlanName).IsUnique();
|
||||||
@@ -97,10 +104,10 @@ public class AppDbContext : IdentityDbContext<NexusUser>, IApplicationDbContext
|
|||||||
|
|
||||||
// Seed Subscription Plans with deterministic IDs
|
// Seed Subscription Plans with deterministic IDs
|
||||||
modelBuilder.Entity<SubscriptionPlan>().HasData(
|
modelBuilder.Entity<SubscriptionPlan>().HasData(
|
||||||
new SubscriptionPlan { Id = 1, PlanName = SubscriptionPlan.FreeName, AITokenLimit = 1000, MonthlyPrice = 0m, StripeProductId = "" },
|
new SubscriptionPlan { Id = 1, PlanName = SubscriptionPlan.FreeName, AITokenLimit = 5000, IsUnlimitedTokens = false, MonthlyPrice = 0m, StripeProductId = "prod_Free789" },
|
||||||
new SubscriptionPlan { Id = 2, PlanName = SubscriptionPlan.BasicName, AITokenLimit = 10000, MonthlyPrice = 9.99m, StripeProductId = "prod_basic_placeholder" },
|
new SubscriptionPlan { Id = 2, PlanName = SubscriptionPlan.BasicName, AITokenLimit = 10000, IsUnlimitedTokens = false, MonthlyPrice = 9.99m, StripeProductId = "prod_basic_placeholder" },
|
||||||
new SubscriptionPlan { Id = 3, PlanName = SubscriptionPlan.ProName, AITokenLimit = 50000, MonthlyPrice = 19.99m, StripeProductId = "prod_pro_placeholder" },
|
new SubscriptionPlan { Id = 3, PlanName = SubscriptionPlan.ProName, AITokenLimit = 50000, IsUnlimitedTokens = false, MonthlyPrice = 19.99m, StripeProductId = "prod_pro_placeholder" },
|
||||||
new SubscriptionPlan { Id = 4, PlanName = SubscriptionPlan.EnterpriseName, AITokenLimit = 500000, MonthlyPrice = 99.99m, StripeProductId = "prod_enterprise_placeholder" }
|
new SubscriptionPlan { Id = 4, PlanName = SubscriptionPlan.EnterpriseName, AITokenLimit = 1000000000, IsUnlimitedTokens = true, MonthlyPrice = 99.99m, StripeProductId = "prod_enterprise_placeholder" }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -3,7 +3,7 @@ using Microsoft.EntityFrameworkCore.Design;
|
|||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Pgvector.EntityFrameworkCore;
|
using Pgvector.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Persistence;
|
namespace NexusReader.Data.Persistence;
|
||||||
|
|
||||||
public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
|
public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
|
||||||
{
|
{
|
||||||
+26
-22
@@ -7,16 +7,16 @@ using System.Threading.Tasks;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Persistence;
|
namespace NexusReader.Data.Persistence;
|
||||||
|
|
||||||
public static class DbInitializer
|
public static class DbInitializer
|
||||||
{
|
{
|
||||||
public static async Task SeedAsync(IServiceProvider serviceProvider)
|
public static async Task SeedAsync(IServiceProvider serviceProvider)
|
||||||
{
|
{
|
||||||
using var scope = serviceProvider.CreateScope();
|
using var scope = serviceProvider.CreateScope();
|
||||||
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<NexusUser>>();
|
var passwordHasher = scope.ServiceProvider.GetRequiredService<IPasswordHasher<NexusUser>>();
|
||||||
var roleManager = scope.ServiceProvider.GetRequiredService<RoleManager<IdentityRole>>();
|
var dbContextFactory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<AppDbContext>>();
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -27,9 +27,9 @@ public static class DbInitializer
|
|||||||
{
|
{
|
||||||
dbContext.SubscriptionPlans.AddRange(new List<SubscriptionPlan>
|
dbContext.SubscriptionPlans.AddRange(new List<SubscriptionPlan>
|
||||||
{
|
{
|
||||||
new SubscriptionPlan { Id = SubscriptionPlan.FreeId, PlanName = SubscriptionPlan.FreeName, AITokenLimit = 5000, MonthlyPrice = 0, StripeProductId = "prod_Free789" },
|
new SubscriptionPlan { Id = SubscriptionPlan.FreeId, PlanName = SubscriptionPlan.FreeName, AITokenLimit = 5000, IsUnlimitedTokens = false, MonthlyPrice = 0, StripeProductId = "prod_Free789" },
|
||||||
new SubscriptionPlan { Id = SubscriptionPlan.ProId, PlanName = SubscriptionPlan.ProName, AITokenLimit = 50000, MonthlyPrice = 19, StripeProductId = "prod_Pro123" },
|
new SubscriptionPlan { Id = SubscriptionPlan.ProId, PlanName = SubscriptionPlan.ProName, AITokenLimit = 50000, IsUnlimitedTokens = false, MonthlyPrice = 19, StripeProductId = "prod_Pro123" },
|
||||||
new SubscriptionPlan { Id = SubscriptionPlan.EnterpriseId, PlanName = SubscriptionPlan.EnterpriseName, AITokenLimit = 500000, MonthlyPrice = 99, StripeProductId = "prod_Enterprise456" }
|
new SubscriptionPlan { Id = SubscriptionPlan.EnterpriseId, PlanName = SubscriptionPlan.EnterpriseName, AITokenLimit = 1000000000, IsUnlimitedTokens = true, MonthlyPrice = 99, StripeProductId = "prod_Enterprise456" }
|
||||||
});
|
});
|
||||||
await dbContext.SaveChangesAsync();
|
await dbContext.SaveChangesAsync();
|
||||||
Console.WriteLine("[Seeder] Subscription plans seeded.");
|
Console.WriteLine("[Seeder] Subscription plans seeded.");
|
||||||
@@ -39,41 +39,45 @@ public static class DbInitializer
|
|||||||
string[] roleNames = { "Admin", "User" };
|
string[] roleNames = { "Admin", "User" };
|
||||||
foreach (var roleName in roleNames)
|
foreach (var roleName in roleNames)
|
||||||
{
|
{
|
||||||
var roleExist = await roleManager.RoleExistsAsync(roleName);
|
var roleExist = dbContext.Roles.Any(r => r.Name == roleName);
|
||||||
if (!roleExist)
|
if (!roleExist)
|
||||||
{
|
{
|
||||||
await roleManager.CreateAsync(new IdentityRole(roleName));
|
dbContext.Roles.Add(new IdentityRole { Name = roleName, NormalizedName = roleName.ToUpper() });
|
||||||
Console.WriteLine($"[Seeder] Created role: {roleName}");
|
Console.WriteLine($"[Seeder] Created role: {roleName}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await dbContext.SaveChangesAsync();
|
||||||
|
|
||||||
// Seed Admin User
|
// Seed Admin User
|
||||||
var adminEmail = "admin@nexus.com";
|
var adminEmail = "admin@nexus.com";
|
||||||
var adminUser = await userManager.FindByEmailAsync(adminEmail);
|
var normalizedEmail = adminEmail.ToUpper();
|
||||||
|
var adminUser = await dbContext.Users.FirstOrDefaultAsync(u => u.NormalizedEmail == normalizedEmail);
|
||||||
|
|
||||||
if (adminUser == null)
|
if (adminUser == null)
|
||||||
{
|
{
|
||||||
adminUser = new NexusUser
|
adminUser = new NexusUser
|
||||||
{
|
{
|
||||||
UserName = adminEmail,
|
UserName = adminEmail,
|
||||||
|
NormalizedUserName = normalizedEmail,
|
||||||
Email = adminEmail,
|
Email = adminEmail,
|
||||||
|
NormalizedEmail = normalizedEmail,
|
||||||
EmailConfirmed = true,
|
EmailConfirmed = true,
|
||||||
SubscriptionPlanId = SubscriptionPlan.EnterpriseId,
|
SubscriptionPlanId = SubscriptionPlan.EnterpriseId,
|
||||||
AITokenLimit = 1000000,
|
AITokenLimit = 1000000,
|
||||||
TenantId = Guid.NewGuid().ToString()
|
TenantId = Guid.NewGuid().ToString(),
|
||||||
|
SecurityStamp = Guid.NewGuid().ToString()
|
||||||
};
|
};
|
||||||
|
|
||||||
var createPowerUser = await userManager.CreateAsync(adminUser, "Admin123!");
|
adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, "Admin123!");
|
||||||
if (createPowerUser.Succeeded)
|
|
||||||
{
|
dbContext.Users.Add(adminUser);
|
||||||
await userManager.AddToRoleAsync(adminUser, "Admin");
|
await dbContext.SaveChangesAsync();
|
||||||
Console.WriteLine($"[Seeder] Admin user created successfully: {adminEmail}");
|
|
||||||
}
|
var adminRole = await dbContext.Roles.FirstAsync(r => r.Name == "Admin");
|
||||||
else
|
dbContext.UserRoles.Add(new IdentityUserRole<string> { UserId = adminUser.Id, RoleId = adminRole.Id });
|
||||||
{
|
await dbContext.SaveChangesAsync();
|
||||||
var errors = string.Join(", ", createPowerUser.Errors.Select(e => e.Description));
|
|
||||||
Console.WriteLine($"[Seeder] Failed to create admin user: {errors}");
|
Console.WriteLine($"[Seeder] Admin user created successfully: {adminEmail}");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -23,6 +23,8 @@ public class SubscriptionPlan
|
|||||||
|
|
||||||
public int AITokenLimit { get; set; }
|
public int AITokenLimit { get; set; }
|
||||||
|
|
||||||
|
public bool IsUnlimitedTokens { get; set; }
|
||||||
|
|
||||||
public decimal MonthlyPrice { get; set; }
|
public decimal MonthlyPrice { get; set; }
|
||||||
|
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ using Microsoft.EntityFrameworkCore;
|
|||||||
using Microsoft.Extensions.AI;
|
using Microsoft.Extensions.AI;
|
||||||
using GeminiDotnet;
|
using GeminiDotnet;
|
||||||
using GeminiDotnet.Extensions.AI;
|
using GeminiDotnet.Extensions.AI;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
|
|
||||||
using NexusReader.Application.Abstractions.Services;
|
using NexusReader.Application.Abstractions.Services;
|
||||||
using NexusReader.Infrastructure.Services;
|
using NexusReader.Infrastructure.Services;
|
||||||
using NexusReader.Infrastructure.Configuration;
|
using NexusReader.Infrastructure.Configuration;
|
||||||
|
|||||||
@@ -4,27 +4,28 @@ using Microsoft.AspNetCore.SignalR;
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NexusReader.Application.Commands.Sync;
|
using NexusReader.Application.Commands.Sync;
|
||||||
using NexusReader.Domain.Entities;
|
using NexusReader.Domain.Entities;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
using NexusReader.Infrastructure.RealTime;
|
using NexusReader.Infrastructure.RealTime;
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Handlers;
|
namespace NexusReader.Infrastructure.Handlers;
|
||||||
|
|
||||||
public class UpdateReadingProgressCommandHandler : IRequestHandler<UpdateReadingProgressCommand, Result>
|
public class UpdateReadingProgressCommandHandler : IRequestHandler<UpdateReadingProgressCommand, Result>
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _context;
|
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||||
private readonly IHubContext<SyncHub> _hubContext;
|
private readonly IHubContext<SyncHub> _hubContext;
|
||||||
|
|
||||||
public UpdateReadingProgressCommandHandler(
|
public UpdateReadingProgressCommandHandler(
|
||||||
AppDbContext context,
|
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||||
IHubContext<SyncHub> hubContext)
|
IHubContext<SyncHub> hubContext)
|
||||||
{
|
{
|
||||||
_context = context;
|
_dbContextFactory = dbContextFactory;
|
||||||
_hubContext = hubContext;
|
_hubContext = hubContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result> Handle(UpdateReadingProgressCommand request, CancellationToken cancellationToken)
|
public async Task<Result> Handle(UpdateReadingProgressCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
using var context = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
var user = await context.Users.FirstOrDefaultAsync(u => u.Id == request.UserId, cancellationToken);
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
return Result.Fail("User not found.");
|
return Result.Fail("User not found.");
|
||||||
@@ -34,7 +35,7 @@ public class UpdateReadingProgressCommandHandler : IRequestHandler<UpdateReading
|
|||||||
user.LastReadPageId = request.PageId;
|
user.LastReadPageId = request.PageId;
|
||||||
user.LastReadAt = now;
|
user.LastReadAt = now;
|
||||||
|
|
||||||
await _context.SaveChangesAsync(cancellationToken);
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
// Broadcast to other devices
|
// Broadcast to other devices
|
||||||
await _hubContext.Clients
|
await _hubContext.Clients
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ using System.Security.Claims;
|
|||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using NexusReader.Domain.Entities;
|
using NexusReader.Domain.Entities;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Identity;
|
namespace NexusReader.Infrastructure.Identity;
|
||||||
|
|
||||||
@@ -11,12 +12,12 @@ namespace NexusReader.Infrastructure.Identity;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public class TokenLimitHandler : AuthorizationHandler<TokenLimitRequirement>
|
public class TokenLimitHandler : AuthorizationHandler<TokenLimitRequirement>
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _dbContext;
|
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||||
private readonly UserManager<NexusUser> _userManager;
|
private readonly UserManager<NexusUser> _userManager;
|
||||||
|
|
||||||
public TokenLimitHandler(AppDbContext dbContext, UserManager<NexusUser> userManager)
|
public TokenLimitHandler(IDbContextFactory<AppDbContext> dbContextFactory, UserManager<NexusUser> userManager)
|
||||||
{
|
{
|
||||||
_dbContext = dbContext;
|
_dbContextFactory = dbContextFactory;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,14 +31,18 @@ public class TokenLimitHandler : AuthorizationHandler<TokenLimitRequirement>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var user = await _userManager.FindByIdAsync(userId);
|
using var db = _dbContextFactory.CreateDbContext();
|
||||||
|
var user = await db.Users
|
||||||
|
.Include(u => u.SubscriptionPlan)
|
||||||
|
.FirstOrDefaultAsync(u => u.Id == userId);
|
||||||
|
|
||||||
if (user == null)
|
if (user == null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user has available tokens
|
// Check if user has available tokens or unlimited plan
|
||||||
if (user.AITokensUsed < user.AITokenLimit)
|
if (user.SubscriptionPlan?.IsUnlimitedTokens == true || user.AITokensUsed < user.AITokenLimit)
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\NexusReader.Application\NexusReader.Application.csproj" />
|
<ProjectReference Include="..\NexusReader.Application\NexusReader.Application.csproj" />
|
||||||
|
<ProjectReference Include="..\NexusReader.Data\NexusReader.Data.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -5,24 +5,24 @@ using Microsoft.Extensions.Options;
|
|||||||
using NexusReader.Application.Abstractions.Services;
|
using NexusReader.Application.Abstractions.Services;
|
||||||
using NexusReader.Domain.Entities;
|
using NexusReader.Domain.Entities;
|
||||||
using NexusReader.Infrastructure.Configuration;
|
using NexusReader.Infrastructure.Configuration;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
|
|
||||||
namespace NexusReader.Infrastructure.Services;
|
namespace NexusReader.Infrastructure.Services;
|
||||||
|
|
||||||
public class BillingService : IBillingService
|
public class BillingService : IBillingService
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _dbContext;
|
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||||
private readonly UserManager<NexusUser> _userManager;
|
private readonly UserManager<NexusUser> _userManager;
|
||||||
private readonly StripeSettings _stripeSettings;
|
private readonly StripeSettings _stripeSettings;
|
||||||
private readonly ILogger<BillingService> _logger;
|
private readonly ILogger<BillingService> _logger;
|
||||||
|
|
||||||
public BillingService(
|
public BillingService(
|
||||||
AppDbContext dbContext,
|
IDbContextFactory<AppDbContext> dbContextFactory,
|
||||||
UserManager<NexusUser> userManager,
|
UserManager<NexusUser> userManager,
|
||||||
IOptions<StripeSettings> stripeSettings,
|
IOptions<StripeSettings> stripeSettings,
|
||||||
ILogger<BillingService> logger)
|
ILogger<BillingService> logger)
|
||||||
{
|
{
|
||||||
_dbContext = dbContext;
|
_dbContextFactory = dbContextFactory;
|
||||||
_userManager = userManager;
|
_userManager = userManager;
|
||||||
_stripeSettings = stripeSettings.Value;
|
_stripeSettings = stripeSettings.Value;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@@ -55,7 +55,8 @@ public class BillingService : IBillingService
|
|||||||
_logger.LogWarning("Unrecognized Stripe Product ID: {ProductId} for user {Email}. Falling back to Free tier.", stripeProductId, customerEmail);
|
_logger.LogWarning("Unrecognized Stripe Product ID: {ProductId} for user {Email}. Falling back to Free tier.", stripeProductId, customerEmail);
|
||||||
}
|
}
|
||||||
|
|
||||||
var plan = await _dbContext.SubscriptionPlans.FirstOrDefaultAsync(p => p.PlanName == targetPlanName);
|
using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
var plan = await dbContext.SubscriptionPlans.FirstOrDefaultAsync(p => p.PlanName == targetPlanName);
|
||||||
if (plan != null)
|
if (plan != null)
|
||||||
{
|
{
|
||||||
user.SubscriptionPlanId = plan.Id;
|
user.SubscriptionPlanId = plan.Id;
|
||||||
@@ -82,7 +83,8 @@ public class BillingService : IBillingService
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var freePlan = await _dbContext.SubscriptionPlans.FirstOrDefaultAsync(p => p.PlanName == SubscriptionPlan.FreeName);
|
using var dbContext = await _dbContextFactory.CreateDbContextAsync();
|
||||||
|
var freePlan = await dbContext.SubscriptionPlans.FirstOrDefaultAsync(p => p.PlanName == SubscriptionPlan.FreeName);
|
||||||
if (freePlan != null)
|
if (freePlan != null)
|
||||||
{
|
{
|
||||||
user.SubscriptionPlanId = freePlan.Id;
|
user.SubscriptionPlanId = freePlan.Id;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ using NexusReader.Application.Abstractions.Services;
|
|||||||
using NexusReader.Application.DTOs.AI;
|
using NexusReader.Application.DTOs.AI;
|
||||||
using NexusReader.Domain.Entities;
|
using NexusReader.Domain.Entities;
|
||||||
using NexusReader.Infrastructure.Helpers;
|
using NexusReader.Infrastructure.Helpers;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
using Polly;
|
using Polly;
|
||||||
using Polly.Registry;
|
using Polly.Registry;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
|
|||||||
@@ -59,6 +59,13 @@
|
|||||||
<div class="auth-error">@_errorMessage</div>
|
<div class="auth-error">@_errorMessage</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<div class="auth-options">
|
||||||
|
<label class="remember-me">
|
||||||
|
<InputCheckbox @bind-Value="_loginModel.RememberMe" />
|
||||||
|
<span>Zapamiętaj mnie</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="btn-submit-auth" disabled="@_isSubmitting">
|
<button type="submit" class="btn-submit-auth" disabled="@_isSubmitting">
|
||||||
@if (_isSubmitting)
|
@if (_isSubmitting)
|
||||||
{
|
{
|
||||||
@@ -95,7 +102,7 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var success = await IdentityService.LoginAsync(_loginModel.Email, _loginModel.Password);
|
var success = await IdentityService.LoginAsync(_loginModel.Email, _loginModel.Password, _loginModel.RememberMe);
|
||||||
if (success) NavigationManager.NavigateTo("/");
|
if (success) NavigationManager.NavigateTo("/");
|
||||||
else _errorMessage = "Nieprawidłowy e-mail lub hasło.";
|
else _errorMessage = "Nieprawidłowy e-mail lub hasło.";
|
||||||
}
|
}
|
||||||
@@ -114,5 +121,7 @@
|
|||||||
|
|
||||||
[System.ComponentModel.DataAnnotations.Required]
|
[System.ComponentModel.DataAnnotations.Required]
|
||||||
public string Password { get; set; } = string.Empty;
|
public string Password { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public bool RememberMe { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace NexusReader.UI.Shared.Services;
|
|||||||
public interface IIdentityService
|
public interface IIdentityService
|
||||||
{
|
{
|
||||||
Task<bool> RegisterAsync(string email, string password);
|
Task<bool> RegisterAsync(string email, string password);
|
||||||
Task<bool> LoginAsync(string email, string password);
|
Task<bool> LoginAsync(string email, string password, bool rememberMe = false);
|
||||||
Task LogoutAsync();
|
Task LogoutAsync();
|
||||||
Task<UserProfile?> GetProfileAsync();
|
Task<UserProfile?> GetProfileAsync();
|
||||||
Task<bool> RefreshTokenAsync();
|
Task<bool> RefreshTokenAsync();
|
||||||
@@ -45,7 +45,7 @@ public class IdentityService : IIdentityService
|
|||||||
return response.IsSuccessStatusCode;
|
return response.IsSuccessStatusCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> LoginAsync(string email, string password)
|
public async Task<bool> LoginAsync(string email, string password, bool rememberMe = false)
|
||||||
{
|
{
|
||||||
var response = await _httpClient.PostAsJsonAsync("identity/login", new { email, password });
|
var response = await _httpClient.PostAsJsonAsync("identity/login", new { email, password });
|
||||||
|
|
||||||
@@ -59,7 +59,22 @@ public class IdentityService : IIdentityService
|
|||||||
{
|
{
|
||||||
await _storageService.SaveSecureString(RefreshTokenKey, result.RefreshToken);
|
await _storageService.SaveSecureString(RefreshTokenKey, result.RefreshToken);
|
||||||
}
|
}
|
||||||
_authStateProvider.NotifyUserAuthentication(result.AccessToken);
|
|
||||||
|
// Option A: Fetch profile to get claims
|
||||||
|
var profile = await GetProfileAsync();
|
||||||
|
if (profile != null)
|
||||||
|
{
|
||||||
|
await _storageService.SaveSecureString("nexus_user_email", profile.Email);
|
||||||
|
await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString());
|
||||||
|
|
||||||
|
_authStateProvider.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Fallback if profile fetch fails
|
||||||
|
_authStateProvider.NotifyUserAuthentication(email, "unknown");
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +86,8 @@ public class IdentityService : IIdentityService
|
|||||||
{
|
{
|
||||||
_storageService.RemoveSecure(TokenKey);
|
_storageService.RemoveSecure(TokenKey);
|
||||||
_storageService.RemoveSecure(RefreshTokenKey);
|
_storageService.RemoveSecure(RefreshTokenKey);
|
||||||
|
_storageService.RemoveSecure("nexus_user_email");
|
||||||
|
_storageService.RemoveSecure("nexus_user_tenant");
|
||||||
_authStateProvider.NotifyUserLogout();
|
_authStateProvider.NotifyUserLogout();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +122,15 @@ public class IdentityService : IIdentityService
|
|||||||
{
|
{
|
||||||
await _storageService.SaveSecureString(RefreshTokenKey, loginResult.RefreshToken);
|
await _storageService.SaveSecureString(RefreshTokenKey, loginResult.RefreshToken);
|
||||||
}
|
}
|
||||||
_authStateProvider.NotifyUserAuthentication(loginResult.AccessToken);
|
|
||||||
|
var profile = await GetProfileAsync();
|
||||||
|
if (profile != null)
|
||||||
|
{
|
||||||
|
await _storageService.SaveSecureString("nexus_user_email", profile.Email);
|
||||||
|
await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString());
|
||||||
|
_authStateProvider.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,15 +19,30 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await _storageService.GetSecureString(TokenKey);
|
var tokenResult = await _storageService.GetSecureString(TokenKey);
|
||||||
var token = result.IsSuccess ? result.Value : null;
|
var token = tokenResult.IsSuccess ? tokenResult.Value : null;
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(token))
|
if (string.IsNullOrWhiteSpace(token))
|
||||||
{
|
{
|
||||||
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
|
||||||
}
|
}
|
||||||
|
|
||||||
var identity = new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt");
|
// For opaque tokens, we read the user info that was stored during login
|
||||||
|
var emailResult = await _storageService.GetSecureString("nexus_user_email");
|
||||||
|
var tenantIdResult = await _storageService.GetSecureString("nexus_user_tenant");
|
||||||
|
|
||||||
|
var claims = new List<Claim>();
|
||||||
|
if (emailResult.IsSuccess && !string.IsNullOrEmpty(emailResult.Value))
|
||||||
|
{
|
||||||
|
claims.Add(new Claim(ClaimTypes.Name, emailResult.Value));
|
||||||
|
claims.Add(new Claim(ClaimTypes.Email, emailResult.Value));
|
||||||
|
}
|
||||||
|
if (tenantIdResult.IsSuccess && !string.IsNullOrEmpty(tenantIdResult.Value))
|
||||||
|
{
|
||||||
|
claims.Add(new Claim("TenantId", tenantIdResult.Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(claims, "OpaqueBearer");
|
||||||
var user = new ClaimsPrincipal(identity);
|
var user = new ClaimsPrincipal(identity);
|
||||||
|
|
||||||
return new AuthenticationState(user);
|
return new AuthenticationState(user);
|
||||||
@@ -38,9 +53,16 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void NotifyUserAuthentication(string token)
|
public void NotifyUserAuthentication(string email, string tenantId)
|
||||||
{
|
{
|
||||||
var identity = new ClaimsIdentity(ParseClaimsFromJwt(token), "jwt");
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.Name, email),
|
||||||
|
new Claim(ClaimTypes.Email, email),
|
||||||
|
new Claim("TenantId", tenantId)
|
||||||
|
};
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(claims, "OpaqueBearer");
|
||||||
var user = new ClaimsPrincipal(identity);
|
var user = new ClaimsPrincipal(identity);
|
||||||
var authState = Task.FromResult(new AuthenticationState(user));
|
var authState = Task.FromResult(new AuthenticationState(user));
|
||||||
NotifyAuthenticationStateChanged(authState);
|
NotifyAuthenticationStateChanged(authState);
|
||||||
@@ -52,30 +74,4 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
|
|||||||
var authState = Task.FromResult(new AuthenticationState(guest));
|
var authState = Task.FromResult(new AuthenticationState(guest));
|
||||||
NotifyAuthenticationStateChanged(authState);
|
NotifyAuthenticationStateChanged(authState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<Claim> ParseClaimsFromJwt(string jwt)
|
|
||||||
{
|
|
||||||
var claims = new List<Claim>();
|
|
||||||
var payload = jwt.Split('.')[1];
|
|
||||||
|
|
||||||
var jsonBytes = ParseBase64WithoutPadding(payload);
|
|
||||||
var keyValuePairs = JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
|
|
||||||
|
|
||||||
if (keyValuePairs != null)
|
|
||||||
{
|
|
||||||
claims.AddRange(keyValuePairs.Select(kvp => new Claim(kvp.Key, kvp.Value.ToString() ?? string.Empty)));
|
|
||||||
}
|
|
||||||
|
|
||||||
return claims;
|
|
||||||
}
|
|
||||||
|
|
||||||
private byte[] ParseBase64WithoutPadding(string base64)
|
|
||||||
{
|
|
||||||
switch (base64.Length % 4)
|
|
||||||
{
|
|
||||||
case 2: base64 += "=="; break;
|
|
||||||
case 3: base64 += "="; break;
|
|
||||||
}
|
|
||||||
return Convert.FromBase64String(base64);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,28 @@
|
|||||||
|
|
||||||
.toggle-visibility { position: absolute; right: 16px; background: none; border: none; color: var(--nexus-text-muted); cursor: pointer; padding: 4px; z-index: 5; }
|
.toggle-visibility { position: absolute; right: 16px; background: none; border: none; color: var(--nexus-text-muted); cursor: pointer; padding: 4px; z-index: 5; }
|
||||||
|
|
||||||
|
.auth-options {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-me {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--nexus-text-muted);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.remember-me input {
|
||||||
|
accent-color: var(--nexus-primary);
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
.btn-submit-auth {
|
.btn-submit-auth {
|
||||||
width: 100%; padding: 14px; background: var(--nexus-primary); border: none; border-radius: 12px;
|
width: 100%; padding: 14px; background: var(--nexus-primary); border: none; border-radius: 12px;
|
||||||
color: #000; font-size: 0.95rem; font-weight: 700; cursor: pointer; transition: all 0.2s;
|
color: #000; font-size: 0.95rem; font-weight: 700; cursor: pointer; transition: all 0.2s;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ using NexusReader.Application.DTOs.User;
|
|||||||
using NexusReader.Web.Client.Services;
|
using NexusReader.Web.Client.Services;
|
||||||
using NexusReader.UI.Shared.Services;
|
using NexusReader.UI.Shared.Services;
|
||||||
using NexusReader.Domain.Entities;
|
using NexusReader.Domain.Entities;
|
||||||
using NexusReader.Infrastructure.Persistence;
|
using NexusReader.Data.Persistence;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
using Microsoft.AspNetCore.Components.Authorization;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
@@ -135,7 +135,8 @@ using (var scope = app.Services.CreateScope())
|
|||||||
{
|
{
|
||||||
var services = scope.ServiceProvider;
|
var services = scope.ServiceProvider;
|
||||||
var logger = services.GetRequiredService<ILogger<Program>>();
|
var logger = services.GetRequiredService<ILogger<Program>>();
|
||||||
var dbContext = services.GetRequiredService<NexusReader.Infrastructure.Persistence.AppDbContext>();
|
var dbContextFactory = services.GetRequiredService<IDbContextFactory<NexusReader.Data.Persistence.AppDbContext>>();
|
||||||
|
using var dbContext = await dbContextFactory.CreateDbContextAsync();
|
||||||
|
|
||||||
int maxRetries = 5;
|
int maxRetries = 5;
|
||||||
int delayMs = 2000;
|
int delayMs = 2000;
|
||||||
|
|||||||
Reference in New Issue
Block a user