From 10dc511f2a9493547e4453da130bef78e0f5d18c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Sun, 10 May 2026 17:07:09 +0200 Subject: [PATCH] refactor: normalize Author entity, eliminate magic strings, and improve exception handling per PR review --- .../DTOs/User/AuthorDto.cs | 7 ++++ .../DTOs/User/UserProfileDto.cs | 2 +- .../Persistence/AppDbContext.cs | 6 ++++ src/NexusReader.Domain/Entities/Author.cs | 15 +++++++++ src/NexusReader.Domain/Entities/Ebook.cs | 7 ++-- .../Constants/PlanConstants.cs | 8 +++++ .../Constants/StorageKeys.cs | 9 ++++++ .../Pages/Dashboard.razor | 2 +- .../Services/IdentityService.cs | 32 +++++++++++-------- .../NexusAuthenticationStateProvider.cs | 11 ++++--- src/NexusReader.Web.New/Program.cs | 6 +++- .../Services/ServerIdentityService.cs | 6 +++- 12 files changed, 86 insertions(+), 25 deletions(-) create mode 100644 src/NexusReader.Application/DTOs/User/AuthorDto.cs create mode 100644 src/NexusReader.Domain/Entities/Author.cs create mode 100644 src/NexusReader.UI.Shared/Constants/PlanConstants.cs create mode 100644 src/NexusReader.UI.Shared/Constants/StorageKeys.cs diff --git a/src/NexusReader.Application/DTOs/User/AuthorDto.cs b/src/NexusReader.Application/DTOs/User/AuthorDto.cs new file mode 100644 index 0000000..8c3a3fc --- /dev/null +++ b/src/NexusReader.Application/DTOs/User/AuthorDto.cs @@ -0,0 +1,7 @@ +namespace NexusReader.Application.DTOs.User; + +public record AuthorDto +{ + public int Id { get; init; } + public string Name { get; init; } = string.Empty; +} diff --git a/src/NexusReader.Application/DTOs/User/UserProfileDto.cs b/src/NexusReader.Application/DTOs/User/UserProfileDto.cs index 3a38ce3..08c99e6 100644 --- a/src/NexusReader.Application/DTOs/User/UserProfileDto.cs +++ b/src/NexusReader.Application/DTOs/User/UserProfileDto.cs @@ -23,7 +23,7 @@ public record LastReadBookDto { public Guid Id { get; init; } 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 double Progress { get; init; } public string? LastChapter { get; init; } diff --git a/src/NexusReader.Data/Persistence/AppDbContext.cs b/src/NexusReader.Data/Persistence/AppDbContext.cs index 16ed15e..03f8cf8 100644 --- a/src/NexusReader.Data/Persistence/AppDbContext.cs +++ b/src/NexusReader.Data/Persistence/AppDbContext.cs @@ -24,6 +24,7 @@ public class AppDbContext : IdentityDbContext public DbSet Ebooks => Set(); public DbSet QuizResults => Set(); public DbSet SubscriptionPlans => Set(); + public DbSet Authors => Set(); protected override void OnModelCreating(ModelBuilder modelBuilder) { @@ -88,6 +89,11 @@ public class AppDbContext : IdentityDbContext .WithMany(u => u.Ebooks) .HasForeignKey(e => e.UserId) .OnDelete(DeleteBehavior.Cascade); + + entity.HasOne(e => e.Author) + .WithMany(a => a.Ebooks) + .HasForeignKey(e => e.AuthorId) + .OnDelete(DeleteBehavior.Restrict); entity.HasIndex(e => e.TenantId); }); diff --git a/src/NexusReader.Domain/Entities/Author.cs b/src/NexusReader.Domain/Entities/Author.cs new file mode 100644 index 0000000..5f15836 --- /dev/null +++ b/src/NexusReader.Domain/Entities/Author.cs @@ -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 Ebooks { get; set; } = new List(); +} diff --git a/src/NexusReader.Domain/Entities/Ebook.cs b/src/NexusReader.Domain/Entities/Ebook.cs index 582f23b..cc12e43 100644 --- a/src/NexusReader.Domain/Entities/Ebook.cs +++ b/src/NexusReader.Domain/Entities/Ebook.cs @@ -15,8 +15,11 @@ public class Ebook [MaxLength(255)] public string Title { get; set; } = string.Empty; - [MaxLength(255)] - public string Author { get; set; } = "Unknown"; + [Required] + public int AuthorId { get; set; } + + [ForeignKey(nameof(AuthorId))] + public virtual Author Author { get; set; } = null!; [Required] public string FilePath { get; set; } = string.Empty; diff --git a/src/NexusReader.UI.Shared/Constants/PlanConstants.cs b/src/NexusReader.UI.Shared/Constants/PlanConstants.cs new file mode 100644 index 0000000..dde7e7f --- /dev/null +++ b/src/NexusReader.UI.Shared/Constants/PlanConstants.cs @@ -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"; +} diff --git a/src/NexusReader.UI.Shared/Constants/StorageKeys.cs b/src/NexusReader.UI.Shared/Constants/StorageKeys.cs new file mode 100644 index 0000000..e5e1fc2 --- /dev/null +++ b/src/NexusReader.UI.Shared/Constants/StorageKeys.cs @@ -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"; +} diff --git a/src/NexusReader.UI.Shared/Pages/Dashboard.razor b/src/NexusReader.UI.Shared/Pages/Dashboard.razor index 45e99de..ebbe001 100644 --- a/src/NexusReader.UI.Shared/Pages/Dashboard.razor +++ b/src/NexusReader.UI.Shared/Pages/Dashboard.razor @@ -60,7 +60,7 @@
@(_profile.LastReadBook.Progress)%
- Postęp: @(_profile.LastReadBook.Progress)% - @_profile.LastReadBook.Author + Postęp: @(_profile.LastReadBook.Progress)% - @_profile.LastReadBook.Author.Name

