refactor: normalize Author entity, eliminate magic strings, and improve exception handling per PR review

This commit is contained in:
2026-05-10 17:07:09 +02:00
parent fc68ee41ab
commit 10dc511f2a
12 changed files with 86 additions and 25 deletions
@@ -0,0 +1,7 @@
namespace NexusReader.Application.DTOs.User;
public record AuthorDto
{
public int Id { get; init; }
public string Name { get; init; } = string.Empty;
}
@@ -23,7 +23,7 @@ public record LastReadBookDto
{ {
public Guid Id { get; init; } public Guid Id { get; init; }
public string Title { get; init; } = string.Empty; public string Title { get; init; } = string.Empty;
public string Author { get; init; } = string.Empty; public AuthorDto Author { get; init; } = new();
public string? CoverUrl { get; init; } public string? CoverUrl { get; init; }
public double Progress { get; init; } public double Progress { get; init; }
public string? LastChapter { get; init; } public string? LastChapter { get; init; }
@@ -24,6 +24,7 @@ public class AppDbContext : IdentityDbContext<NexusUser>
public DbSet<Ebook> Ebooks => Set<Ebook>(); public DbSet<Ebook> Ebooks => Set<Ebook>();
public DbSet<QuizResult> QuizResults => Set<QuizResult>(); public DbSet<QuizResult> QuizResults => Set<QuizResult>();
public DbSet<SubscriptionPlan> SubscriptionPlans => Set<SubscriptionPlan>(); public DbSet<SubscriptionPlan> SubscriptionPlans => Set<SubscriptionPlan>();
public DbSet<Author> Authors => Set<Author>();
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
@@ -89,6 +90,11 @@ public class AppDbContext : IdentityDbContext<NexusUser>
.HasForeignKey(e => e.UserId) .HasForeignKey(e => e.UserId)
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
entity.HasOne(e => e.Author)
.WithMany(a => a.Ebooks)
.HasForeignKey(e => e.AuthorId)
.OnDelete(DeleteBehavior.Restrict);
entity.HasIndex(e => e.TenantId); entity.HasIndex(e => e.TenantId);
}); });
+15
View File
@@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace NexusReader.Domain.Entities;
public class Author
{
[Key]
public int Id { get; set; }
[Required]
[MaxLength(255)]
public string Name { get; set; } = string.Empty;
public virtual ICollection<Ebook> Ebooks { get; set; } = new List<Ebook>();
}
+5 -2
View File
@@ -15,8 +15,11 @@ public class Ebook
[MaxLength(255)] [MaxLength(255)]
public string Title { get; set; } = string.Empty; public string Title { get; set; } = string.Empty;
[MaxLength(255)] [Required]
public string Author { get; set; } = "Unknown"; public int AuthorId { get; set; }
[ForeignKey(nameof(AuthorId))]
public virtual Author Author { get; set; } = null!;
[Required] [Required]
public string FilePath { get; set; } = string.Empty; public string FilePath { get; set; } = string.Empty;
@@ -0,0 +1,8 @@
namespace NexusReader.UI.Shared.Constants;
public static class PlanConstants
{
public const string DefaultPlanName = "Free";
public const int DefaultTokenLimit = 1000;
public const string DefaultActivityLabel = "Brak aktywności";
}
@@ -0,0 +1,9 @@
namespace NexusReader.UI.Shared.Constants;
public static class StorageKeys
{
public const string AuthToken = "nexus_auth_token";
public const string RefreshToken = "nexus_refresh_token";
public const string UserEmail = "nexus_user_email";
public const string UserTenant = "nexus_user_tenant";
}
@@ -60,7 +60,7 @@
<div class="progress-bubble">@(_profile.LastReadBook.Progress)%</div> <div class="progress-bubble">@(_profile.LastReadBook.Progress)%</div>
</div> </div>
</div> </div>
<span class="progress-detail">Postęp: @(_profile.LastReadBook.Progress)% - @_profile.LastReadBook.Author</span> <span class="progress-detail">Postęp: @(_profile.LastReadBook.Progress)% - @_profile.LastReadBook.Author.Name</span>
</div> </div>
<p class="reading-desc"> <p class="reading-desc">
Kontynuuj odkrywanie wiedzy w książce "@_profile.LastReadBook.Title". Kontynuuj odkrywanie wiedzy w książce "@_profile.LastReadBook.Title".
@@ -2,6 +2,7 @@ using System.Net.Http.Json;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using NexusReader.Application.Abstractions.Services; using NexusReader.Application.Abstractions.Services;
using NexusReader.Application.DTOs.User; using NexusReader.Application.DTOs.User;
using NexusReader.UI.Shared.Constants;
using FluentResults; using FluentResults;
namespace NexusReader.UI.Shared.Services; namespace NexusReader.UI.Shared.Services;
@@ -25,9 +26,9 @@ public record UserProfile(
LastReadBookDto? LastReadBook) LastReadBookDto? LastReadBook)
{ {
// Helper properties for UI compatibility // Helper properties for UI compatibility
public string CurrentPlan => Plan?.Name ?? "Standard"; public string CurrentPlan => Plan?.Name ?? PlanConstants.DefaultPlanName;
public int AITokenLimit => Plan?.AITokenLimit ?? 1000; public int AITokenLimit => Plan?.AITokenLimit ?? PlanConstants.DefaultTokenLimit;
public string LastReadBookTitle => LastReadBook?.Title ?? "Brak aktywności"; public string LastReadBookTitle => LastReadBook?.Title ?? PlanConstants.DefaultActivityLabel;
} }
public class IdentityService : IIdentityService public class IdentityService : IIdentityService
@@ -35,8 +36,8 @@ public class IdentityService : IIdentityService
private readonly HttpClient _httpClient; private readonly HttpClient _httpClient;
private readonly INativeStorageService _storageService; private readonly INativeStorageService _storageService;
private readonly AuthenticationStateProvider? _authStateProvider; private readonly AuthenticationStateProvider? _authStateProvider;
private const string TokenKey = "nexus_auth_token"; private const string TokenKey = StorageKeys.AuthToken;
private const string RefreshTokenKey = "nexus_refresh_token"; private const string RefreshTokenKey = StorageKeys.RefreshToken;
private Task<UserProfile?>? _profileTask; private Task<UserProfile?>? _profileTask;
private UserProfile? _cachedProfile; private UserProfile? _cachedProfile;
private DateTime _lastFetchAttempt = DateTime.MinValue; private DateTime _lastFetchAttempt = DateTime.MinValue;
@@ -80,7 +81,10 @@ public class IdentityService : IIdentityService
{ {
result = await response.Content.ReadFromJsonAsync<LoginResponse>(); result = await response.Content.ReadFromJsonAsync<LoginResponse>();
} }
catch (System.Text.Json.JsonException) { } catch (System.Text.Json.JsonException ex)
{
return Result.Fail(new Error("Błąd przetwarzania odpowiedzi serwera.").CausedBy(ex));
}
if (result != null && !string.IsNullOrEmpty(result.AccessToken)) if (result != null && !string.IsNullOrEmpty(result.AccessToken))
{ {
@@ -98,8 +102,8 @@ public class IdentityService : IIdentityService
if (profileResult.IsSuccess) if (profileResult.IsSuccess)
{ {
var profile = profileResult.Value; var profile = profileResult.Value;
await _storageService.SaveSecureString("nexus_user_email", profile.Email); await _storageService.SaveSecureString(StorageKeys.UserEmail, profile.Email);
await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); await _storageService.SaveSecureString(StorageKeys.UserTenant, profile.TenantId.ToString());
(_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString());
} }
else else
@@ -126,8 +130,8 @@ public class IdentityService : IIdentityService
{ {
await _storageService.SaveSecureString(TokenKey, ""); await _storageService.SaveSecureString(TokenKey, "");
await _storageService.SaveSecureString(RefreshTokenKey, ""); await _storageService.SaveSecureString(RefreshTokenKey, "");
await _storageService.SaveSecureString("nexus_user_email", ""); await _storageService.SaveSecureString(StorageKeys.UserEmail, "");
await _storageService.SaveSecureString("nexus_user_tenant", ""); await _storageService.SaveSecureString(StorageKeys.UserTenant, "");
} }
if (OnStateInvalidated != null) await OnStateInvalidated.Invoke(); if (OnStateInvalidated != null) await OnStateInvalidated.Invoke();
@@ -191,8 +195,8 @@ public class IdentityService : IIdentityService
if (profile != null) if (profile != null)
{ {
_cachedProfile = profile; _cachedProfile = profile;
await _storageService.SaveSecureString("nexus_user_email", profile.Email); await _storageService.SaveSecureString(StorageKeys.UserEmail, profile.Email);
await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); await _storageService.SaveSecureString(StorageKeys.UserTenant, profile.TenantId.ToString());
(_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString());
} }
return profile; return profile;
@@ -240,8 +244,8 @@ public class IdentityService : IIdentityService
if (profileResult.IsSuccess) if (profileResult.IsSuccess)
{ {
var profile = profileResult.Value; var profile = profileResult.Value;
await _storageService.SaveSecureString("nexus_user_email", profile.Email); await _storageService.SaveSecureString(StorageKeys.UserEmail, profile.Email);
await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); await _storageService.SaveSecureString(StorageKeys.UserTenant, profile.TenantId.ToString());
(_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString());
} }
@@ -2,13 +2,14 @@ using System.Security.Claims;
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.Authorization;
using NexusReader.Application.Abstractions.Services; using NexusReader.Application.Abstractions.Services;
using NexusReader.UI.Shared.Constants;
namespace NexusReader.UI.Shared.Services; namespace NexusReader.UI.Shared.Services;
public class NexusAuthenticationStateProvider : AuthenticationStateProvider public class NexusAuthenticationStateProvider : AuthenticationStateProvider
{ {
private readonly INativeStorageService _storageService; private readonly INativeStorageService _storageService;
private const string TokenKey = "nexus_auth_token"; private const string TokenKey = StorageKeys.AuthToken;
public NexusAuthenticationStateProvider(INativeStorageService storageService) public NexusAuthenticationStateProvider(INativeStorageService storageService)
{ {
@@ -35,8 +36,8 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
// 1. Try Token-based auth // 1. Try Token-based auth
if (!string.IsNullOrWhiteSpace(token)) if (!string.IsNullOrWhiteSpace(token))
{ {
var emailResult = await _storageService.GetSecureString("nexus_user_email"); var emailResult = await _storageService.GetSecureString(StorageKeys.UserEmail);
var tenantIdResult = await _storageService.GetSecureString("nexus_user_tenant"); var tenantIdResult = await _storageService.GetSecureString(StorageKeys.UserTenant);
if (emailResult.IsSuccess && !string.IsNullOrEmpty(emailResult.Value)) if (emailResult.IsSuccess && !string.IsNullOrEmpty(emailResult.Value))
{ {
@@ -46,10 +47,10 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider
} }
// 2. Try Cookie-based auth indicators // 2. Try Cookie-based auth indicators
var storedEmailResult = await _storageService.GetSecureString("nexus_user_email"); var storedEmailResult = await _storageService.GetSecureString(StorageKeys.UserEmail);
if (storedEmailResult.IsSuccess && !string.IsNullOrEmpty(storedEmailResult.Value)) if (storedEmailResult.IsSuccess && !string.IsNullOrEmpty(storedEmailResult.Value))
{ {
var tenantIdResult = await _storageService.GetSecureString("nexus_user_tenant"); var tenantIdResult = await _storageService.GetSecureString(StorageKeys.UserTenant);
_cachedState = CreateState(storedEmailResult.Value, tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown", "CookieAuth"); _cachedState = CreateState(storedEmailResult.Value, tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown", "CookieAuth");
return _cachedState; return _cachedState;
} }
+5 -1
View File
@@ -458,7 +458,11 @@ app.MapGet("/identity/profile", async (ClaimsPrincipal user, UserManager<NexusUs
{ {
Id = e.Id, Id = e.Id,
Title = e.Title, Title = e.Title,
Author = e.Author, Author = new AuthorDto
{
Id = e.Author.Id,
Name = e.Author.Name
},
CoverUrl = e.CoverUrl, CoverUrl = e.CoverUrl,
Progress = 65, Progress = 65,
LastChapter = "Chapter 4: Renaissance in Italy" LastChapter = "Chapter 4: Renaissance in Italy"
@@ -68,7 +68,11 @@ public class ServerIdentityService : IIdentityService
{ {
Id = e.Id, Id = e.Id,
Title = e.Title, Title = e.Title,
Author = e.Author, Author = new AuthorDto
{
Id = e.Author.Id,
Name = e.Author.Name
},
CoverUrl = e.CoverUrl, CoverUrl = e.CoverUrl,
Progress = 65, // Hardcoded for now as per design requirements, will link to real segments later Progress = 65, // Hardcoded for now as per design requirements, will link to real segments later
LastChapter = "Chapter 4: Renaissance in Italy" LastChapter = "Chapter 4: Renaissance in Italy"