feat: implement identity authentication, authorization policies, and MAUI platform support with Docker orchestration
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user