This commit is contained in:
2026-06-02 15:46:13 +02:00
25 changed files with 951 additions and 215 deletions
+116 -3
View File
@@ -2,14 +2,16 @@
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@inject IUserService UserService
@inject IUserContextAccessor UserContext
@inject IAuditService Audit
@inject NavigationManager Nav
@rendermode InteractiveServer
@using SharepointToolbox.Web.Core.Models
@using SharepointToolbox.Web.Services.Audit
@using SharepointToolbox.Web.Services.Auth
@using SharepointToolbox.Web.Services.Session
<h1 class="page-title">User Management</h1>
<p class="page-subtitle">Manage technician accounts and roles. Auto-provisioned on first OIDC login.</p>
<p class="page-subtitle">Manage technician accounts and roles. Entra users are auto-provisioned on first OIDC login; local users are created here.</p>
@if (!UserContext.IsAuthenticated || UserContext.Role != UserRole.Admin)
{
@@ -19,9 +21,39 @@
@if (!string.IsNullOrEmpty(_message))
{
<div class="alert @(_isError ? "alert-error" : "alert-info")">@_message</div>
<div class="alert @(_isError ? "alert-error" : "alert-success")">@_message</div>
}
<div class="card">
<h2 class="card-title">Create local user</h2>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:12px;max-width:640px">
<div>
<label class="form-label" for="new-email">Email</label>
<input id="new-email" class="form-input" type="email" @bind="_newEmail" placeholder="user@example.com" />
</div>
<div>
<label class="form-label" for="new-name">Display name</label>
<input id="new-name" class="form-input" type="text" @bind="_newName" placeholder="Jane Doe" />
</div>
<div>
<label class="form-label" for="new-role">Role</label>
<select id="new-role" class="form-input" @bind="_newRole">
@foreach (var role in Enum.GetValues<UserRole>())
{
<option value="@role">@role</option>
}
</select>
</div>
<div>
<label class="form-label" for="new-pw">Password</label>
<input id="new-pw" class="form-input" type="password" @bind="_newPassword" autocomplete="new-password" />
</div>
</div>
<div style="margin-top:12px">
<button class="btn btn-primary" @onclick="CreateLocalUserAsync">Create user</button>
</div>
</div>
@if (_users.Count == 0)
{
<div class="alert alert-info">No users provisioned yet.</div>
@@ -34,6 +66,7 @@ else
<tr style="border-bottom:2px solid var(--border)">
<th style="text-align:left;padding:8px">User</th>
<th style="text-align:left;padding:8px">Email</th>
<th style="text-align:left;padding:8px">Source</th>
<th style="text-align:left;padding:8px">Role</th>
<th style="text-align:left;padding:8px">Last Login</th>
<th style="text-align:left;padding:8px">Actions</th>
@@ -45,6 +78,11 @@ else
<tr style="border-bottom:1px solid var(--border)">
<td style="padding:8px">@user.DisplayName</td>
<td style="padding:8px">@user.Email</td>
<td style="padding:8px">
<span class="chip @(user.Provider == AuthProvider.Local ? "chip-blue" : "chip-green")">
@(user.Provider == AuthProvider.Local ? "Local" : "Entra")
</span>
</td>
<td style="padding:8px">
<select class="form-input" style="width:130px"
value="@user.Role"
@@ -57,7 +95,11 @@ else
</select>
</td>
<td style="padding:8px">@(user.LastLogin?.ToString("yyyy-MM-dd HH:mm") ?? "Never")</td>
<td style="padding:8px">
<td style="padding:8px;white-space:nowrap">
@if (user.Provider == AuthProvider.Local)
{
<button class="btn btn-secondary btn-sm" @onclick="() => OpenReset(user)">Reset password</button>
}
@if (user.Email != UserContext.Email)
{
<button class="btn btn-danger btn-sm" @onclick="() => DeleteUserAsync(user)">Remove</button>
@@ -74,23 +116,92 @@ else
</div>
}
@if (_resetUser is not null)
{
<div class="card" style="max-width:420px">
<h2 class="card-title">Reset password — @_resetUser.DisplayName</h2>
<label class="form-label" for="reset-pw">New password</label>
<input id="reset-pw" class="form-input" type="password" @bind="_resetPassword" autocomplete="new-password" />
<div style="margin-top:12px;display:flex;gap:8px">
<button class="btn btn-primary" @onclick="ResetPasswordAsync">Set password</button>
<button class="btn btn-secondary" @onclick="() => _resetUser = null">Cancel</button>
</div>
</div>
}
@code {
private List<AppUser> _users = new();
private string _message = string.Empty;
private bool _isError;
private string _newEmail = string.Empty;
private string _newName = string.Empty;
private UserRole _newRole = UserRole.TechN0;
private string _newPassword = string.Empty;
private AppUser? _resetUser;
private string _resetPassword = string.Empty;
protected override async Task OnInitializedAsync()
{
_users = (await UserService.GetAllAsync()).ToList();
}
private async Task CreateLocalUserAsync()
{
try
{
var user = await UserService.CreateLocalUserAsync(_newEmail, _newName, _newRole, _newPassword);
_users.Add(user);
await Audit.LogAsync("UserCreated", "", Array.Empty<string>(),
$"Created local user {user.Email} ({user.DisplayName}) with role {user.Role}.");
_message = $"Local user {user.DisplayName} created.";
_isError = false;
_newEmail = _newName = _newPassword = string.Empty;
_newRole = UserRole.TechN0;
}
catch (Exception ex)
{
_message = $"Error: {ex.Message}";
_isError = true;
}
}
private void OpenReset(AppUser user)
{
_resetUser = user;
_resetPassword = string.Empty;
}
private async Task ResetPasswordAsync()
{
if (_resetUser is null) return;
try
{
await UserService.SetPasswordAsync(_resetUser.Id, _resetPassword);
await Audit.LogAsync("PasswordReset", "", Array.Empty<string>(),
$"Reset password for local user {_resetUser.Email} ({_resetUser.DisplayName}).");
_message = $"Password reset for {_resetUser.DisplayName}.";
_isError = false;
_resetUser = null;
}
catch (Exception ex)
{
_message = $"Error: {ex.Message}";
_isError = true;
}
}
private async Task OnRoleChange(AppUser user, ChangeEventArgs e)
{
if (!Enum.TryParse<UserRole>(e.Value?.ToString(), out var newRole)) return;
try
{
var oldRole = user.Role;
await UserService.UpdateRoleAsync(user.Id, newRole);
user.Role = newRole;
await Audit.LogAsync("RoleChanged", "", Array.Empty<string>(),
$"Changed role for {user.Email} ({user.DisplayName}) from {oldRole} to {newRole}.");
_message = $"Role updated for {user.DisplayName}.";
_isError = false;
}
@@ -105,6 +216,8 @@ else
{
await UserService.DeleteAsync(user.Id);
_users.Remove(user);
await Audit.LogAsync("UserDeleted", "", Array.Empty<string>(),
$"Removed {user.Provider} user {user.Email} ({user.DisplayName}), role {user.Role}.");
_message = $"User {user.DisplayName} removed.";
_isError = false;
}