From 9e77aee2312eeab9c17780d315476f9df1870835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Fri, 8 May 2026 18:50:15 +0000 Subject: [PATCH] Refactor Intelligence Toolbar (#14) and fix auth regression (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR resolves the authentication regression issue where users encountered "Unauthorized" errors after logging out and back in. This regression was identified during the refactoring of the Intelligence Toolbar. Fixes #14 ### Changes: - **WASM Client**: Added `AuthenticationHeaderHandler` to automatically attach Bearer tokens to `HttpClient` requests. - **Server**: Configured Cookie authentication to return `401 Unauthorized` for `/api` requests instead of redirecting to the login page. - **Project Configuration**: Added `Microsoft.Extensions.Http` to the WASM client project to support `IHttpClientFactory` and message handlers. Verified with local build. Reviewed-on: https://git.archimap.cloud/mjasin/Nexus.Reader/pulls/24 Co-authored-by: Marek Jasiński Co-committed-by: Marek Jasiński --- .../Components/Atoms/NexusIcon.razor | 3 +++ .../Molecules/IntelligenceToolbar.razor | 11 ++++++++ .../Molecules/IntelligenceToolbar.razor.css | 11 +++++--- .../Layout/MainLayout.razor | 11 ++------ .../Handlers/AuthenticationHeaderHandler.cs | 27 +++++++++++++++++++ .../NexusReader.Web.Client.csproj | 1 + src/NexusReader.Web.Client/Program.cs | 9 ++++++- src/NexusReader.Web.New/Program.cs | 12 +++++++++ 8 files changed, 72 insertions(+), 13 deletions(-) create mode 100644 src/NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs diff --git a/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor b/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor index 4909fe2..3d73d8f 100644 --- a/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor +++ b/src/NexusReader.UI.Shared/Components/Atoms/NexusIcon.razor @@ -46,6 +46,9 @@ case "arrow-right": break; + case "log-out": + + break; default: diff --git a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor index 0c3f270..f603abb 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor +++ b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor @@ -2,6 +2,8 @@ @using NexusReader.Application.Abstractions.Services @inject IFocusModeService FocusMode @inject IKnowledgeService KnowledgeService +@inject IIdentityService IdentityService +@inject NavigationManager NavigationManager @@ -56,6 +61,12 @@ } } + private async Task HandleLogout() + { + await IdentityService.LogoutAsync(); + NavigationManager.NavigateTo("/", true); + } + private Task HandleUpdate() => InvokeAsync(StateHasChanged); public void Dispose() diff --git a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor.css b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor.css index eaa1e48..521ddd1 100644 --- a/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor.css +++ b/src/NexusReader.UI.Shared/Components/Molecules/IntelligenceToolbar.razor.css @@ -1,8 +1,8 @@ .intelligence-toolbar { width: 50px; height: 100%; - background: #080808; - border-right: 1px solid rgba(255, 255, 255, 0.03); + background: #0D0D0D; + border-right: 1px solid rgba(255, 255, 255, 0.08); display: flex; flex-direction: column; justify-content: space-between; @@ -10,6 +10,7 @@ align-items: center; z-index: 20; box-shadow: inset -2px 0 10px rgba(0,0,0,0.5); + backdrop-filter: blur(10px); } @@ -22,7 +23,7 @@ .toolbar-item { background: none; border: none; - color: #444; + color: #555; cursor: pointer; width: 34px; height: 34px; @@ -37,11 +38,15 @@ .toolbar-item:hover { color: var(--nexus-neon); background: rgba(0, 255, 153, 0.05); + box-shadow: 0 0 15px rgba(0, 255, 153, 0.15); + filter: drop-shadow(0 0 5px var(--nexus-neon)); } .toolbar-item.active { color: var(--nexus-neon); background: rgba(0, 255, 153, 0.08); + box-shadow: 0 0 20px rgba(0, 255, 153, 0.25); + filter: drop-shadow(0 0 8px var(--nexus-neon)); } .toolbar-item.active::after { diff --git a/src/NexusReader.UI.Shared/Layout/MainLayout.razor b/src/NexusReader.UI.Shared/Layout/MainLayout.razor index 2092401..5b3bd3a 100644 --- a/src/NexusReader.UI.Shared/Layout/MainLayout.razor +++ b/src/NexusReader.UI.Shared/Layout/MainLayout.razor @@ -35,10 +35,7 @@ Asystent AI - + @@ -93,11 +90,7 @@ } } - private async Task HandleLogout() - { - await IdentityService.LogoutAsync(); - NavigationManager.NavigateTo("/", true); - } + protected override async Task OnAfterRenderAsync(bool firstRender) { diff --git a/src/NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs b/src/NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs new file mode 100644 index 0000000..3772810 --- /dev/null +++ b/src/NexusReader.Web.Client/Handlers/AuthenticationHeaderHandler.cs @@ -0,0 +1,27 @@ +using System.Net.Http.Headers; +using NexusReader.Application.Abstractions.Services; + +namespace NexusReader.Web.Client.Handlers; + +public class AuthenticationHeaderHandler : DelegatingHandler +{ + private readonly INativeStorageService _storageService; + private const string TokenKey = "nexus_auth_token"; + + public AuthenticationHeaderHandler(INativeStorageService storageService) + { + _storageService = storageService; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + var tokenResult = await _storageService.GetSecureString(TokenKey); + + if (tokenResult.IsSuccess && !string.IsNullOrEmpty(tokenResult.Value)) + { + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokenResult.Value); + } + + return await base.SendAsync(request, cancellationToken); + } +} diff --git a/src/NexusReader.Web.Client/NexusReader.Web.Client.csproj b/src/NexusReader.Web.Client/NexusReader.Web.Client.csproj index d806b42..8046dc7 100644 --- a/src/NexusReader.Web.Client/NexusReader.Web.Client.csproj +++ b/src/NexusReader.Web.Client/NexusReader.Web.Client.csproj @@ -12,6 +12,7 @@ + diff --git a/src/NexusReader.Web.Client/Program.cs b/src/NexusReader.Web.Client/Program.cs index 56034d7..736517f 100644 --- a/src/NexusReader.Web.Client/Program.cs +++ b/src/NexusReader.Web.Client/Program.cs @@ -33,7 +33,14 @@ builder.Services.AddCascadingAuthenticationState(); // AI & Content Services builder.Services.AddScoped(); -builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + +builder.Services.AddTransient(); +builder.Services.AddHttpClient("NexusAPI", client => +{ + client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress); +}).AddHttpMessageHandler(); + +builder.Services.AddScoped(sp => sp.GetRequiredService().CreateClient("NexusAPI")); // Dummy registrations for server-only handlers to satisfy DI validation builder.Services.AddSingleton>(new ThrowingDbContextFactory()); diff --git a/src/NexusReader.Web.New/Program.cs b/src/NexusReader.Web.New/Program.cs index b335169..82cd785 100644 --- a/src/NexusReader.Web.New/Program.cs +++ b/src/NexusReader.Web.New/Program.cs @@ -96,6 +96,18 @@ builder.Services.ConfigureApplicationCookie(options => options.Cookie.HttpOnly = true; options.ExpireTimeSpan = TimeSpan.FromDays(30); options.SlidingExpiration = true; + options.Events.OnRedirectToLogin = context => + { + if (context.Request.Path.StartsWithSegments("/api")) + { + context.Response.StatusCode = 401; + } + else + { + context.Response.Redirect(context.RedirectUri); + } + return Task.CompletedTask; + }; }); builder.Services.Configure(options =>