refactor: introduce IConceptsMapReadRepository and SegmentIdParser to improve data access and domain logic encapsulation.
This commit is contained in:
@@ -0,0 +1,23 @@
|
|||||||
|
using NexusReader.Domain.Entities;
|
||||||
|
|
||||||
|
namespace NexusReader.Application.Abstractions.Persistence;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Read-only abstraction for fetching concepts map data.
|
||||||
|
/// Defined in the Application layer to avoid a direct dependency on EF Core / NexusReader.Data.
|
||||||
|
/// </summary>
|
||||||
|
public interface IConceptsMapReadRepository
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the last read page ID for the specified user.
|
||||||
|
/// </summary>
|
||||||
|
Task<string?> GetLastReadPageIdAsync(string userId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all knowledge units associated with a book, scoped by tenant.
|
||||||
|
/// </summary>
|
||||||
|
Task<List<KnowledgeUnit>> GetKnowledgeUnitsForBookAsync(
|
||||||
|
Guid bookId,
|
||||||
|
string tenantId,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -5,45 +5,30 @@ using System.Text.Json;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using FluentResults;
|
using FluentResults;
|
||||||
using Microsoft.EntityFrameworkCore;
|
|
||||||
using NexusReader.Application.Abstractions.Messaging;
|
using NexusReader.Application.Abstractions.Messaging;
|
||||||
|
using NexusReader.Application.Abstractions.Persistence;
|
||||||
using NexusReader.Application.Queries.Graph;
|
using NexusReader.Application.Queries.Graph;
|
||||||
using NexusReader.Data.Persistence;
|
using NexusReader.Application.Utilities;
|
||||||
|
|
||||||
namespace NexusReader.Application.Queries.Concepts;
|
namespace NexusReader.Application.Queries.Concepts;
|
||||||
|
|
||||||
internal sealed class GetBookConceptsMapQueryHandler : IQueryHandler<GetBookConceptsMapQuery, BookConceptsMapResultDto>
|
internal sealed class GetBookConceptsMapQueryHandler : IQueryHandler<GetBookConceptsMapQuery, BookConceptsMapResultDto>
|
||||||
{
|
{
|
||||||
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
private readonly IConceptsMapReadRepository _repository;
|
||||||
|
|
||||||
public GetBookConceptsMapQueryHandler(IDbContextFactory<AppDbContext> dbContextFactory)
|
public GetBookConceptsMapQueryHandler(IConceptsMapReadRepository repository)
|
||||||
{
|
{
|
||||||
_dbContextFactory = dbContextFactory;
|
_repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<BookConceptsMapResultDto>> Handle(GetBookConceptsMapQuery request, CancellationToken cancellationToken)
|
public async Task<Result<BookConceptsMapResultDto>> Handle(GetBookConceptsMapQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
|
||||||
|
|
||||||
// 1. Fetch user to extract reading progress (LastReadPageId)
|
// 1. Fetch user to extract reading progress (LastReadPageId)
|
||||||
var user = await dbContext.Users
|
var lastReadPageId = await _repository.GetLastReadPageIdAsync(request.UserId, cancellationToken);
|
||||||
.Where(u => u.Id == request.UserId)
|
var lastReadBlockId = lastReadPageId ?? string.Empty;
|
||||||
.Select(u => new { u.LastReadPageId })
|
|
||||||
.FirstOrDefaultAsync(cancellationToken);
|
|
||||||
|
|
||||||
if (user == null)
|
|
||||||
{
|
|
||||||
return Result.Fail<BookConceptsMapResultDto>("User not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastReadBlockId = user.LastReadPageId ?? string.Empty;
|
|
||||||
|
|
||||||
// 2. Fetch all KnowledgeUnits associated with the ebook and user's tenant
|
// 2. Fetch all KnowledgeUnits associated with the ebook and user's tenant
|
||||||
var units = await dbContext.KnowledgeUnits
|
var units = await _repository.GetKnowledgeUnitsForBookAsync(request.BookId, request.TenantId, cancellationToken);
|
||||||
.Where(k => k.EbookId == request.BookId &&
|
|
||||||
(k.TenantId == request.TenantId || k.TenantId == "global" || string.IsNullOrEmpty(k.TenantId)))
|
|
||||||
.OrderBy(k => k.CreatedAt)
|
|
||||||
.ToListAsync(cancellationToken);
|
|
||||||
|
|
||||||
var nodes = new List<GraphNodeDto>();
|
var nodes = new List<GraphNodeDto>();
|
||||||
|
|
||||||
@@ -99,16 +84,9 @@ internal sealed class GetBookConceptsMapQueryHandler : IQueryHandler<GetBookConc
|
|||||||
|
|
||||||
// Return sorted by the numeric value in the seg-ID to ensure topdown vertical alignment
|
// Return sorted by the numeric value in the seg-ID to ensure topdown vertical alignment
|
||||||
var sortedNodes = nodes
|
var sortedNodes = nodes
|
||||||
.OrderBy(n => ParseSegmentNumber(n.Id))
|
.OrderBy(n => SegmentIdParser.Parse(n.Id))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
return Result.Ok(new BookConceptsMapResultDto(sortedNodes, lastReadBlockId));
|
return Result.Ok(new BookConceptsMapResultDto(sortedNodes, lastReadBlockId));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ParseSegmentNumber(string? id)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(id)) return 0;
|
|
||||||
var digits = new string(id.Where(char.IsDigit).ToArray());
|
|
||||||
return int.TryParse(digits, out var val) ? val : 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace NexusReader.Application.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared utility for parsing numeric segment identifiers from IDs like "seg-42".
|
||||||
|
/// Centralizes the parsing contract to avoid duplication across handlers and UI components.
|
||||||
|
/// </summary>
|
||||||
|
public static class SegmentIdParser
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts the numeric portion from a segment identifier string (e.g., "seg-42" → 42).
|
||||||
|
/// Returns 0 if the string is null, empty, or contains no digits.
|
||||||
|
/// </summary>
|
||||||
|
public static int Parse(string? id)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(id)) return 0;
|
||||||
|
var digits = new string(id.Where(char.IsDigit).ToArray());
|
||||||
|
return int.TryParse(digits, out var val) ? val : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -121,6 +121,7 @@ public static class DependencyInjection
|
|||||||
// Fix #1: Ebook repository (scoped, matches AppDbContext lifetime)
|
// Fix #1: Ebook repository (scoped, matches AppDbContext lifetime)
|
||||||
services.AddScoped<IEbookRepository, EbookRepository>();
|
services.AddScoped<IEbookRepository, EbookRepository>();
|
||||||
services.AddScoped<IQuizResultRepository, QuizResultRepository>();
|
services.AddScoped<IQuizResultRepository, QuizResultRepository>();
|
||||||
|
services.AddScoped<IConceptsMapReadRepository, ConceptsMapReadRepository>();
|
||||||
|
|
||||||
// Fix #2: SignalR broadcaster (scoped, wraps IHubContext which is itself a singleton wrapper)
|
// Fix #2: SignalR broadcaster (scoped, wraps IHubContext which is itself a singleton wrapper)
|
||||||
services.AddScoped<ISyncBroadcaster, SignalRSyncBroadcaster>();
|
services.AddScoped<ISyncBroadcaster, SignalRSyncBroadcaster>();
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using NexusReader.Application.Abstractions.Persistence;
|
||||||
|
using NexusReader.Data.Persistence;
|
||||||
|
using NexusReader.Domain.Entities;
|
||||||
|
|
||||||
|
namespace NexusReader.Infrastructure.Persistence;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// EF Core implementation of <see cref="IConceptsMapReadRepository"/>.
|
||||||
|
/// Uses <see cref="IDbContextFactory{TContext}"/> for Blazor-safe scoped context creation.
|
||||||
|
/// </summary>
|
||||||
|
internal sealed class ConceptsMapReadRepository : IConceptsMapReadRepository
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
|
||||||
|
|
||||||
|
public ConceptsMapReadRepository(IDbContextFactory<AppDbContext> dbContextFactory)
|
||||||
|
{
|
||||||
|
_dbContextFactory = dbContextFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<string?> GetLastReadPageIdAsync(string userId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
var user = await dbContext.Users
|
||||||
|
.Where(u => u.Id == userId)
|
||||||
|
.Select(u => new { u.LastReadPageId })
|
||||||
|
.FirstOrDefaultAsync(cancellationToken);
|
||||||
|
|
||||||
|
return user?.LastReadPageId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<List<KnowledgeUnit>> GetKnowledgeUnitsForBookAsync(
|
||||||
|
Guid bookId,
|
||||||
|
string tenantId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken);
|
||||||
|
|
||||||
|
return await dbContext.KnowledgeUnits
|
||||||
|
.Where(k => k.EbookId == bookId &&
|
||||||
|
(k.TenantId == tenantId || k.TenantId == "global" || string.IsNullOrEmpty(k.TenantId)))
|
||||||
|
.OrderBy(k => k.CreatedAt)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@using NexusReader.Application.Queries.Graph
|
@using NexusReader.Application.Queries.Graph
|
||||||
|
@using NexusReader.Application.Utilities
|
||||||
@using NexusReader.UI.Shared.Components.Atoms
|
@using NexusReader.UI.Shared.Components.Atoms
|
||||||
|
|
||||||
<div class="concepts-map">
|
<div class="concepts-map">
|
||||||
@@ -75,10 +76,10 @@
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(nodeId)) return false;
|
if (string.IsNullOrEmpty(nodeId)) return false;
|
||||||
|
|
||||||
var nodeSeq = ParseSegmentNumber(nodeId);
|
var nodeSeq = SegmentIdParser.Parse(nodeId);
|
||||||
|
|
||||||
// Always unlock the very first segment so the user has a starting node
|
// Always unlock the very first segment so the user has a starting node
|
||||||
var minNodeSeq = Nodes.Any() ? Nodes.Min(n => ParseSegmentNumber(n.Id)) : 0;
|
var minNodeSeq = Nodes.Any() ? Nodes.Min(n => SegmentIdParser.Parse(n.Id)) : 0;
|
||||||
if (nodeSeq == minNodeSeq) return true;
|
if (nodeSeq == minNodeSeq) return true;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(LastReadBlockId))
|
if (string.IsNullOrEmpty(LastReadBlockId))
|
||||||
@@ -86,16 +87,11 @@
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var progressSeq = ParseSegmentNumber(LastReadBlockId);
|
var progressSeq = SegmentIdParser.Parse(LastReadBlockId);
|
||||||
return nodeSeq <= progressSeq;
|
return nodeSeq <= progressSeq;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ParseSegmentNumber(string? id)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(id)) return 0;
|
|
||||||
var digits = new string(id.Where(char.IsDigit).ToArray());
|
|
||||||
return int.TryParse(digits, out var val) ? val : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleNodeClick(GraphNodeDto node)
|
private async Task HandleNodeClick(GraphNodeDto node)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -65,14 +65,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.timeline-step.unlocked:hover {
|
.timeline-step.unlocked:hover {
|
||||||
border-color: rgba(0, 243, 255, 0.15);
|
border-color: rgba(0, 255, 153, 0.15);
|
||||||
box-shadow: 0 4px 20px rgba(0, 243, 255, 0.05);
|
box-shadow: 0 4px 20px rgba(0, 255, 153, 0.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-step.selected {
|
.timeline-step.selected {
|
||||||
background: rgba(0, 243, 255, 0.04);
|
background: rgba(0, 255, 153, 0.04);
|
||||||
border-color: var(--nexus-neon);
|
border-color: var(--nexus-neon);
|
||||||
box-shadow: 0 0 15px rgba(0, 243, 255, 0.1);
|
box-shadow: 0 0 15px var(--nexus-primary-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-connector-wrapper {
|
.node-connector-wrapper {
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
.unlocked .node-circle {
|
.unlocked .node-circle {
|
||||||
border: 2px solid var(--nexus-neon);
|
border: 2px solid var(--nexus-neon);
|
||||||
color: var(--nexus-neon);
|
color: var(--nexus-neon);
|
||||||
box-shadow: 0 0 10px rgba(0, 243, 255, 0.3);
|
box-shadow: 0 0 10px var(--nexus-primary-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.locked .node-circle {
|
.locked .node-circle {
|
||||||
@@ -136,8 +136,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-active {
|
.track-active {
|
||||||
background: linear-gradient(180deg, var(--nexus-neon), rgba(0, 243, 255, 0.2));
|
background: linear-gradient(180deg, var(--nexus-neon), rgba(0, 255, 153, 0.2));
|
||||||
box-shadow: 0 0 6px rgba(0, 243, 255, 0.2);
|
box-shadow: 0 0 6px var(--nexus-primary-glow);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-inactive {
|
.track-inactive {
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
|
|
||||||
.timeline-step.selected .node-content {
|
.timeline-step.selected .node-content {
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
border-color: rgba(0, 243, 255, 0.2);
|
border-color: rgba(0, 255, 153, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-header {
|
.node-header {
|
||||||
@@ -188,9 +188,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.badge-unlocked {
|
.badge-unlocked {
|
||||||
background: rgba(0, 243, 255, 0.08);
|
background: rgba(0, 255, 153, 0.08);
|
||||||
color: var(--nexus-neon);
|
color: var(--nexus-neon);
|
||||||
border: 1px solid rgba(0, 243, 255, 0.2);
|
border: 1px solid rgba(0, 255, 153, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-locked {
|
.badge-locked {
|
||||||
|
|||||||
@@ -8,12 +8,13 @@
|
|||||||
@using NexusReader.Application.Queries.Concepts
|
@using NexusReader.Application.Queries.Concepts
|
||||||
@using System.Net.Http.Json
|
@using System.Net.Http.Json
|
||||||
@using NexusReader.Application.Abstractions.Services
|
@using NexusReader.Application.Abstractions.Services
|
||||||
|
@using NexusReader.Application.Utilities
|
||||||
@inject IConceptsMapService ConceptsMapService
|
@inject IConceptsMapService ConceptsMapService
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject IIdentityService IdentityService
|
@inject IIdentityService IdentityService
|
||||||
@inject ISyncService SyncService
|
@inject ISyncService SyncService
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@implements IDisposable
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
<PageTitle>Mapa Pojęć | Nexus Reader</PageTitle>
|
<PageTitle>Mapa Pojęć | Nexus Reader</PageTitle>
|
||||||
|
|
||||||
@@ -235,23 +236,18 @@
|
|||||||
private bool IsUnlocked(string nodeId)
|
private bool IsUnlocked(string nodeId)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(nodeId)) return false;
|
if (string.IsNullOrEmpty(nodeId)) return false;
|
||||||
var nodeSeq = ParseSegmentNumber(nodeId);
|
var nodeSeq = SegmentIdParser.Parse(nodeId);
|
||||||
|
|
||||||
var minNodeSeq = Nodes.Any() ? Nodes.Min(n => ParseSegmentNumber(n.Id)) : 0;
|
var minNodeSeq = Nodes.Any() ? Nodes.Min(n => SegmentIdParser.Parse(n.Id)) : 0;
|
||||||
if (nodeSeq == minNodeSeq) return true;
|
if (nodeSeq == minNodeSeq) return true;
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(LastReadBlockId)) return false;
|
if (string.IsNullOrEmpty(LastReadBlockId)) return false;
|
||||||
|
|
||||||
var progressSeq = ParseSegmentNumber(LastReadBlockId);
|
var progressSeq = SegmentIdParser.Parse(LastReadBlockId);
|
||||||
return nodeSeq <= progressSeq;
|
return nodeSeq <= progressSeq;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int ParseSegmentNumber(string? id)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(id)) return 0;
|
|
||||||
var digits = new string(id.Where(char.IsDigit).ToArray());
|
|
||||||
return int.TryParse(digits, out var val) ? val : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void HandleNodeSelected(GraphNodeDto node)
|
private void HandleNodeSelected(GraphNodeDto node)
|
||||||
{
|
{
|
||||||
@@ -276,7 +272,7 @@
|
|||||||
{
|
{
|
||||||
if (BookId.HasValue && SelectedNode != null)
|
if (BookId.HasValue && SelectedNode != null)
|
||||||
{
|
{
|
||||||
var chapterIndex = ParseSegmentNumber(SelectedNode.Id);
|
var chapterIndex = SegmentIdParser.Parse(SelectedNode.Id);
|
||||||
NavigationManager.NavigateTo($"/reader/{BookId.Value}?chapter={chapterIndex}");
|
NavigationManager.NavigateTo($"/reader/{BookId.Value}?chapter={chapterIndex}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -299,9 +295,10 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
IdentityService.OnStateInvalidated -= HandleStateInvalidatedAsync;
|
IdentityService.OnStateInvalidated -= HandleStateInvalidatedAsync;
|
||||||
SyncService.OnProgressReceived -= HandleProgressReceivedAsync;
|
SyncService.OnProgressReceived -= HandleProgressReceivedAsync;
|
||||||
|
return ValueTask.CompletedTask;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
::deep .add-book-trigger {
|
.add-book-trigger {
|
||||||
background: var(--nexus-neon) !important;
|
background: var(--nexus-neon) !important;
|
||||||
color: #000000 !important;
|
color: #000000 !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
@@ -41,7 +41,7 @@
|
|||||||
border-radius: var(--radius-md) !important;
|
border-radius: var(--radius-md) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
::deep .add-book-trigger:hover {
|
.add-book-trigger:hover {
|
||||||
transform: translateY(-2px) !important;
|
transform: translateY(-2px) !important;
|
||||||
box-shadow: 0 8px 20px rgba(0, 255, 153, 0.5) !important;
|
box-shadow: 0 8px 20px rgba(0, 255, 153, 0.5) !important;
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
@inject ILogger<SerilogDemo> Logger
|
@inject ILogger<SerilogDemo> Logger
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
<div class="serilog-demo-container">
|
<div class="serilog-demo-container">
|
||||||
<div class="header-card glass-panel">
|
<div class="header-card glass-panel">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<NexusIcon Name="cpu" Size="36" Class="header-icon" />
|
<NexusIcon Name="cpu" Size="36" Class="header-icon" />
|
||||||
<div class="header-text">
|
<div class="header-text">
|
||||||
<h1>Serilog Logging Infrastructure</h1>
|
<h1>Serilog Logging Infrastructure</h1>
|
||||||
<p class="subtitle">Production-grade diagnostic pipeline for unified native & web logs</p>
|
<p class="subtitle">Production-grade diagnostic pipeline for unified native & web logs</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="status-badge">
|
<div class="status-badge">
|
||||||
@@ -87,9 +88,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
#else
|
||||||
|
<div class="serilog-demo-container">
|
||||||
|
<div class="glass-panel" style="text-align: center; padding: 3rem;">
|
||||||
|
<h2>Diagnostics Unavailable</h2>
|
||||||
|
<p>This page is only available in DEBUG builds.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
|
#if DEBUG
|
||||||
private void LogInfo()
|
private void LogInfo()
|
||||||
{
|
{
|
||||||
Logger.LogInformation("Structured native log triggered by user from SerilogDemo. Button: LogInfo");
|
Logger.LogInformation("Structured native log triggered by user from SerilogDemo. Button: LogInfo");
|
||||||
@@ -121,4 +131,5 @@
|
|||||||
{
|
{
|
||||||
await JSRuntime.InvokeVoidAsync("eval", "throw new Error('Simulated runtime JS Exception triggered from Blazor UI button click!');");
|
await JSRuntime.InvokeVoidAsync("eval", "throw new Error('Simulated runtime JS Exception triggered from Blazor UI button click!');");
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,15 @@
|
|||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
<p>Configure your account and application preferences.</p>
|
<p>Configure your account and application preferences.</p>
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
<div class="settings-section glass-panel">
|
<div class="settings-section glass-panel">
|
||||||
<h2>Diagnostics & System Logs</h2>
|
<h2>Diagnostics & System Logs</h2>
|
||||||
<p>Inspect native logging infrastructure, trigger custom logs, and trace WebView errors.</p>
|
<p>Inspect native logging infrastructure, trigger custom logs, and trace WebView errors.</p>
|
||||||
<a class="diag-btn" href="/serilog-demo">
|
<a class="diag-btn" href="/serilog-demo">
|
||||||
<NexusIcon Name="cpu" Size="16" />
|
<NexusIcon Name="cpu" Size="16" />
|
||||||
Open Serilog Diagnostics Dashboard
|
Open Serilog Diagnostics Dashboard
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
#endif
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
animation: fadeIn 0.6s ease-out;
|
animation: fadeIn 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
.settings-page > h1 {
|
||||||
font-family: var(--nexus-font-serif);
|
font-family: var(--nexus-font-serif);
|
||||||
font-size: 2.8rem;
|
font-size: 2.8rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
@@ -73,8 +73,13 @@
|
|||||||
.btn-nexus:hover {
|
.btn-nexus:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
filter: brightness(1.1);
|
filter: brightness(1.1);
|
||||||
|
}
|
||||||
|
.btn-nexus-primary:hover {
|
||||||
box-shadow: 0 4px 15px var(--nexus-primary-glow);
|
box-shadow: 0 4px 15px var(--nexus-primary-glow);
|
||||||
}
|
}
|
||||||
|
.btn-nexus-secondary:hover {
|
||||||
|
box-shadow: 0 4px 15px rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.theme-light {
|
.theme-light {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using FluentResults;
|
using FluentResults;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using Microsoft.AspNetCore.Components.Authorization;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using NexusReader.Application.Abstractions.Services;
|
using NexusReader.Application.Abstractions.Services;
|
||||||
using NexusReader.Application.Queries.Concepts;
|
using NexusReader.Application.Queries.Concepts;
|
||||||
|
|
||||||
@@ -11,38 +9,30 @@ namespace NexusReader.Web.Services;
|
|||||||
public class ServerConceptsMapService : IConceptsMapService
|
public class ServerConceptsMapService : IConceptsMapService
|
||||||
{
|
{
|
||||||
private readonly IMediator _mediator;
|
private readonly IMediator _mediator;
|
||||||
private readonly AuthenticationStateProvider _authStateProvider;
|
private readonly IIdentityService _identityService;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
|
||||||
|
|
||||||
public ServerConceptsMapService(
|
public ServerConceptsMapService(
|
||||||
IMediator mediator,
|
IMediator mediator,
|
||||||
AuthenticationStateProvider authStateProvider,
|
IIdentityService identityService)
|
||||||
IHttpContextAccessor httpContextAccessor)
|
|
||||||
{
|
{
|
||||||
_mediator = mediator;
|
_mediator = mediator;
|
||||||
_authStateProvider = authStateProvider;
|
_identityService = identityService;
|
||||||
_httpContextAccessor = httpContextAccessor;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Result<BookConceptsMapResultDto>> GetConceptsMapAsync(Guid bookId)
|
public async Task<Result<BookConceptsMapResultDto>> GetConceptsMapAsync(Guid bookId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var authState = await _authStateProvider.GetAuthenticationStateAsync();
|
var profileResult = await _identityService.GetProfileAsync();
|
||||||
var user = authState.User;
|
|
||||||
|
|
||||||
if (user == null || !user.Identity?.IsAuthenticated == true)
|
if (profileResult.IsFailed)
|
||||||
{
|
|
||||||
user = _httpContextAccessor.HttpContext?.User;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user == null || !user.Identity?.IsAuthenticated == true)
|
|
||||||
{
|
{
|
||||||
return Result.Fail<BookConceptsMapResultDto>("Użytkownik nie jest uwierzytelniony.");
|
return Result.Fail<BookConceptsMapResultDto>("Użytkownik nie jest uwierzytelniony.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
var profile = profileResult.Value;
|
||||||
var tenantId = user.FindFirstValue("TenantId") ?? "global";
|
var userId = profile.UserId;
|
||||||
|
var tenantId = profile.TenantId.ToString();
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(userId))
|
if (string.IsNullOrEmpty(userId))
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -114,12 +114,12 @@ public class ServerIdentityService : IIdentityService
|
|||||||
var authState = await _authStateProvider.GetAuthenticationStateAsync();
|
var authState = await _authStateProvider.GetAuthenticationStateAsync();
|
||||||
var user = authState.User;
|
var user = authState.User;
|
||||||
|
|
||||||
if (user == null || !user.Identity?.IsAuthenticated == true)
|
if (user == null || user.Identity?.IsAuthenticated != true)
|
||||||
{
|
{
|
||||||
user = _httpContextAccessor.HttpContext?.User;
|
user = _httpContextAccessor.HttpContext?.User;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user == null || !user.Identity?.IsAuthenticated == true) return Result.Fail("Not authenticated.");
|
if (user == null || user.Identity?.IsAuthenticated != true) return Result.Fail("Not authenticated.");
|
||||||
|
|
||||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
if (userId == null) return Result.Fail("User ID not found.");
|
if (userId == null) return Result.Fail("User ID not found.");
|
||||||
|
|||||||
Reference in New Issue
Block a user