fix(ui): implement ServerIdentityService to resolve SSR profile fetch issues

- Added ServerIdentityService to handle profile requests directly from DB on server
- Updated Program.cs to use ServerIdentityService in the Web project
- Cleaned up diagnostic logs across the solution
- Finalized Dashboard glass panel animations
This commit is contained in:
2026-05-10 09:49:44 +02:00
parent ab605dff42
commit 39d9423d67
7 changed files with 89 additions and 24 deletions
+3
View File
@@ -44,3 +44,6 @@ description: Design System & Component rules for Blazor
- **Interactive Flow:** - **Interactive Flow:**
- AI Assistant interactions must be non-blocking and smoothly transition using CSS animations. - AI Assistant interactions must be non-blocking and smoothly transition using CSS animations.
- Interactive elements must have clear `:hover`, `:active`, and `:focus` states. - Interactive elements must have clear `:hover`, `:active`, and `:focus` states.
- **Glass Panel Standard:** All primary data panels (`.glass-panel`) must implement the following interaction signature:
- `transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1)`
- `:hover` state must include: `transform: translateY(-4px)`, increased background opacity, and a subtle `--nexus-neon` border highlight (e.g., `rgba(0, 255, 153, 0.2)`).
@@ -85,14 +85,6 @@
private bool _isSyncing = false; private bool _isSyncing = false;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
Console.WriteLine($"[MainHubLayout] Rendered. IsBrowser: {System.OperatingSystem.IsBrowser()}");
}
}
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
if (_isSyncing) return; if (_isSyncing) return;
@@ -110,9 +110,7 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
Console.WriteLine("[Profile] Initializing...");
_profile = await IdentityService.GetProfileAsync(); _profile = await IdentityService.GetProfileAsync();
Console.WriteLine($"[Profile] Profile loaded: {_profile?.Email ?? "NULL"}");
StateHasChanged(); StateHasChanged();
} }
@@ -128,6 +128,14 @@
border: 1px solid rgba(255, 255, 255, 0.05); border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 20px; border-radius: 20px;
padding: 1.5rem; padding: 1.5rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.glass-panel:hover {
background: rgba(255, 255, 255, 0.05);
border-color: rgba(0, 255, 153, 0.2);
transform: translateY(-4px);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
} }
/* Reading Card */ /* Reading Card */
@@ -121,17 +121,14 @@ public class IdentityService : IIdentityService
{ {
if (_cachedProfile != null) if (_cachedProfile != null)
{ {
Console.WriteLine("[IdentityService] Returning cached profile.");
return _cachedProfile; return _cachedProfile;
} }
if (_profileTask != null) if (_profileTask != null)
{ {
Console.WriteLine("[IdentityService] Awaiting existing profile task...");
return await _profileTask; return await _profileTask;
} }
Console.WriteLine("[IdentityService] Starting new profile fetch task...");
_profileTask = GetProfileInternalAsync(); _profileTask = GetProfileInternalAsync();
return await _profileTask; return await _profileTask;
} }
@@ -142,13 +139,11 @@ public class IdentityService : IIdentityService
{ {
if (!System.OperatingSystem.IsBrowser()) if (!System.OperatingSystem.IsBrowser())
{ {
Console.WriteLine("[IdentityService] Skipping profile fetch: Not in browser.");
return null; return null;
} }
if (DateTime.UtcNow - _lastFetchAttempt < TimeSpan.FromSeconds(5)) if (DateTime.UtcNow - _lastFetchAttempt < TimeSpan.FromSeconds(5))
{ {
Console.WriteLine("[IdentityService] Skipping profile fetch: Throttled.");
return null; return null;
} }
@@ -156,23 +151,18 @@ public class IdentityService : IIdentityService
try try
{ {
var response = await _httpClient.GetAsync("identity/profile"); var response = await _httpClient.GetAsync("identity/profile");
Console.WriteLine($"[IdentityService] Profile response: {response.StatusCode}");
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{ {
Console.WriteLine("[IdentityService] Unauthorized. Logging out...");
await LogoutAsync(); await LogoutAsync();
return null; return null;
} }
if (response.IsSuccessStatusCode) if (response.IsSuccessStatusCode)
{ {
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"[IdentityService] Raw content: {content}");
var profile = await response.Content.ReadFromJsonAsync<UserProfile>(); var profile = await response.Content.ReadFromJsonAsync<UserProfile>();
if (profile != null) if (profile != null)
{ {
Console.WriteLine($"[IdentityService] Profile fetched: {profile.Email}");
_cachedProfile = profile; _cachedProfile = profile;
await _storageService.SaveSecureString("nexus_user_email", profile.Email); await _storageService.SaveSecureString("nexus_user_email", profile.Email);
await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString());
@@ -183,9 +173,8 @@ public class IdentityService : IIdentityService
return null; return null;
} }
catch (Exception ex) catch
{ {
Console.WriteLine($"[IdentityService] ERROR: {ex.Message}");
return null; return null;
} }
finally finally
+2 -1
View File
@@ -52,7 +52,8 @@ builder.Services.AddHttpClient("NexusAPI", client =>
}); });
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI")); builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
builder.Services.AddScoped<IIdentityService, NexusReader.UI.Shared.Services.IdentityService>(); builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<IIdentityService, NexusReader.Web.New.Services.ServerIdentityService>();
builder.Services.AddCascadingAuthenticationState(); builder.Services.AddCascadingAuthenticationState();
builder.Services.AddApplication(); builder.Services.AddApplication();
@@ -0,0 +1,74 @@
using System.Security.Claims;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using NexusReader.Application.DTOs.User;
using NexusReader.Data.Persistence;
using NexusReader.Domain.Entities;
using NexusReader.UI.Shared.Services;
namespace NexusReader.Web.New.Services;
public class ServerIdentityService : IIdentityService
{
private readonly UserManager<NexusUser> _userManager;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
public ServerIdentityService(
UserManager<NexusUser> userManager,
IHttpContextAccessor httpContextAccessor,
IDbContextFactory<AppDbContext> dbContextFactory)
{
_userManager = userManager;
_httpContextAccessor = httpContextAccessor;
_dbContextFactory = dbContextFactory;
}
public Task<bool> LoginAsync(string email, string password, bool rememberMe = false)
=> throw new NotSupportedException("Use standard Identity endpoints for login on server.");
public Task LogoutAsync()
=> throw new NotSupportedException("Use standard Identity endpoints for logout on server.");
public Task<bool> RegisterAsync(string email, string password)
=> throw new NotSupportedException("Use standard Identity endpoints for registration on server.");
public Task<bool> RefreshTokenAsync() => Task.FromResult(true);
public async Task<UserProfile?> GetProfileAsync()
{
var user = _httpContextAccessor.HttpContext?.User;
if (user == null || !user.Identity?.IsAuthenticated == true) return null;
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId == null) return null;
using var dbContext = await _dbContextFactory.CreateDbContextAsync();
var profile = await dbContext.Users
.Where(u => u.Id == userId)
.Select(u => new UserProfile(
u.Email ?? string.Empty,
u.AITokensUsed,
u.TenantId != null && u.TenantId.Length == 36 ? new Guid(u.TenantId) : Guid.Empty,
u.SubscriptionPlan != null ? new SubscriptionPlanDto
{
Id = u.SubscriptionPlan.Id,
Name = u.SubscriptionPlan.PlanName,
AITokenLimit = u.SubscriptionPlan.AITokenLimit,
MonthlyPrice = u.SubscriptionPlan.MonthlyPrice
} : new SubscriptionPlanDto(),
u.QuizResults.Any(q => q.TotalQuestions > 0)
? (int)u.QuizResults.Where(q => q.TotalQuestions > 0).Average(q => (double)q.Score / q.TotalQuestions * 100)
: 0,
u.Ebooks.OrderByDescending(e => e.LastReadDate).Select(e => new LastReadBookDto
{
Id = e.Id,
Title = e.Title
}).FirstOrDefault()
))
.FirstOrDefaultAsync();
return profile;
}
}