refactor: normalize Author entity, eliminate magic strings, and improve exception handling per PR review
This commit is contained in:
@@ -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);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user