refactor: remove Stripe webhook controller, optimize MainLayout rendering, and update DI registration in Program.cs
This commit is contained in:
@@ -3,12 +3,14 @@
|
|||||||
@using NexusReader.UI.Shared.Services
|
@using NexusReader.UI.Shared.Services
|
||||||
@using NexusReader.UI.Shared.Components.Molecules
|
@using NexusReader.UI.Shared.Components.Molecules
|
||||||
@using NexusReader.UI.Shared.Components.Organisms
|
@using NexusReader.UI.Shared.Components.Organisms
|
||||||
|
@using Microsoft.Extensions.Logging
|
||||||
@inject IPlatformService PlatformService
|
@inject IPlatformService PlatformService
|
||||||
@inject IFocusModeService FocusMode
|
@inject IFocusModeService FocusMode
|
||||||
@inject IQuizStateService QuizService
|
@inject IQuizStateService QuizService
|
||||||
@inject IJSRuntime JS
|
@inject IJSRuntime JS
|
||||||
@inject IIdentityService IdentityService
|
@inject IIdentityService IdentityService
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
|
@inject Microsoft.Extensions.Logging.ILogger<MainLayout> Logger
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
@@ -28,7 +30,8 @@
|
|||||||
<div class="intelligence-content">
|
<div class="intelligence-content">
|
||||||
<div class="intelligence-header">
|
<div class="intelligence-header">
|
||||||
<div class="ai-title">
|
<div class="ai-title">
|
||||||
<NexusIcon Name="robot" Size="20" Class="@($"neon-glow {(QuizService.HasNewQuiz ? "quiz-available" : "")}")" />
|
<NexusIcon Name="robot" Size="20"
|
||||||
|
Class="@($"neon-glow {(QuizService.HasNewQuiz ? "quiz-available" : "")}")" />
|
||||||
<span>Asystent AI</span>
|
<span>Asystent AI</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -41,7 +44,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="intelligence-scroll-area">
|
<div class="intelligence-scroll-area">
|
||||||
|
@if (!_isMobile)
|
||||||
|
{
|
||||||
<KnowledgeGraph />
|
<KnowledgeGraph />
|
||||||
|
}
|
||||||
<KnowledgeCheck />
|
<KnowledgeCheck />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -67,6 +73,7 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
private string _platformClass = "platform-desktop";
|
private string _platformClass = "platform-desktop";
|
||||||
|
private bool _isMobile = false;
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
@@ -76,11 +83,13 @@
|
|||||||
var context = PlatformService.GetDeviceContext();
|
var context = PlatformService.GetDeviceContext();
|
||||||
if (context.IsSuccess)
|
if (context.IsSuccess)
|
||||||
{
|
{
|
||||||
_platformClass = context.Value.DeviceType switch
|
_isMobile = context.Value.DeviceType switch
|
||||||
{
|
{
|
||||||
DeviceType.Phone or DeviceType.Tablet => "platform-mobile",
|
DeviceType.Phone or DeviceType.Tablet => true,
|
||||||
_ => "platform-desktop"
|
_ => false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
_platformClass = _isMobile ? "platform-mobile" : "platform-desktop";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,7 +108,10 @@
|
|||||||
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/layoutResizer.js");
|
var module = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/NexusReader.UI.Shared/js/layoutResizer.js");
|
||||||
await module.InvokeVoidAsync("initResizer", ".app-container", "#sidebar-resizer", "--sidebar-width");
|
await module.InvokeVoidAsync("initResizer", ".app-container", "#sidebar-resizer", "--sidebar-width");
|
||||||
}
|
}
|
||||||
catch { }
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex, "Failed to initialize layout resizer JS module.");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,4 +121,3 @@
|
|||||||
QuizService.OnQuizUpdated -= StateHasChanged;
|
QuizService.OnQuizUpdated -= StateHasChanged;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
using Microsoft.AspNetCore.Identity;
|
|
||||||
using Microsoft.AspNetCore.Mvc;
|
|
||||||
using NexusReader.Domain.Entities;
|
|
||||||
using Stripe;
|
|
||||||
|
|
||||||
namespace NexusReader.Web.New.Controllers;
|
|
||||||
|
|
||||||
[Route("api/[controller]")]
|
|
||||||
[ApiController]
|
|
||||||
public class StripeWebhookController : ControllerBase
|
|
||||||
{
|
|
||||||
private readonly UserManager<NexusUser> _userManager;
|
|
||||||
private readonly IConfiguration _configuration;
|
|
||||||
private readonly string _webhookSecret;
|
|
||||||
|
|
||||||
public StripeWebhookController(UserManager<NexusUser> userManager, IConfiguration configuration)
|
|
||||||
{
|
|
||||||
_userManager = userManager;
|
|
||||||
_configuration = configuration;
|
|
||||||
_webhookSecret = _configuration["Stripe:WebhookSecret"] ?? "";
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpPost]
|
|
||||||
public async Task<IActionResult> Index()
|
|
||||||
{
|
|
||||||
var json = await new StreamReader(HttpContext.Request.Body).ReadToEndAsync();
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var stripeEvent = EventUtility.ConstructEvent(
|
|
||||||
json,
|
|
||||||
Request.Headers["Stripe-Signature"],
|
|
||||||
_webhookSecret
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (stripeEvent.Type)
|
|
||||||
{
|
|
||||||
case EventTypes.CheckoutSessionCompleted:
|
|
||||||
var session = stripeEvent.Data.Object as Stripe.Checkout.Session;
|
|
||||||
await HandleSubscriptionSuccess(session?.CustomerEmail, session?.Metadata);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EventTypes.CustomerSubscriptionUpdated:
|
|
||||||
var subscription = stripeEvent.Data.Object as Stripe.Subscription;
|
|
||||||
// Subscription update might not have email directly, would need to fetch customer
|
|
||||||
// For now, assuming email is in metadata if we set it during checkout
|
|
||||||
await HandleSubscriptionSuccess(subscription?.Metadata["CustomerEmail"], subscription?.Metadata);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case EventTypes.CustomerSubscriptionDeleted:
|
|
||||||
var deletedSubscription = stripeEvent.Data.Object as Stripe.Subscription;
|
|
||||||
await HandleSubscriptionCancellation(deletedSubscription?.Metadata["CustomerEmail"]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Ok();
|
|
||||||
}
|
|
||||||
catch (StripeException e)
|
|
||||||
{
|
|
||||||
return BadRequest(e.Message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleSubscriptionSuccess(string? email, Dictionary<string, string>? metadata)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(email)) return;
|
|
||||||
|
|
||||||
var user = await _userManager.FindByEmailAsync(email);
|
|
||||||
if (user != null)
|
|
||||||
{
|
|
||||||
var plan = metadata != null && metadata.ContainsKey("Plan") ? metadata["Plan"] : "Pro";
|
|
||||||
|
|
||||||
user.CurrentPlan = plan;
|
|
||||||
user.AITokenLimit = plan.ToLower() switch
|
|
||||||
{
|
|
||||||
"pro" => 50000,
|
|
||||||
"enterprise" => 500000,
|
|
||||||
_ => 10000 // default for unknown or free
|
|
||||||
};
|
|
||||||
|
|
||||||
await _userManager.UpdateAsync(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleSubscriptionCancellation(string? email)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(email)) return;
|
|
||||||
|
|
||||||
var user = await _userManager.FindByEmailAsync(email);
|
|
||||||
if (user != null)
|
|
||||||
{
|
|
||||||
user.CurrentPlan = "Free";
|
|
||||||
user.AITokenLimit = 5000; // Free tier limit
|
|
||||||
await _userManager.UpdateAsync(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,7 @@ using NexusReader.Infrastructure.Identity;
|
|||||||
using Microsoft.AspNetCore.Authentication;
|
using Microsoft.AspNetCore.Authentication;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using NexusReader.Infrastructure.Services;
|
using NexusReader.Infrastructure.Services;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
|
||||||
|
|
||||||
@@ -24,8 +25,6 @@ builder.Services.AddRazorComponents()
|
|||||||
.AddInteractiveServerComponents()
|
.AddInteractiveServerComponents()
|
||||||
.AddInteractiveWebAssemblyComponents();
|
.AddInteractiveWebAssemblyComponents();
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
|
||||||
|
|
||||||
// Enable detailed circuit errors for Server‑Side Blazor components
|
// Enable detailed circuit errors for Server‑Side Blazor components
|
||||||
builder.Services.AddServerSideBlazor()
|
builder.Services.AddServerSideBlazor()
|
||||||
.AddCircuitOptions(options =>
|
.AddCircuitOptions(options =>
|
||||||
@@ -49,7 +48,7 @@ builder.Services.AddHttpClient("NexusAPI", client =>
|
|||||||
});
|
});
|
||||||
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
|
builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("NexusAPI"));
|
||||||
|
|
||||||
builder.Services.AddScoped<IIdentityService, IdentityService>();
|
builder.Services.AddScoped<IIdentityService, NexusReader.UI.Shared.Services.IdentityService>();
|
||||||
builder.Services.AddScoped<NexusAuthenticationStateProvider>();
|
builder.Services.AddScoped<NexusAuthenticationStateProvider>();
|
||||||
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<NexusAuthenticationStateProvider>());
|
builder.Services.AddScoped<AuthenticationStateProvider>(sp => sp.GetRequiredService<NexusAuthenticationStateProvider>());
|
||||||
builder.Services.AddCascadingAuthenticationState();
|
builder.Services.AddCascadingAuthenticationState();
|
||||||
@@ -59,14 +58,12 @@ builder.Services.AddInfrastructure(builder.Configuration);
|
|||||||
|
|
||||||
// Authorization Policies
|
// Authorization Policies
|
||||||
builder.Services.AddScoped<IAuthorizationHandler, TokenLimitHandler>();
|
builder.Services.AddScoped<IAuthorizationHandler, TokenLimitHandler>();
|
||||||
builder.Services.AddAuthorization(options =>
|
builder.Services.AddAuthorizationBuilder()
|
||||||
{
|
.AddPolicy("ProUser", policy => policy.RequireClaim("Plan", "Pro", "Enterprise"))
|
||||||
options.AddPolicy("ProUser", policy => policy.RequireClaim("Plan", "Pro", "Enterprise"));
|
.AddPolicy("HasAvailableTokens", policy => policy.AddRequirements(new TokenLimitRequirement()));
|
||||||
options.AddPolicy("HasAvailableTokens", policy => policy.AddRequirements(new TokenLimitRequirement()));
|
|
||||||
});
|
|
||||||
|
|
||||||
// Billing & Stripe
|
// Billing & Stripe
|
||||||
builder.Services.AddScoped<IBillingService, BillingService>();
|
builder.Services.AddScoped<IBillingService, NexusReader.Infrastructure.Services.BillingService>();
|
||||||
|
|
||||||
// Authentication
|
// Authentication
|
||||||
builder.Services.AddAuthentication(options =>
|
builder.Services.AddAuthentication(options =>
|
||||||
@@ -127,22 +124,37 @@ using (var scope = app.Services.CreateScope())
|
|||||||
for (int i = 0; i < maxRetries; i++)
|
for (int i = 0; i < maxRetries; i++)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
{
|
||||||
|
if (logger.IsEnabled(LogLevel.Information))
|
||||||
{
|
{
|
||||||
logger.LogInformation("Próba połączenia z bazą danych (próba {Attempt}/{MaxRetries})...", i + 1, maxRetries);
|
logger.LogInformation("Próba połączenia z bazą danych (próba {Attempt}/{MaxRetries})...", i + 1, maxRetries);
|
||||||
|
}
|
||||||
|
|
||||||
await dbContext.Database.MigrateAsync();
|
await dbContext.Database.MigrateAsync();
|
||||||
await DbInitializer.SeedAsync(services);
|
await DbInitializer.SeedAsync(services);
|
||||||
|
|
||||||
|
if (logger.IsEnabled(LogLevel.Information))
|
||||||
|
{
|
||||||
logger.LogInformation("Baza danych zainicjowana pomyślnie.");
|
logger.LogInformation("Baza danych zainicjowana pomyślnie.");
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
catch (Npgsql.NpgsqlException ex) when (i < maxRetries - 1)
|
catch (Npgsql.NpgsqlException ex) when (i < maxRetries - 1)
|
||||||
{
|
{
|
||||||
logger.LogWarning("Błąd połączenia z bazą danych: {Message}. Ponowna próba za {Delay}ms...", ex.Message, delayMs);
|
if (logger.IsEnabled(LogLevel.Warning))
|
||||||
|
{
|
||||||
|
logger.LogWarning(ex, "Błąd połączenia z bazą danych. Ponowna próba za {Delay}ms...", delayMs);
|
||||||
|
}
|
||||||
|
|
||||||
await Task.Delay(delayMs);
|
await Task.Delay(delayMs);
|
||||||
delayMs *= 2; // Exponential backoff
|
delayMs *= 2; // Exponential backoff
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (logger.IsEnabled(LogLevel.Critical))
|
||||||
{
|
{
|
||||||
logger.LogCritical(ex, "Krytyczny błąd podczas inicjalizacji bazy danych.");
|
logger.LogCritical(ex, "Krytyczny błąd podczas inicjalizacji bazy danych.");
|
||||||
|
}
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,7 +181,6 @@ app.UseAntiforgery();
|
|||||||
app.UseAuthentication();
|
app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
app.MapStaticAssets();
|
app.MapStaticAssets();
|
||||||
app.MapControllers();
|
|
||||||
|
|
||||||
// API endpoint for WASM client to fetch EPUB content
|
// API endpoint for WASM client to fetch EPUB content
|
||||||
app.MapGet("/api/epub/{index}", async (int index, IEpubService epubService) =>
|
app.MapGet("/api/epub/{index}", async (int index, IEpubService epubService) =>
|
||||||
@@ -177,40 +188,115 @@ app.MapGet("/api/epub/{index}", async (int index, IEpubService epubService) =>
|
|||||||
var result = await epubService.GetEpubContentAsync(index);
|
var result = await epubService.GetEpubContentAsync(index);
|
||||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||||
|
|
||||||
var errorMsg = result.Errors.FirstOrDefault()?.Message ?? "Unknown server error";
|
var errorMsg = result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error";
|
||||||
return Results.BadRequest(errorMsg);
|
return Results.BadRequest(errorMsg);
|
||||||
});
|
}).RequireAuthorization();
|
||||||
|
|
||||||
app.MapPost("/api/knowledge", async (KnowledgeRequest request, IKnowledgeService knowledgeService) =>
|
var knowledgeApi = app.MapGroup("/api/knowledge").RequireAuthorization("HasAvailableTokens");
|
||||||
|
|
||||||
|
knowledgeApi.MapPost("/", async (KnowledgeRequest request, IKnowledgeService knowledgeService) =>
|
||||||
{
|
{
|
||||||
var result = await knowledgeService.GetKnowledgeAsync(request.Text);
|
var result = await knowledgeService.GetKnowledgeAsync(request.Text);
|
||||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||||
return Results.BadRequest(result.Errors.FirstOrDefault()?.Message ?? "Unknown server error");
|
return Results.BadRequest(result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.MapPost("/api/knowledge/graph", async (KnowledgeRequest request, IKnowledgeService knowledgeService) =>
|
knowledgeApi.MapPost("/graph", async (KnowledgeRequest request, IKnowledgeService knowledgeService) =>
|
||||||
{
|
{
|
||||||
var result = await knowledgeService.GetGraphDataAsync(request.Text);
|
var result = await knowledgeService.GetGraphDataAsync(request.Text);
|
||||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||||
return Results.BadRequest(result.Errors.FirstOrDefault()?.Message ?? "Unknown server error");
|
return Results.BadRequest(result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.MapPost("/api/knowledge/summary", async (KnowledgeRequest request, IKnowledgeService knowledgeService) =>
|
knowledgeApi.MapPost("/summary", async (KnowledgeRequest request, IKnowledgeService knowledgeService) =>
|
||||||
{
|
{
|
||||||
var result = await knowledgeService.GetSummaryAndQuizAsync(request.Text);
|
var result = await knowledgeService.GetSummaryAndQuizAsync(request.Text);
|
||||||
if (result.IsSuccess) return Results.Ok(result.Value);
|
if (result.IsSuccess) return Results.Ok(result.Value);
|
||||||
return Results.BadRequest(result.Errors.FirstOrDefault()?.Message ?? "Unknown server error");
|
return Results.BadRequest(result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error");
|
||||||
});
|
});
|
||||||
|
|
||||||
app.MapDelete("/api/knowledge", async (IKnowledgeService knowledgeService) =>
|
knowledgeApi.MapDelete("/", async (IKnowledgeService knowledgeService) =>
|
||||||
{
|
{
|
||||||
var result = await knowledgeService.ClearCacheAsync();
|
var result = await knowledgeService.ClearCacheAsync();
|
||||||
if (result.IsSuccess) return Results.Ok();
|
if (result.IsSuccess) return Results.Ok();
|
||||||
|
|
||||||
var errorMsg = result.Errors.FirstOrDefault()?.Message ?? "Unknown server error";
|
var errorMsg = result.Errors.Count > 0 ? result.Errors[0].Message : "Unknown server error";
|
||||||
return Results.BadRequest(errorMsg);
|
return Results.BadRequest(errorMsg);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
app.MapPost("/api/StripeWebhook", async (HttpContext context, UserManager<NexusUser> userManager, IConfiguration configuration) =>
|
||||||
|
{
|
||||||
|
var json = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
||||||
|
var webhookSecret = configuration["Stripe:WebhookSecret"] ?? "";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var stripeEvent = EventUtility.ConstructEvent(
|
||||||
|
json,
|
||||||
|
context.Request.Headers["Stripe-Signature"],
|
||||||
|
webhookSecret
|
||||||
|
);
|
||||||
|
|
||||||
|
switch (stripeEvent.Type)
|
||||||
|
{
|
||||||
|
case EventTypes.CheckoutSessionCompleted:
|
||||||
|
var session = stripeEvent.Data.Object as Stripe.Checkout.Session;
|
||||||
|
await HandleSubscriptionSuccess(session?.CustomerEmail, session?.Metadata, userManager);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EventTypes.CustomerSubscriptionUpdated:
|
||||||
|
var subscription = stripeEvent.Data.Object as Stripe.Subscription;
|
||||||
|
await HandleSubscriptionSuccess(subscription?.Metadata["CustomerEmail"], subscription?.Metadata, userManager);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case EventTypes.CustomerSubscriptionDeleted:
|
||||||
|
var deletedSubscription = stripeEvent.Data.Object as Stripe.Subscription;
|
||||||
|
await HandleSubscriptionCancellation(deletedSubscription?.Metadata["CustomerEmail"], userManager);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Results.Ok();
|
||||||
|
}
|
||||||
|
catch (StripeException e)
|
||||||
|
{
|
||||||
|
return Results.BadRequest(e.Message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async Task HandleSubscriptionSuccess(string? email, Dictionary<string, string>? metadata, UserManager<NexusUser> userManager)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(email)) return;
|
||||||
|
|
||||||
|
var user = await userManager.FindByEmailAsync(email);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
var plan = metadata?.GetValueOrDefault("Plan") ?? "Pro";
|
||||||
|
|
||||||
|
user.CurrentPlan = plan;
|
||||||
|
user.AITokenLimit = plan.ToLower() switch
|
||||||
|
{
|
||||||
|
"pro" => 50000,
|
||||||
|
"enterprise" => 500000,
|
||||||
|
_ => 10000 // default for unknown or free
|
||||||
|
};
|
||||||
|
|
||||||
|
await userManager.UpdateAsync(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task HandleSubscriptionCancellation(string? email, UserManager<NexusUser> userManager)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(email)) return;
|
||||||
|
|
||||||
|
var user = await userManager.FindByEmailAsync(email);
|
||||||
|
if (user != null)
|
||||||
|
{
|
||||||
|
user.CurrentPlan = "Free";
|
||||||
|
user.AITokenLimit = 5000; // Free tier limit
|
||||||
|
await userManager.UpdateAsync(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
app.MapGroup("/identity").MapIdentityApi<NexusUser>();
|
app.MapGroup("/identity").MapIdentityApi<NexusUser>();
|
||||||
|
|
||||||
app.MapGet("/identity/login/google", (string? returnUrl) =>
|
app.MapGet("/identity/login/google", (string? returnUrl) =>
|
||||||
@@ -241,6 +327,7 @@ app.MapGet("/identity/callback/google", async (
|
|||||||
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
|
var email = info.Principal.FindFirstValue(ClaimTypes.Email);
|
||||||
if (email != null)
|
if (email != null)
|
||||||
{
|
{
|
||||||
|
// TODO: REV-5 - Consider redirecting to Terms of Service / Onboarding before final provisioning
|
||||||
var user = new NexusUser { UserName = email, Email = email, EmailConfirmed = true };
|
var user = new NexusUser { UserName = email, Email = email, EmailConfirmed = true };
|
||||||
var createResult = await userManager.CreateAsync(user);
|
var createResult = await userManager.CreateAsync(user);
|
||||||
if (createResult.Succeeded)
|
if (createResult.Succeeded)
|
||||||
@@ -259,31 +346,23 @@ app.MapGet("/identity/profile", async (ClaimsPrincipal user, UserManager<NexusUs
|
|||||||
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
var userId = user.FindFirstValue(ClaimTypes.NameIdentifier);
|
||||||
if (userId == null) return Results.Unauthorized();
|
if (userId == null) return Results.Unauthorized();
|
||||||
|
|
||||||
var nexusUser = await dbContext.Users
|
var profile = await dbContext.Users
|
||||||
.Include(u => u.Ebooks)
|
.Where(u => u.Id == userId)
|
||||||
.Include(u => u.QuizResults)
|
.Select(u => new
|
||||||
.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,
|
u.Email,
|
||||||
nexusUser.AITokenLimit,
|
u.AITokenLimit,
|
||||||
nexusUser.AITokensUsed,
|
u.AITokensUsed,
|
||||||
nexusUser.CurrentPlan,
|
u.CurrentPlan,
|
||||||
nexusUser.TenantId,
|
u.TenantId,
|
||||||
AverageQuizScore = avgScore,
|
AverageQuizScore = u.QuizResults.Any() ? (int?)u.QuizResults.Average(q => q.Percentage) ?? 0 : 0,
|
||||||
LastReadBookTitle = lastReadBook
|
LastReadBookTitle = u.Ebooks.OrderByDescending(e => e.LastReadDate).Select(e => e.Title).FirstOrDefault() ?? "None"
|
||||||
});
|
})
|
||||||
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
|
if (profile == null) return Results.NotFound();
|
||||||
|
|
||||||
|
return Results.Ok(profile);
|
||||||
}).RequireAuthorization();
|
}).RequireAuthorization();
|
||||||
|
|
||||||
app.MapRazorComponents<App>()
|
app.MapRazorComponents<App>()
|
||||||
|
|||||||
Reference in New Issue
Block a user