feat: implement identity authentication, authorization policies, and MAUI platform support with Docker orchestration
This commit is contained in:
@@ -3,7 +3,6 @@ using NexusReader.Application;
|
||||
using NexusReader.Infrastructure;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
using NexusReader.Web.Client.Services;
|
||||
using NexusReader.Web.New.Services;
|
||||
using NexusReader.UI.Shared.Services;
|
||||
using NexusReader.Domain.Entities;
|
||||
using NexusReader.Infrastructure.Persistence;
|
||||
@@ -14,6 +13,9 @@ using Microsoft.AspNetCore.Authorization;
|
||||
using NexusReader.Infrastructure.Identity;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using System.Security.Claims;
|
||||
using NexusReader.Infrastructure.Services;
|
||||
|
||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
@@ -32,7 +34,7 @@ builder.Services.AddServerSideBlazor()
|
||||
});
|
||||
|
||||
builder.Services.AddScoped<IPlatformService, WebPlatformService>();
|
||||
builder.Services.AddScoped<INativeStorageService, WebStorageService>();
|
||||
builder.Services.AddScoped<INativeStorageService, NexusReader.UI.Shared.Services.WebStorageService>();
|
||||
builder.Services.AddScoped<IThemeService, ThemeService>();
|
||||
builder.Services.AddScoped<IQuizStateService, QuizStateService>();
|
||||
builder.Services.AddScoped<IFocusModeService, FocusModeService>();
|
||||
@@ -63,6 +65,9 @@ builder.Services.AddAuthorization(options =>
|
||||
options.AddPolicy("HasAvailableTokens", policy => policy.AddRequirements(new TokenLimitRequirement()));
|
||||
});
|
||||
|
||||
// Billing & Stripe
|
||||
builder.Services.AddScoped<IBillingService, BillingService>();
|
||||
|
||||
// Authentication
|
||||
builder.Services.AddAuthentication(options =>
|
||||
{
|
||||
@@ -80,6 +85,7 @@ builder.Services.AddIdentityApiEndpoints<NexusUser>()
|
||||
|
||||
builder.Services.ConfigureApplicationCookie(options =>
|
||||
{
|
||||
options.LoginPath = "/account/login";
|
||||
options.Cookie.HttpOnly = true;
|
||||
options.ExpireTimeSpan = TimeSpan.FromDays(30);
|
||||
options.SlidingExpiration = true;
|
||||
@@ -220,21 +226,35 @@ app.MapGet("/identity/callback/google", async (
|
||||
return Results.Redirect("/account/login?error=ProvisioningFailed");
|
||||
});
|
||||
|
||||
app.MapGet("/identity/profile", async (ClaimsPrincipal user, UserManager<NexusUser> userManager) =>
|
||||
app.MapGet("/identity/profile", async (ClaimsPrincipal user, UserManager<NexusUser> userManager, AppDbContext dbContext) =>
|
||||
{
|
||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||
if (userId == null) return Results.Unauthorized();
|
||||
|
||||
var nexusUser = await userManager.FindByIdAsync(userId);
|
||||
var nexusUser = await dbContext.Users
|
||||
.Include(u => u.Ebooks)
|
||||
.Include(u => u.QuizResults)
|
||||
.FirstOrDefaultAsync(u => u.Id == userId);
|
||||
|
||||
if (nexusUser == null) return Results.NotFound();
|
||||
|
||||
var avgScore = nexusUser.QuizResults.Any()
|
||||
? (int)nexusUser.QuizResults.Average(q => q.Percentage)
|
||||
: 0;
|
||||
|
||||
var lastReadBook = nexusUser.Ebooks
|
||||
.OrderByDescending(e => e.LastReadDate)
|
||||
.FirstOrDefault()?.Title ?? "None";
|
||||
|
||||
return Results.Ok(new
|
||||
{
|
||||
nexusUser.Email,
|
||||
nexusUser.AITokenLimit,
|
||||
nexusUser.AITokensUsed,
|
||||
nexusUser.CurrentPlan,
|
||||
nexusUser.TenantId
|
||||
nexusUser.TenantId,
|
||||
AverageQuizScore = avgScore,
|
||||
LastReadBookTitle = lastReadBook
|
||||
});
|
||||
}).RequireAuthorization();
|
||||
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
using FluentResults;
|
||||
using Microsoft.JSInterop;
|
||||
using NexusReader.Application.Abstractions.Services;
|
||||
|
||||
namespace NexusReader.Web.New.Services;
|
||||
|
||||
public class WebStorageService : INativeStorageService
|
||||
{
|
||||
private readonly IJSRuntime _jsRuntime;
|
||||
|
||||
public WebStorageService(IJSRuntime jsRuntime)
|
||||
{
|
||||
_jsRuntime = jsRuntime;
|
||||
}
|
||||
|
||||
public Result SaveString(string key, string value)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Note: We can't use await in a non-async method,
|
||||
// but for Blazor Server/WASM we usually want async.
|
||||
// However, INativeStorageService has some non-async methods.
|
||||
// We'll use InvokeVoidAsync and ignore the task if needed, or implement them properly.
|
||||
_jsRuntime.InvokeVoidAsync("localStorage.setItem", key, value);
|
||||
return Result.Ok();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Result.Fail(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public Result<string?> GetString(string key)
|
||||
{
|
||||
// This is problematic for synchronous Blazor Server calls.
|
||||
// But in InteractiveAuto/WASM it should be fine if called from async context.
|
||||
// For simplicity and since we mostly care about the async ones for auth:
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"Stripe": {
|
||||
"ApiKey": "sk_test_placeholder",
|
||||
"WebhookSecret": "whsec_placeholder"
|
||||
},
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
@@ -7,7 +11,8 @@
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ConnectionStrings": {
|
||||
"SqliteConnection": "Data Source=nexus.db"
|
||||
"SqliteConnection": "Data Source=nexus.db",
|
||||
"PostgresConnection": "Host=localhost;Database=nexus_db;Username=nexus_user;Password=nexus_password"
|
||||
},
|
||||
"Authentication": {
|
||||
"Google": {
|
||||
|
||||
Reference in New Issue
Block a user