From 0c3fccc91e9990228e1c5c16824e6fb6180c1dc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20Jasi=C5=84ski?= Date: Sun, 10 May 2026 20:24:59 +0200 Subject: [PATCH] feat: resolve role-based authorization by extracting Roles claim from JWT and storing in state provider --- .../DTOs/User/UserProfileDto.cs | 2 ++ .../User/GetUserProfileQueryHandler.cs | 6 +++- .../Constants/StorageKeys.cs | 1 + .../Services/IdentityService.cs | 24 +++++++++++---- .../NexusAuthenticationStateProvider.cs | 30 +++++++++++++++---- .../Services/ServerIdentityService.cs | 4 ++- 6 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/NexusReader.Application/DTOs/User/UserProfileDto.cs b/src/NexusReader.Application/DTOs/User/UserProfileDto.cs index b85ddfd..2918c92 100644 --- a/src/NexusReader.Application/DTOs/User/UserProfileDto.cs +++ b/src/NexusReader.Application/DTOs/User/UserProfileDto.cs @@ -17,6 +17,8 @@ public record UserProfileDto /// Summary of the last read book. /// public LastReadBookDto? LastReadBook { get; init; } + + public string[] Roles { get; init; } = Array.Empty(); } public record LastReadBookDto diff --git a/src/NexusReader.Application/Queries/User/GetUserProfileQueryHandler.cs b/src/NexusReader.Application/Queries/User/GetUserProfileQueryHandler.cs index b6fe031..92b04f3 100644 --- a/src/NexusReader.Application/Queries/User/GetUserProfileQueryHandler.cs +++ b/src/NexusReader.Application/Queries/User/GetUserProfileQueryHandler.cs @@ -48,7 +48,11 @@ public class GetUserProfileQueryHandler : IRequestHandler ur.UserId == u.Id) + .Join(dbContext.Roles, ur => ur.RoleId, r => r.Id, (ur, r) => r.Name!) + .ToArray() }) .FirstOrDefaultAsync(cancellationToken); diff --git a/src/NexusReader.UI.Shared/Constants/StorageKeys.cs b/src/NexusReader.UI.Shared/Constants/StorageKeys.cs index e5e1fc2..084d791 100644 --- a/src/NexusReader.UI.Shared/Constants/StorageKeys.cs +++ b/src/NexusReader.UI.Shared/Constants/StorageKeys.cs @@ -6,4 +6,5 @@ public static class StorageKeys public const string RefreshToken = "nexus_refresh_token"; public const string UserEmail = "nexus_user_email"; public const string UserTenant = "nexus_user_tenant"; + public const string UserRoles = "nexus_user_roles"; } diff --git a/src/NexusReader.UI.Shared/Services/IdentityService.cs b/src/NexusReader.UI.Shared/Services/IdentityService.cs index 8bc8821..0a4e672 100644 --- a/src/NexusReader.UI.Shared/Services/IdentityService.cs +++ b/src/NexusReader.UI.Shared/Services/IdentityService.cs @@ -23,7 +23,8 @@ public record UserProfile( Guid TenantId, SubscriptionPlanDto Plan, int AverageQuizScore, - LastReadBookDto? LastReadBook) + LastReadBookDto? LastReadBook, + string[] Roles) { // Helper properties for UI compatibility public string CurrentPlan => Plan?.Name ?? PlanConstants.DefaultPlanName; @@ -104,11 +105,15 @@ public class IdentityService : IIdentityService var profile = profileResult.Value; await _storageService.SaveSecureString(StorageKeys.UserEmail, profile.Email); await _storageService.SaveSecureString(StorageKeys.UserTenant, profile.TenantId.ToString()); - (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); + + var rolesStr = string.Join(",", profile.Roles ?? Array.Empty()); + await _storageService.SaveSecureString(StorageKeys.UserRoles, rolesStr); + + (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString(), rolesStr); } else { - (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(email, "unknown"); + (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(email, "unknown", ""); } return Result.Ok(); @@ -132,6 +137,7 @@ public class IdentityService : IIdentityService await _storageService.SaveSecureString(RefreshTokenKey, ""); await _storageService.SaveSecureString(StorageKeys.UserEmail, ""); await _storageService.SaveSecureString(StorageKeys.UserTenant, ""); + await _storageService.SaveSecureString(StorageKeys.UserRoles, ""); } if (OnStateInvalidated != null) await OnStateInvalidated.Invoke(); @@ -197,7 +203,11 @@ public class IdentityService : IIdentityService _cachedProfile = profile; await _storageService.SaveSecureString(StorageKeys.UserEmail, profile.Email); await _storageService.SaveSecureString(StorageKeys.UserTenant, profile.TenantId.ToString()); - (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); + + var rolesStr = string.Join(",", profile.Roles ?? Array.Empty()); + await _storageService.SaveSecureString(StorageKeys.UserRoles, rolesStr); + + (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString(), rolesStr); } return profile; } @@ -246,7 +256,11 @@ public class IdentityService : IIdentityService var profile = profileResult.Value; await _storageService.SaveSecureString(StorageKeys.UserEmail, profile.Email); await _storageService.SaveSecureString(StorageKeys.UserTenant, profile.TenantId.ToString()); - (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString()); + + var rolesStr = string.Join(",", profile.Roles ?? Array.Empty()); + await _storageService.SaveSecureString(StorageKeys.UserRoles, rolesStr); + + (_authStateProvider as NexusAuthenticationStateProvider)?.NotifyUserAuthentication(profile.Email, profile.TenantId.ToString(), rolesStr); } return Result.Ok(); diff --git a/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs b/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs index 42e5218..de299ee 100644 --- a/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs +++ b/src/NexusReader.UI.Shared/Services/NexusAuthenticationStateProvider.cs @@ -38,10 +38,15 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider { var emailResult = await _storageService.GetSecureString(StorageKeys.UserEmail); var tenantIdResult = await _storageService.GetSecureString(StorageKeys.UserTenant); + var rolesResult = await _storageService.GetSecureString(StorageKeys.UserRoles); if (emailResult.IsSuccess && !string.IsNullOrEmpty(emailResult.Value)) { - _cachedState = CreateState(emailResult.Value, tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown", "OpaqueBearer"); + _cachedState = CreateState( + emailResult.Value, + tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown", + "OpaqueBearer", + rolesResult.IsSuccess ? rolesResult.Value! : ""); return _cachedState; } } @@ -51,7 +56,12 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider if (storedEmailResult.IsSuccess && !string.IsNullOrEmpty(storedEmailResult.Value)) { var tenantIdResult = await _storageService.GetSecureString(StorageKeys.UserTenant); - _cachedState = CreateState(storedEmailResult.Value, tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown", "CookieAuth"); + var rolesResult = await _storageService.GetSecureString(StorageKeys.UserRoles); + _cachedState = CreateState( + storedEmailResult.Value, + tenantIdResult.IsSuccess ? tenantIdResult.Value! : "unknown", + "CookieAuth", + rolesResult.IsSuccess ? rolesResult.Value! : ""); return _cachedState; } @@ -67,7 +77,7 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider } } - private AuthenticationState CreateState(string email, string tenantId, string authType) + private AuthenticationState CreateState(string email, string tenantId, string authType, string rolesStr = "") { var claims = new List { @@ -75,13 +85,23 @@ public class NexusAuthenticationStateProvider : AuthenticationStateProvider new Claim(ClaimTypes.Email, email), new Claim("TenantId", tenantId) }; + + if (!string.IsNullOrEmpty(rolesStr)) + { + var roles = rolesStr.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var role in roles) + { + claims.Add(new Claim(ClaimTypes.Role, role.Trim())); + } + } + var identity = new ClaimsIdentity(claims, authType); return new AuthenticationState(new ClaimsPrincipal(identity)); } - public void NotifyUserAuthentication(string email, string tenantId) + public void NotifyUserAuthentication(string email, string tenantId, string rolesStr = "") { - _cachedState = CreateState(email, tenantId, "OpaqueBearer"); + _cachedState = CreateState(email, tenantId, "OpaqueBearer", rolesStr); NotifyAuthenticationStateChanged(Task.FromResult(_cachedState)); } diff --git a/src/NexusReader.Web.New/Services/ServerIdentityService.cs b/src/NexusReader.Web.New/Services/ServerIdentityService.cs index 9b52d88..f8f5e2a 100644 --- a/src/NexusReader.Web.New/Services/ServerIdentityService.cs +++ b/src/NexusReader.Web.New/Services/ServerIdentityService.cs @@ -48,6 +48,7 @@ public class ServerIdentityService : IIdentityService await _storageService.SaveSecureString(StorageKeys.RefreshToken, ""); await _storageService.SaveSecureString(StorageKeys.UserEmail, ""); await _storageService.SaveSecureString(StorageKeys.UserTenant, ""); + await _storageService.SaveSecureString(StorageKeys.UserRoles, ""); } catch { @@ -88,7 +89,8 @@ public class ServerIdentityService : IIdentityService dto.TenantId, dto.Plan, dto.AverageQuizScore, - dto.LastReadBook + dto.LastReadBook, + dto.Roles ); return Result.Ok(profile);