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:
@@ -43,4 +43,7 @@ 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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user