Kontynuuj odkrywanie wiedzy w książce "@_profile.LastReadBook.Title". diff --git a/src/NexusReader.UI.Shared/Services/IdentityService.cs b/src/NexusReader.UI.Shared/Services/IdentityService.cs index 11d7e10..d2ade88 100644 --- a/src/NexusReader.UI.Shared/Services/IdentityService.cs +++ b/src/NexusReader.UI.Shared/Services/IdentityService.cs @@ -2,6 +2,7 @@ using System.Net.Http.Json; using Microsoft.AspNetCore.Components.Authorization; using NexusReader.Application.Abstractions.Services; using NexusReader.Application.DTOs.User; +using NexusReader.UI.Shared.Constants; using FluentResults; namespace NexusReader.UI.Shared.Services; @@ -25,9 +26,9 @@ public record UserProfile( LastReadBookDto? LastReadBook) { // Helper properties for UI compatibility - public string CurrentPlan => Plan?.Name ?? "Standard"; - public int AITokenLimit => Plan?.AITokenLimit ?? 1000; - public string LastReadBookTitle => LastReadBook?.Title ?? "Brak aktywności"; + public string CurrentPlan => Plan?.Name ?? PlanConstants.DefaultPlanName; + public int AITokenLimit => Plan?.AITokenLimit ?? PlanConstants.DefaultTokenLimit; + public string LastReadBookTitle => LastReadBook?.Title ?? PlanConstants.DefaultActivityLabel; } public class IdentityService : IIdentityService @@ -35,8 +36,8 @@ public class IdentityService : IIdentityService private readonly HttpClient _httpClient; private readonly INativeStorageService _storageService; private readonly AuthenticationStateProvider? _authStateProvider; - private const string TokenKey = "nexus_auth_token"; - private const string RefreshTokenKey = "nexus_refresh_token"; + private const string TokenKey = StorageKeys.AuthToken; + private const string RefreshTokenKey = StorageKeys.RefreshToken; private Task? _profileTask; private UserProfile? _cachedProfile; private DateTime _lastFetchAttempt = DateTime.MinValue; @@ -80,7 +81,10 @@ public class IdentityService : IIdentityService { result = await response.Content.ReadFromJsonAsync(); } - 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)) { @@ -98,8 +102,8 @@ public class IdentityService : IIdentityService if (profileResult.IsSuccess) { var profile = profileResult.Value; - await _storageService.SaveSecureString("nexus_user_email", profile.Email); - await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); + await _storageService.SaveSecureString(StorageKeys.UserEmail, profile.Email); + await _storageService.SaveSecureString(StorageKeys.UserTenant, profile.TenantId.ToString()); (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); } else @@ -126,8 +130,8 @@ public class IdentityService : IIdentityService { await _storageService.SaveSecureString(TokenKey, ""); await _storageService.SaveSecureString(RefreshTokenKey, ""); - await _storageService.SaveSecureString("nexus_user_email", ""); - await _storageService.SaveSecureString("nexus_user_tenant", ""); + await _storageService.SaveSecureString(StorageKeys.UserEmail, ""); + await _storageService.SaveSecureString(StorageKeys.UserTenant, ""); } if (OnStateInvalidated != null) await OnStateInvalidated.Invoke(); @@ -191,8 +195,8 @@ public class IdentityService : IIdentityService if (profile != null) { _cachedProfile = profile; - await _storageService.SaveSecureString("nexus_user_email", profile.Email); - await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); + await _storageService.SaveSecureString(StorageKeys.UserEmail, profile.Email); + await _storageService.SaveSecureString(StorageKeys.UserTenant, profile.TenantId.ToString()); (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); } return profile; @@ -240,8 +244,8 @@ public class IdentityService : IIdentityService if (profileResult.IsSuccess) { var profile = profileResult.Value; - await _storageService.SaveSecureString("nexus_user_email", profile.Email); - await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); + await _storageService.SaveSecureString(StorageKeys.UserEmail, profile.Email); + await _storageService.SaveSecureString(StorageKeys.UserTenant, profile.TenantId.ToString()); (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); } diff --git a/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs b/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs index 75f2daa..42e5218 100644 --- a/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs +++ b/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs @@ -2,13 +2,14 @@ using System.Security.Claims; using System.Text.Json; using Microsoft.AspNetCore.Components.Authorization; using NexusReader.Application.Abstractions.Services; +using NexusReader.UI.Shared.Constants; namespace NexusReader.UI.Shared.Services; public class NexusAuthenticationStateProvider : AuthenticationStateProvider { private readonly INativeStorageService _storageService; - private const string TokenKey = "nexus_auth_token"; + private const string TokenKey = StorageKeys.AuthToken; public NexusAuthenticationStateProvider(INativeStorageService storageService) { @@ -35,8 +36,8 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider // 1. Try Token-based auth if (!string.IsNullOrWhiteSpace(token)) { - var emailResult = await _storageService.GetSecureString("nexus_user_email"); - var tenantIdResult = await _storageService.GetSecureString("nexus_user_tenant"); + var emailResult = await _storageService.GetSecureString(StorageKeys.UserEmail); + var tenantIdResult = await _storageService.GetSecureString(StorageKeys.UserTenant); if (emailResult.IsSuccess && !string.IsNullOrEmpty(emailResult.Value)) { @@ -46,10 +47,10 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider } // 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)) { - 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"); return _cachedState; } diff --git a/src/NexusReader.Web.New/Program.cs b/src/NexusReader.Web.New/Program.cs index ff87c10..4b2dd37 100644 --- a/src/NexusReader.Web.New/Program.cs +++ b/src/NexusReader.Web.New/Program.cs @@ -458,7 +458,11 @@ app.MapGet("/identity/profile", async (ClaimsPrincipal user, UserManager