diff --git a/src/NexusReader.UI.Shared/Pages/Account/Login.razor b/src/NexusReader.UI.Shared/Pages/Account/Login.razor index 60ea128..3feec48 100644 --- a/src/NexusReader.UI.Shared/Pages/Account/Login.razor +++ b/src/NexusReader.UI.Shared/Pages/Account/Login.razor @@ -122,12 +122,24 @@ try { - var success = await IdentityService.LoginAsync(_loginModel.Email, _loginModel.Password, _loginModel.RememberMe); - if (success) NavigationManager.NavigateTo("/"); - else _errorMessage = "Nieprawidłowy e-mail lub hasło."; + var result = await IdentityService.LoginAsync(_loginModel.Email, _loginModel.Password, _loginModel.RememberMe); + if (result.IsSuccess) + { + NavigationManager.NavigateTo("/"); + } + else + { + _errorMessage = result.Errors.FirstOrDefault()?.Message ?? "Nieprawidłowy e-mail lub hasło."; + } + } + catch (Exception ex) + { + _errorMessage = $"Wystąpił błąd logowania: {ex.Message}."; + } + finally + { + _isSubmitting = false; } - catch (Exception ex) { _errorMessage = $"Wystąpił błąd logowania: {ex.Message}."; } - finally { _isSubmitting = false; } } private void HandleGoogleLogin() => NavigationManager.NavigateTo("identity/login/google", forceLoad: true); diff --git a/src/NexusReader.UI.Shared/Pages/Account/Profile.razor b/src/NexusReader.UI.Shared/Pages/Account/Profile.razor index 511ce76..370aa41 100644 --- a/src/NexusReader.UI.Shared/Pages/Account/Profile.razor +++ b/src/NexusReader.UI.Shared/Pages/Account/Profile.razor @@ -110,7 +110,11 @@ protected override async Task OnInitializedAsync() { - _profile = await IdentityService.GetProfileAsync(); + var result = await IdentityService.GetProfileAsync(); + if (result.IsSuccess) + { + _profile = result.Value; + } StateHasChanged(); } diff --git a/src/NexusReader.UI.Shared/Pages/Account/Profile.razor.css b/src/NexusReader.UI.Shared/Pages/Account/Profile.razor.css index 144336d..2cec44a 100644 --- a/src/NexusReader.UI.Shared/Pages/Account/Profile.razor.css +++ b/src/NexusReader.UI.Shared/Pages/Account/Profile.razor.css @@ -119,15 +119,21 @@ } .glass-panel { - background: rgba(255, 255, 255, 0.03); - backdrop-filter: blur(12px); - -webkit-backdrop-filter: blur(12px); + background: rgba(20, 20, 20, 0.8); /* Fallback */ border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 24px; padding: 32px; transition: all 0.3s ease; } +@supports (backdrop-filter: blur(12px)) { + .glass-panel { + background: rgba(255, 255, 255, 0.03); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + } +} + .glass-panel:hover { background: rgba(255, 255, 255, 0.05); border-color: rgba(0, 255, 153, 0.2); diff --git a/src/NexusReader.UI.Shared/Pages/Account/Register.razor b/src/NexusReader.UI.Shared/Pages/Account/Register.razor index 6a32d64..d50cb85 100644 --- a/src/NexusReader.UI.Shared/Pages/Account/Register.razor +++ b/src/NexusReader.UI.Shared/Pages/Account/Register.razor @@ -81,17 +81,32 @@ try { - var success = await IdentityService.RegisterAsync(_registerModel.Email, _registerModel.Password); - if (success) + var regResult = await IdentityService.RegisterAsync(_registerModel.Email, _registerModel.Password); + if (regResult.IsSuccess) { - var loginSuccess = await IdentityService.LoginAsync(_registerModel.Email, _registerModel.Password); - if (loginSuccess) NavigationManager.NavigateTo("/"); - else NavigationManager.NavigateTo("/account/login"); + var loginResult = await IdentityService.LoginAsync(_registerModel.Email, _registerModel.Password); + if (loginResult.IsSuccess) + { + NavigationManager.NavigateTo("/"); + } + else + { + NavigationManager.NavigateTo("/account/login"); + } + } + else + { + _errorMessage = regResult.Errors.FirstOrDefault()?.Message ?? "Rejestracja nie powiodła się."; } - else { _errorMessage = "Rejestracja nie powiodła się."; } } - catch (Exception) { _errorMessage = "Wystąpił błąd podczas rejestracji."; } - finally { _isSubmitting = false; } + catch (Exception) + { + _errorMessage = "Wystąpił błąd podczas rejestracji."; + } + finally + { + _isSubmitting = false; + } } public class RegisterModel diff --git a/src/NexusReader.UI.Shared/Pages/Dashboard.razor b/src/NexusReader.UI.Shared/Pages/Dashboard.razor index 5158295..45e99de 100644 --- a/src/NexusReader.UI.Shared/Pages/Dashboard.razor +++ b/src/NexusReader.UI.Shared/Pages/Dashboard.razor @@ -138,6 +138,10 @@ protected override async Task OnInitializedAsync() { - _profile = await IdentityService.GetProfileAsync(); + var result = await IdentityService.GetProfileAsync(); + if (result.IsSuccess) + { + _profile = result.Value; + } } } diff --git a/src/NexusReader.UI.Shared/Pages/Dashboard.razor.css b/src/NexusReader.UI.Shared/Pages/Dashboard.razor.css index 94ca25b..eaf4d28 100644 --- a/src/NexusReader.UI.Shared/Pages/Dashboard.razor.css +++ b/src/NexusReader.UI.Shared/Pages/Dashboard.razor.css @@ -136,14 +136,20 @@ } .glass-panel { - background: rgba(255, 255, 255, 0.03); - backdrop-filter: blur(10px); + background: rgba(20, 20, 20, 0.8); /* Fallback for browsers without backdrop-filter */ border: 1px solid rgba(255, 255, 255, 0.05); border-radius: 20px; padding: 1.5rem; transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); } +@supports (backdrop-filter: blur(10px)) { + .glass-panel { + background: rgba(255, 255, 255, 0.03); + backdrop-filter: blur(10px); + } +} + .glass-panel:hover { background: rgba(255, 255, 255, 0.05); border-color: rgba(0, 255, 153, 0.2); diff --git a/src/NexusReader.UI.Shared/Services/IdentityService.cs b/src/NexusReader.UI.Shared/Services/IdentityService.cs index 79d71c8..11d7e10 100644 --- a/src/NexusReader.UI.Shared/Services/IdentityService.cs +++ b/src/NexusReader.UI.Shared/Services/IdentityService.cs @@ -2,16 +2,18 @@ using System.Net.Http.Json; using Microsoft.AspNetCore.Components.Authorization; using NexusReader.Application.Abstractions.Services; using NexusReader.Application.DTOs.User; +using FluentResults; namespace NexusReader.UI.Shared.Services; public interface IIdentityService { - Task RegisterAsync(string email, string password); - Task LoginAsync(string email, string password, bool rememberMe = false); - Task LogoutAsync(); - Task GetProfileAsync(); - Task RefreshTokenAsync(); + event Func? OnStateInvalidated; + Task RegisterAsync(string email, string password); + Task LoginAsync(string email, string password, bool rememberMe = false); + Task LogoutAsync(); + Task> GetProfileAsync(); + Task RefreshTokenAsync(); } public record UserProfile( @@ -37,6 +39,9 @@ public class IdentityService : IIdentityService private const string RefreshTokenKey = "nexus_refresh_token"; private Task? _profileTask; private UserProfile? _cachedProfile; + private DateTime _lastFetchAttempt = DateTime.MinValue; + + public event Func? OnStateInvalidated; public IdentityService( HttpClient httpClient, @@ -48,92 +53,114 @@ public class IdentityService : IIdentityService _authStateProvider = authStateProvider; } - public async Task RegisterAsync(string email, string password) + public async Task RegisterAsync(string email, string password) { - var response = await _httpClient.PostAsJsonAsync("identity/register", new { email, password }); - return response.IsSuccessStatusCode; + try + { + var response = await _httpClient.PostAsJsonAsync("identity/register", new { email, password }); + return response.IsSuccessStatusCode ? Result.Ok() : Result.Fail("Rejestracja nie powiodła się."); + } + catch (Exception ex) + { + return Result.Fail(new Error("Błąd sieci podczas rejestracji.").CausedBy(ex)); + } } - public async Task LoginAsync(string email, string password, bool rememberMe = false) + public async Task LoginAsync(string email, string password, bool rememberMe = false) { - var response = await _httpClient.PostAsJsonAsync("identity/login?useCookies=true", new { email, password }); - - if (response.IsSuccessStatusCode) + try { - _cachedProfile = null; // Clear cache to force fresh fetch - LoginResponse? result = null; - try - { - result = await response.Content.ReadFromJsonAsync(); - } - catch (System.Text.Json.JsonException) - { - // Expected if useCookies=true and body is empty - } - - if (result != null && !string.IsNullOrEmpty(result.AccessToken)) - { - await _storageService.SaveSecureString(TokenKey, result.AccessToken); - if (!string.IsNullOrEmpty(result.RefreshToken)) - { - await _storageService.SaveSecureString(RefreshTokenKey, result.RefreshToken); - } - } - - // Always try to fetch profile after successful login (either via token or cookie) - var profile = await GetProfileAsync(); - if (profile != null) - { - await _storageService.SaveSecureString("nexus_user_email", profile.Email); - await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); - - (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); - return true; - } + var response = await _httpClient.PostAsJsonAsync("identity/login?useCookies=true", new { email, password }); - // If we have a successful status code but can't get the profile, - // we might still be logged in via cookie. - // We should try to notify with whatever info we have. if (response.IsSuccessStatusCode) { - (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(email, "unknown"); - return true; - } - } + _cachedProfile = null; + LoginResponse? result = null; + try + { + result = await response.Content.ReadFromJsonAsync(); + } + catch (System.Text.Json.JsonException) { } - return false; + 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 as NexusAuthenticationStateProvider)?.ClearCache(); + if (OnStateInvalidated != null) await OnStateInvalidated.Invoke(); + + var profileResult = await GetProfileAsync(); + if (profileResult.IsSuccess) + { + var profile = profileResult.Value; + await _storageService.SaveSecureString("nexus_user_email", profile.Email); + await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); + (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); + } + else + { + (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(email, "unknown"); + } + + return Result.Ok(); + } + return Result.Fail("Nieprawidłowy e-mail lub hasło."); + } + catch (Exception ex) + { + return Result.Fail(new Error("Błąd połączenia z serwerem.").CausedBy(ex)); + } } - public async Task LogoutAsync() + public async Task LogoutAsync() { - _cachedProfile = null; - if (System.OperatingSystem.IsBrowser()) + try { - await _storageService.SaveSecureString(TokenKey, ""); - await _storageService.SaveSecureString(RefreshTokenKey, ""); - await _storageService.SaveSecureString("nexus_user_email", ""); - await _storageService.SaveSecureString("nexus_user_tenant", ""); + _cachedProfile = null; + if (System.OperatingSystem.IsBrowser()) + { + await _storageService.SaveSecureString(TokenKey, ""); + await _storageService.SaveSecureString(RefreshTokenKey, ""); + await _storageService.SaveSecureString("nexus_user_email", ""); + await _storageService.SaveSecureString("nexus_user_tenant", ""); + } + + if (OnStateInvalidated != null) await OnStateInvalidated.Invoke(); + (_authStateProvider as NexusAuthenticationStateProvider)?.ClearCache(); + (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserLogout(); + + return Result.Ok(); + } + catch (Exception ex) + { + return Result.Fail(new Error("Błąd podczas wylogowywania.").CausedBy(ex)); } - (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserLogout(); } - public async Task GetProfileAsync() + public async Task> GetProfileAsync() { if (_cachedProfile != null) { - return _cachedProfile; + return Result.Ok(_cachedProfile); } if (_profileTask != null) { - return await _profileTask; + var p = await _profileTask; + return p != null ? Result.Ok(p) : Result.Fail("Nie znaleziono profilu."); } _profileTask = GetProfileInternalAsync(); - return await _profileTask; + var result = await _profileTask; + return result != null ? Result.Ok(result) : Result.Fail("Błąd podczas pobierania profilu."); } - private DateTime _lastFetchAttempt = DateTime.MinValue; + private async Task GetProfileInternalAsync() { @@ -183,39 +210,50 @@ public class IdentityService : IIdentityService } } - public async Task RefreshTokenAsync() + public async Task 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) + try { - var loginResult = await response.Content.ReadFromJsonAsync(); - if (loginResult != null && !string.IsNullOrEmpty(loginResult.AccessToken)) + var result = await _storageService.GetSecureString(RefreshTokenKey); + var refreshToken = result.IsSuccess ? result.Value : null; + + if (string.IsNullOrEmpty(refreshToken)) return Result.Fail("Brak tokena odświeżania."); + + var response = await _httpClient.PostAsJsonAsync("identity/refresh", new { refreshToken }); + + if (response.IsSuccessStatusCode) { - await _storageService.SaveSecureString(TokenKey, loginResult.AccessToken); - if (!string.IsNullOrEmpty(loginResult.RefreshToken)) + var loginResult = await response.Content.ReadFromJsonAsync(); + if (loginResult != null && !string.IsNullOrEmpty(loginResult.AccessToken)) { - await _storageService.SaveSecureString(RefreshTokenKey, loginResult.RefreshToken); - } + await _storageService.SaveSecureString(TokenKey, loginResult.AccessToken); + if (!string.IsNullOrEmpty(loginResult.RefreshToken)) + { + await _storageService.SaveSecureString(RefreshTokenKey, loginResult.RefreshToken); + } - var profile = await GetProfileAsync(); - if (profile != null) - { - await _storageService.SaveSecureString("nexus_user_email", profile.Email); - await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); - (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); - } + _cachedProfile = null; + (_authStateProvider as NexusAuthenticationStateProvider)?.ClearCache(); + if (OnStateInvalidated != null) await OnStateInvalidated.Invoke(); - return true; + var profileResult = await GetProfileAsync(); + if (profileResult.IsSuccess) + { + var profile = profileResult.Value; + await _storageService.SaveSecureString("nexus_user_email", profile.Email); + await _storageService.SaveSecureString("nexus_user_tenant", profile.TenantId.ToString()); + (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); + } + + return Result.Ok(); + } } + return Result.Fail("Sesja wygasła."); + } + catch (Exception ex) + { + return Result.Fail(new Error("Błąd odświeżania sesji.").CausedBy(ex)); } - - return false; } private class LoginResponse diff --git a/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs b/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs index 20bfe4c..75f2daa 100644 --- a/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs +++ b/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs @@ -15,6 +15,12 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider _storageService = storageService; } + public void ClearCache() + { + _cachedState = null; + NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); + } + private AuthenticationState? _cachedState; public override async Task GetAuthenticationStateAsync() diff --git a/src/NexusReader.UI.Shared/wwwroot/app.css b/src/NexusReader.UI.Shared/wwwroot/app.css index df8d6f2..8de4576 100644 --- a/src/NexusReader.UI.Shared/wwwroot/app.css +++ b/src/NexusReader.UI.Shared/wwwroot/app.css @@ -19,6 +19,22 @@ --nexus-transition: 0.4s cubic-bezier(0.4, 0, 0.2, 1); } +/* Global Glassmorphism with Fallback */ +.glass-panel { + background: rgba(20, 20, 20, 0.85); /* Darker fallback for readability */ + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: 20px; + padding: 1.5rem; + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +@supports (backdrop-filter: blur(10px)) { + .glass-panel { + background: rgba(255, 255, 255, 0.03); + backdrop-filter: blur(10px); + } +} + .theme-light { --nexus-bg: var(--nexus-paper); diff --git a/src/NexusReader.Web.New/Services/ServerIdentityService.cs b/src/NexusReader.Web.New/Services/ServerIdentityService.cs index 2527b02..8e175b2 100644 --- a/src/NexusReader.Web.New/Services/ServerIdentityService.cs +++ b/src/NexusReader.Web.New/Services/ServerIdentityService.cs @@ -1,4 +1,5 @@ using System.Security.Claims; +using FluentResults; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using NexusReader.Application.DTOs.User; @@ -14,6 +15,8 @@ public class ServerIdentityService : IIdentityService private readonly IHttpContextAccessor _httpContextAccessor; private readonly IDbContextFactory _dbContextFactory; + public event Func? OnStateInvalidated; + public ServerIdentityService( UserManager userManager, IHttpContextAccessor httpContextAccessor, @@ -24,24 +27,24 @@ public class ServerIdentityService : IIdentityService _dbContextFactory = dbContextFactory; } - public Task LoginAsync(string email, string password, bool rememberMe = false) + public Task LoginAsync(string email, string password, bool rememberMe = false) => throw new NotSupportedException("Use standard Identity endpoints for login on server."); - public Task LogoutAsync() + public Task LogoutAsync() => throw new NotSupportedException("Use standard Identity endpoints for logout on server."); - public Task RegisterAsync(string email, string password) + public Task RegisterAsync(string email, string password) => throw new NotSupportedException("Use standard Identity endpoints for registration on server."); - public Task RefreshTokenAsync() => Task.FromResult(true); + public Task RefreshTokenAsync() => Task.FromResult(Result.Ok()); - public async Task GetProfileAsync() + public async Task> GetProfileAsync() { var user = _httpContextAccessor.HttpContext?.User; - if (user == null || !user.Identity?.IsAuthenticated == true) return null; + if (user == null || !user.Identity?.IsAuthenticated == true) return Result.Fail("Not authenticated."); var userId = user.FindFirstValue(ClaimTypes.NameIdentifier); - if (userId == null) return null; + if (userId == null) return Result.Fail("User ID not found."); using var dbContext = await _dbContextFactory.CreateDbContextAsync(); @@ -73,6 +76,6 @@ public class ServerIdentityService : IIdentityService )) .FirstOrDefaultAsync(); - return profile; + return profile != null ? Result.Ok(profile) : Result.Fail("Profile not found."); } }