Refactor Intelligence Toolbar (#14) and fix auth regression (#24)

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: #24
Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Co-committed-by: Marek Jasiński <jasins.marek@gmail.com>
This commit was merged in pull request #24.
This commit is contained in:
2026-05-08 18:50:15 +00:00
committed by Marek Jaisński
parent 55cc3ae10d
commit 9e77aee231
8 changed files with 72 additions and 13 deletions
@@ -46,6 +46,9 @@
case "arrow-right": case "arrow-right":
<path d="M5 12h14M12 5l7 7-7 7" /> <path d="M5 12h14M12 5l7 7-7 7" />
break; break;
case "log-out":
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
break;
default: default:
<!-- Fallback circle --> <!-- Fallback circle -->
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="10" />

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

@@ -2,6 +2,8 @@
@using NexusReader.Application.Abstractions.Services @using NexusReader.Application.Abstractions.Services
@inject IFocusModeService FocusMode @inject IFocusModeService FocusMode
@inject IKnowledgeService KnowledgeService @inject IKnowledgeService KnowledgeService
@inject IIdentityService IdentityService
@inject NavigationManager NavigationManager
<aside class="intelligence-toolbar"> <aside class="intelligence-toolbar">
<div class="toolbar-top"> <div class="toolbar-top">
@@ -36,6 +38,9 @@
<button class="toolbar-item" title="Global Settings"> <button class="toolbar-item" title="Global Settings">
<NexusIcon Name="settings" Size="20" /> <NexusIcon Name="settings" Size="20" />
</button> </button>
<button class="toolbar-item logout-item" @onclick="HandleLogout" title="Logout">
<NexusIcon Name="log-out" Size="20" />
</button>
</div> </div>
</aside> </aside>
@@ -56,6 +61,12 @@
} }
} }
private async Task HandleLogout()
{
await IdentityService.LogoutAsync();
NavigationManager.NavigateTo("/", true);
}
private Task HandleUpdate() => InvokeAsync(StateHasChanged); private Task HandleUpdate() => InvokeAsync(StateHasChanged);
public void Dispose() public void Dispose()
@@ -1,8 +1,8 @@
.intelligence-toolbar { .intelligence-toolbar {
width: 50px; width: 50px;
height: 100%; height: 100%;
background: #080808; background: #0D0D0D;
border-right: 1px solid rgba(255, 255, 255, 0.03); border-right: 1px solid rgba(255, 255, 255, 0.08);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
justify-content: space-between; justify-content: space-between;
@@ -10,6 +10,7 @@
align-items: center; align-items: center;
z-index: 20; z-index: 20;
box-shadow: inset -2px 0 10px rgba(0,0,0,0.5); box-shadow: inset -2px 0 10px rgba(0,0,0,0.5);
backdrop-filter: blur(10px);
} }
@@ -22,7 +23,7 @@
.toolbar-item { .toolbar-item {
background: none; background: none;
border: none; border: none;
color: #444; color: #555;
cursor: pointer; cursor: pointer;
width: 34px; width: 34px;
height: 34px; height: 34px;
@@ -37,11 +38,15 @@
.toolbar-item:hover { .toolbar-item:hover {
color: var(--nexus-neon); color: var(--nexus-neon);
background: rgba(0, 255, 153, 0.05); 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 { .toolbar-item.active {
color: var(--nexus-neon); color: var(--nexus-neon);
background: rgba(0, 255, 153, 0.08); 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 { .toolbar-item.active::after {
@@ -35,10 +35,7 @@
<span>Asystent AI</span> <span>Asystent AI</span>
</div> </div>
<div class="user-profile">
<span class="user-email">@context.User.Identity?.Name</span>
<button class="logout-btn" @onclick="HandleLogout">Logout</button>
</div>
<button class="close-btn">×</button> <button class="close-btn">×</button>
</div> </div>
@@ -93,11 +90,7 @@
} }
} }
private async Task HandleLogout()
{
await IdentityService.LogoutAsync();
NavigationManager.NavigateTo("/", true);
}
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
@@ -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<HttpResponseMessage> 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);
}
}
@@ -12,6 +12,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="MediatR" Version="12.1.1" /> <PackageReference Include="MediatR" Version="12.1.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.Http" Version="10.0.7" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+8 -1
View File
@@ -33,7 +33,14 @@ builder.Services.AddCascadingAuthenticationState();
// AI & Content Services // AI & Content Services
builder.Services.AddScoped<IKnowledgeService, WasmKnowledgeService>(); builder.Services.AddScoped<IKnowledgeService, WasmKnowledgeService>();
builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
builder.Services.AddTransient<NexusReader.Web.Client.Handlers.AuthenticationHeaderHandler>();
builder.Services.AddHttpClient("NexusAPI", client =>
{
client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress);
}).AddHttpMessageHandler<NexusReader.Web.Client.Handlers.AuthenticationHeaderHandler>();
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
// Dummy registrations for server-only handlers to satisfy DI validation // Dummy registrations for server-only handlers to satisfy DI validation
builder.Services.AddSingleton<IDbContextFactory<AppDbContext>>(new ThrowingDbContextFactory()); builder.Services.AddSingleton<IDbContextFactory<AppDbContext>>(new ThrowingDbContextFactory());
+12
View File
@@ -96,6 +96,18 @@ builder.Services.ConfigureApplicationCookie(options =>
options.Cookie.HttpOnly = true; options.Cookie.HttpOnly = true;
options.ExpireTimeSpan = TimeSpan.FromDays(30); options.ExpireTimeSpan = TimeSpan.FromDays(30);
options.SlidingExpiration = true; 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<IdentityOptions>(options => builder.Services.Configure<IdentityOptions>(options =>