using System.Security.Claims; using FluentResults; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using NexusReader.Application.DTOs.User; using NexusReader.Data.Persistence; using NexusReader.Domain.Entities; using NexusReader.Application.Queries.User; using MediatR; using NexusReader.Application.Constants; using NexusReader.Application.Abstractions.Services; using Microsoft.AspNetCore.Components.Authorization; namespace NexusReader.Web.Services; public class ServerIdentityService : IIdentityService { private readonly UserManager _userManager; private readonly SignInManager _signInManager; private readonly IHttpContextAccessor _httpContextAccessor; private readonly IMediator _mediator; private readonly INativeStorageService _storageService; private readonly AuthenticationStateProvider _authStateProvider; public event Func? OnStateInvalidated; public ServerIdentityService( UserManager userManager, SignInManager signInManager, IHttpContextAccessor httpContextAccessor, IMediator mediator, INativeStorageService storageService, AuthenticationStateProvider authStateProvider) { _userManager = userManager; _signInManager = signInManager; _httpContextAccessor = httpContextAccessor; _mediator = mediator; _storageService = storageService; _authStateProvider = authStateProvider; } public async Task LoginAsync(string email, string password, bool rememberMe = false) { try { var user = await _userManager.FindByEmailAsync(email); if (user == null) return Result.Fail("Nieprawidłowy e-mail lub hasło."); // Check if account is locked if (await _userManager.IsLockedOutAsync(user)) return Result.Fail("Konto zostało zablokowane."); // Check password var isCorrect = await _userManager.CheckPasswordAsync(user, password); if (!isCorrect) { await _userManager.AccessFailedAsync(user); return Result.Fail("Nieprawidłowy e-mail lub hasło."); } // Reset access failed count on success await _userManager.ResetAccessFailedCountAsync(user); // In Blazor Interactive Server, we cannot use PasswordSignInAsync directly // because headers are read-only once the circuit is established. // We return success here to indicate credentials are valid. // The UI will then perform a POST redirect to /account/login-form to set cookies. return Result.Ok(); } catch (Exception ex) { return Result.Fail(new Error($"Błąd podczas weryfikacji poświadczeń: {ex.Message}").CausedBy(ex)); } } public async Task LogoutAsync() { // Logout via SignalR is also problematic for cookie clearing. // The UI should redirect to /account/logout-form return Result.Ok(); } public async Task RegisterAsync(string email, string password) { try { var user = new NexusUser { UserName = email, Email = email, SubscriptionPlanId = SubscriptionPlan.FreeId, TenantId = "global" }; var result = await _userManager.CreateAsync(user, password); if (result.Succeeded) { // Similar to Login, we return success but don't sign in here. return Result.Ok(); } return Result.Fail(result.Errors.Select(e => e.Description).FirstOrDefault() ?? "Rejestracja nie powiodła się."); } catch (Exception ex) { return Result.Fail(new Error($"Błąd podczas rejestracji na serwerze: {ex.Message}").CausedBy(ex)); } } public Task RefreshTokenAsync() => Task.FromResult(Result.Ok()); public async Task> GetProfileAsync() { var authState = await _authStateProvider.GetAuthenticationStateAsync(); var user = authState.User; if (user == null || !user.Identity?.IsAuthenticated == true) { user = _httpContextAccessor.HttpContext?.User; } if (user == null || !user.Identity?.IsAuthenticated == true) return Result.Fail("Not authenticated."); var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); if (userId == null) return Result.Fail("User ID not found."); var result = await _mediator.Send(new GetUserProfileQuery(userId)); if (result.IsFailed) return Result.Fail(result.Errors); return Result.Ok(result.Value); } public void ClearCache() { if (OnStateInvalidated != null) { _ = Task.Run(async () => { try { await OnStateInvalidated.Invoke(); } catch { // Ignore } }); } } }