Fix role change silently failing via @bind
The role <select> used a manual value=/@onchange pattern that parsed e.Value and returned silently when the parse failed, so changing a role did nothing and showed no message. Switch to @bind + @bind:after so the framework handles the enum conversion, and log/verify the persisted role in UpdateRoleAsync (now returns the previous role) for diagnosis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -86,12 +86,11 @@ else
|
|||||||
</td>
|
</td>
|
||||||
<td style="padding:8px">
|
<td style="padding:8px">
|
||||||
<select class="form-input" style="width:130px"
|
<select class="form-input" style="width:130px"
|
||||||
value="@user.Role"
|
@bind="user.Role" @bind:after="() => PersistRoleAsync(user)"
|
||||||
@onchange="e => OnRoleChange(user, e)"
|
|
||||||
disabled="@(user.Email == UserContext.Email)">
|
disabled="@(user.Email == UserContext.Email)">
|
||||||
@foreach (var role in Enum.GetValues<UserRole>())
|
@foreach (var role in Enum.GetValues<UserRole>())
|
||||||
{
|
{
|
||||||
<option value="@role" selected="@(user.Role == role)">@role</option>
|
<option value="@role">@role</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
</td>
|
</td>
|
||||||
@@ -193,16 +192,14 @@ else
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OnRoleChange(AppUser user, ChangeEventArgs e)
|
// Bound via @bind:after, so user.Role already holds the newly-selected value when this runs.
|
||||||
|
private async Task PersistRoleAsync(AppUser user)
|
||||||
{
|
{
|
||||||
if (!Enum.TryParse<UserRole>(e.Value?.ToString(), out var newRole)) return;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var oldRole = user.Role;
|
var oldRole = await UserService.UpdateRoleAsync(user.Id, user.Role);
|
||||||
await UserService.UpdateRoleAsync(user.Id, newRole);
|
|
||||||
user.Role = newRole;
|
|
||||||
await Audit.LogAsync("RoleChanged", "", Array.Empty<string>(),
|
await Audit.LogAsync("RoleChanged", "", Array.Empty<string>(),
|
||||||
$"Changed role for {user.Email} ({user.DisplayName}) from {oldRole} to {newRole}.");
|
$"Changed role for {user.Email} ({user.DisplayName}) from {oldRole} to {user.Role}.");
|
||||||
_message = string.Format(T["usermgmt.msg.roleupdated"], user.DisplayName);
|
_message = string.Format(T["usermgmt.msg.roleupdated"], user.DisplayName);
|
||||||
_isError = false;
|
_isError = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ public interface IUserService
|
|||||||
|
|
||||||
Task<AppUser?> GetByEmailAsync(string email);
|
Task<AppUser?> GetByEmailAsync(string email);
|
||||||
Task<IReadOnlyList<AppUser>> GetAllAsync();
|
Task<IReadOnlyList<AppUser>> GetAllAsync();
|
||||||
Task UpdateRoleAsync(string userId, UserRole role);
|
/// <summary>Persist a new role for the user. Returns the previous role (read from the store).</summary>
|
||||||
|
/// <exception cref="KeyNotFoundException">No user matches <paramref name="userId"/>.</exception>
|
||||||
|
Task<UserRole> UpdateRoleAsync(string userId, UserRole role);
|
||||||
Task DeleteAsync(string userId);
|
Task DeleteAsync(string userId);
|
||||||
|
|
||||||
/// <summary>Create a local password-based account. First user ever becomes Admin.</summary>
|
/// <summary>Create a local password-based account. First user ever becomes Admin.</summary>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Microsoft.AspNetCore.Identity;
|
using Microsoft.AspNetCore.Identity;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using SharepointToolbox.Web.Core.Models;
|
using SharepointToolbox.Web.Core.Models;
|
||||||
using SharepointToolbox.Web.Infrastructure.Persistence;
|
using SharepointToolbox.Web.Infrastructure.Persistence;
|
||||||
|
|
||||||
@@ -9,11 +10,13 @@ public class UserService : IUserService
|
|||||||
{
|
{
|
||||||
private readonly UserRepository _repo;
|
private readonly UserRepository _repo;
|
||||||
private readonly IPasswordHasher<AppUser> _hasher;
|
private readonly IPasswordHasher<AppUser> _hasher;
|
||||||
|
private readonly ILogger<UserService> _logger;
|
||||||
|
|
||||||
public UserService(UserRepository repo, IPasswordHasher<AppUser> hasher)
|
public UserService(UserRepository repo, IPasswordHasher<AppUser> hasher, ILogger<UserService> logger)
|
||||||
{
|
{
|
||||||
_repo = repo;
|
_repo = repo;
|
||||||
_hasher = hasher;
|
_hasher = hasher;
|
||||||
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<AppUser> ProvisionAsync(ClaimsPrincipal principal)
|
public async Task<AppUser> ProvisionAsync(ClaimsPrincipal principal)
|
||||||
@@ -56,13 +59,23 @@ public class UserService : IUserService
|
|||||||
|
|
||||||
public Task<IReadOnlyList<AppUser>> GetAllAsync() => _repo.LoadAsync();
|
public Task<IReadOnlyList<AppUser>> GetAllAsync() => _repo.LoadAsync();
|
||||||
|
|
||||||
public async Task UpdateRoleAsync(string userId, UserRole role)
|
public async Task<UserRole> UpdateRoleAsync(string userId, UserRole role)
|
||||||
{
|
{
|
||||||
var users = (await _repo.LoadAsync()).ToList();
|
var users = (await _repo.LoadAsync()).ToList();
|
||||||
var user = users.FirstOrDefault(u => u.Id == userId)
|
var user = users.FirstOrDefault(u => u.Id == userId)
|
||||||
?? throw new KeyNotFoundException($"User {userId} not found.");
|
?? throw new KeyNotFoundException($"User '{userId}' not found among {users.Count} stored users.");
|
||||||
|
|
||||||
|
var oldRole = user.Role;
|
||||||
user.Role = role;
|
user.Role = role;
|
||||||
await _repo.UpsertAsync(user);
|
await _repo.UpsertAsync(user);
|
||||||
|
|
||||||
|
// Verify the write landed by re-reading the row from disk.
|
||||||
|
var persisted = (await _repo.LoadAsync()).FirstOrDefault(u => u.Id == user.Id)?.Role;
|
||||||
|
_logger.LogInformation(
|
||||||
|
"UpdateRoleAsync: {Email} (id {Id}) {OldRole} → {NewRole}; persisted value now {Persisted}.",
|
||||||
|
user.Email, user.Id, oldRole, role, persisted);
|
||||||
|
|
||||||
|
return oldRole;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task DeleteAsync(string userId) => _repo.DeleteAsync(userId);
|
public Task DeleteAsync(string userId) => _repo.DeleteAsync(userId);
|
||||||
|
|||||||
Reference in New Issue
Block a user