feat: implement identity authentication, authorization policies, and MAUI platform support with Docker orchestration

This commit is contained in:
2026-04-29 20:37:41 +02:00
parent 10efed0369
commit 0210611edf
55 changed files with 2359 additions and 949 deletions
@@ -6,10 +6,10 @@ public interface IReaderNavigationService
int TotalChapters { get; }
string ChapterTitle { get; }
event Action OnNavigationChanged;
event Func<Task>? OnNavigationChanged;
void GoToChapter(int index);
void GoToNextChapter();
void GoToPreviousChapter();
Task GoToChapter(int index);
Task GoToNextChapter();
Task GoToPreviousChapter();
void UpdateMetadata(int currentIndex, int totalChapters, string title);
}
@@ -9,6 +9,7 @@ public interface IIdentityService
Task<bool> LoginAsync(string email, string password);
Task LogoutAsync();
Task<UserProfile?> GetProfileAsync();
Task<bool> RefreshTokenAsync();
}
public record UserProfile(
@@ -16,7 +17,9 @@ public record UserProfile(
int AITokenLimit,
int AITokensUsed,
string CurrentPlan,
Guid TenantId);
Guid TenantId,
int AverageQuizScore,
string LastReadBookTitle);
public class IdentityService : IIdentityService
{
@@ -24,6 +27,7 @@ public class IdentityService : IIdentityService
private readonly INativeStorageService _storageService;
private readonly NexusAuthenticationStateProvider _authStateProvider;
private const string TokenKey = "nexus_auth_token";
private const string RefreshTokenKey = "nexus_refresh_token";
public IdentityService(
HttpClient httpClient,
@@ -51,6 +55,10 @@ public class IdentityService : IIdentityService
if (result != null && !string.IsNullOrEmpty(result.AccessToken))
{
await _storageService.SaveSecureString(TokenKey, result.AccessToken);
if (!string.IsNullOrEmpty(result.RefreshToken))
{
await _storageService.SaveSecureString(RefreshTokenKey, result.RefreshToken);
}
_authStateProvider.NotifyUserAuthentication(result.AccessToken);
return true;
}
@@ -62,6 +70,7 @@ public class IdentityService : IIdentityService
public async Task LogoutAsync()
{
_storageService.RemoveSecure(TokenKey);
_storageService.RemoveSecure(RefreshTokenKey);
_authStateProvider.NotifyUserLogout();
}
@@ -77,6 +86,33 @@ public class IdentityService : IIdentityService
}
}
public async Task<bool> RefreshTokenAsync()
{
var result = await _storageService.GetSecureString(RefreshTokenKey);
var refreshToken = result.IsSuccess ? result.Value : null;
if (string.IsNullOrEmpty(refreshToken)) return false;
var response = await _httpClient.PostAsJsonAsync("identity/refresh", new { refreshToken });
if (response.IsSuccessStatusCode)
{
var loginResult = await response.Content.ReadFromJsonAsync<LoginResponse>();
if (loginResult != null && !string.IsNullOrEmpty(loginResult.AccessToken))
{
await _storageService.SaveSecureString(TokenKey, loginResult.AccessToken);
if (!string.IsNullOrEmpty(loginResult.RefreshToken))
{
await _storageService.SaveSecureString(RefreshTokenKey, loginResult.RefreshToken);
}
_authStateProvider.NotifyUserAuthentication(loginResult.AccessToken);
return true;
}
}
return false;
}
private class LoginResponse
{
public string TokenType { get; set; } = string.Empty;
@@ -1,3 +1,5 @@
using System.Linq;
namespace NexusReader.UI.Shared.Services;
public class ReaderNavigationService : IReaderNavigationService
@@ -6,29 +8,29 @@ public class ReaderNavigationService : IReaderNavigationService
public int TotalChapters { get; private set; } = 1;
public string ChapterTitle { get; private set; } = "Loading...";
public event Action? OnNavigationChanged;
public event Func<Task>? OnNavigationChanged;
public void GoToChapter(int index)
public async Task GoToChapter(int index)
{
if (index < 0 || index >= TotalChapters) return;
CurrentChapterIndex = index;
OnNavigationChanged?.Invoke();
await NotifyNavigationChangedAsync();
}
public void GoToNextChapter()
public async Task GoToNextChapter()
{
if (CurrentChapterIndex < TotalChapters - 1)
{
GoToChapter(CurrentChapterIndex + 1);
await GoToChapter(CurrentChapterIndex + 1);
}
}
public void GoToPreviousChapter()
public async Task GoToPreviousChapter()
{
if (CurrentChapterIndex > 0)
{
GoToChapter(CurrentChapterIndex - 1);
await GoToChapter(CurrentChapterIndex - 1);
}
}
@@ -41,7 +43,21 @@ public class ReaderNavigationService : IReaderNavigationService
if (changed)
{
OnNavigationChanged?.Invoke();
// Note: UpdateMetadata remains void, so we trigger notification fire-and-forget here
// but usually this is called during a render cycle where metadata is updated from a load.
_ = NotifyNavigationChangedAsync();
}
}
private async Task NotifyNavigationChangedAsync()
{
var handlers = OnNavigationChanged?.GetInvocationList();
if (handlers != null)
{
foreach (var handler in handlers.Cast<Func<Task>>())
{
await handler();
}
}
}
}
@@ -0,0 +1,84 @@
using FluentResults;
using Microsoft.JSInterop;
using NexusReader.Application.Abstractions.Services;
namespace NexusReader.UI.Shared.Services;
public class WebStorageService : INativeStorageService
{
private readonly IJSRuntime _jsRuntime;
public WebStorageService(IJSRuntime jsRuntime)
{
_jsRuntime = jsRuntime;
}
public Result SaveString(string key, string value)
{
try
{
_jsRuntime.InvokeVoidAsync("localStorage.setItem", key, value);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(ex.Message);
}
}
public Result<string?> GetString(string key)
{
return Result.Fail("Use GetStringAsync or similar if available, or call from async context.");
}
public Result SaveBool(string key, bool value) => SaveString(key, value.ToString());
public Result<bool> GetBool(string key, bool defaultValue = false)
{
return Result.Ok(defaultValue);
}
public Result Remove(string key)
{
try
{
_jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(ex.Message);
}
}
public async Task<Result> SaveSecureString(string key, string value)
{
try
{
await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, value);
return Result.Ok();
}
catch (Exception ex)
{
return Result.Fail(ex.Message);
}
}
public async Task<Result<string?>> GetSecureString(string key)
{
try
{
var value = await _jsRuntime.InvokeAsync<string?>("localStorage.getItem", key);
return Result.Ok(value);
}
catch (Exception ex)
{
return Result.Fail(ex.Message);
}
}
public Result RemoveSecure(string key)
{
return Remove(key);
}
}