Merge branch 'main' of https://git.azuze.fr/kawa/SharepointToolbox-Web
This commit is contained in:
@@ -3,34 +3,35 @@
|
||||
@inject IAuditService AuditService
|
||||
@inject IUserContextAccessor UserContext
|
||||
@inject NavigationManager Nav
|
||||
@inject TranslationSource T
|
||||
@rendermode InteractiveServer
|
||||
@using SharepointToolbox.Web.Core.Models
|
||||
@using SharepointToolbox.Web.Services.Audit
|
||||
@using SharepointToolbox.Web.Services.Session
|
||||
|
||||
<h1 class="page-title">Audit Logs</h1>
|
||||
<p class="page-subtitle">All technician and admin actions within the application.</p>
|
||||
<h1 class="page-title">@T["adminaudit.title"]</h1>
|
||||
<p class="page-subtitle">@T["adminaudit.subtitle"]</p>
|
||||
|
||||
@if (!UserContext.IsAuthenticated || UserContext.Role != UserRole.Admin)
|
||||
{
|
||||
<div class="alert alert-error">Access denied. Admin role required.</div>
|
||||
<div class="alert alert-error">@T["adminaudit.accessdenied"]</div>
|
||||
return;
|
||||
}
|
||||
|
||||
<div class="flex-row" style="margin-bottom:16px;flex-wrap:wrap;gap:8px">
|
||||
<input class="form-input" style="width:200px" placeholder="Filter by user..." @bind="_filterUser" @bind:event="oninput" />
|
||||
<input class="form-input" style="width:200px" placeholder="Filter by client..." @bind="_filterClient" @bind:event="oninput" />
|
||||
<input class="form-input" style="width:200px" placeholder="Filter by action..." @bind="_filterAction" @bind:event="oninput" />
|
||||
<a href="/audit/export" class="btn btn-secondary" target="_blank">Export CSV</a>
|
||||
<input class="form-input" style="width:200px" placeholder="@T["adminaudit.filter.user"]" @bind="_filterUser" @bind:event="oninput" />
|
||||
<input class="form-input" style="width:200px" placeholder="@T["adminaudit.filter.client"]" @bind="_filterClient" @bind:event="oninput" />
|
||||
<input class="form-input" style="width:200px" placeholder="@T["adminaudit.filter.action"]" @bind="_filterAction" @bind:event="oninput" />
|
||||
<a href="/audit/export" class="btn btn-secondary" target="_blank">@T["audit.btn.exportCsv"]</a>
|
||||
</div>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<div class="alert alert-info">Loading audit log...</div>
|
||||
<div class="alert alert-info">@T["adminaudit.loading"]</div>
|
||||
}
|
||||
else if (_filtered.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info">No audit entries found.</div>
|
||||
<div class="alert alert-info">@T["adminaudit.noentries"]</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -38,13 +39,13 @@ else
|
||||
<table style="width:100%;border-collapse:collapse;font-size:13px">
|
||||
<thead>
|
||||
<tr style="border-bottom:2px solid var(--border)">
|
||||
<th style="text-align:left;padding:6px">Timestamp</th>
|
||||
<th style="text-align:left;padding:6px">User</th>
|
||||
<th style="text-align:left;padding:6px">Role</th>
|
||||
<th style="text-align:left;padding:6px">Action</th>
|
||||
<th style="text-align:left;padding:6px">Client</th>
|
||||
<th style="text-align:left;padding:6px">Sites</th>
|
||||
<th style="text-align:left;padding:6px">Details</th>
|
||||
<th style="text-align:left;padding:6px">@T["report.col.timestamp"]</th>
|
||||
<th style="text-align:left;padding:6px">@T["report.col.user"]</th>
|
||||
<th style="text-align:left;padding:6px">@T["adminaudit.col.role"]</th>
|
||||
<th style="text-align:left;padding:6px">@T["adminaudit.col.action"]</th>
|
||||
<th style="text-align:left;padding:6px">@T["adminaudit.col.client"]</th>
|
||||
<th style="text-align:left;padding:6px">@T["report.col.sites"]</th>
|
||||
<th style="text-align:left;padding:6px">@T["adminaudit.col.details"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -63,7 +64,7 @@ else
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="text-muted" style="margin-top:8px;font-size:12px">Showing @_filtered.Count of @_entries.Count entries</p>
|
||||
<p class="text-muted" style="margin-top:8px;font-size:12px">@string.Format(T["adminaudit.showing"], _filtered.Count, _entries.Count)</p>
|
||||
}
|
||||
|
||||
@code {
|
||||
|
||||
@@ -4,18 +4,19 @@
|
||||
@inject IUserContextAccessor UserContext
|
||||
@inject IAuditService Audit
|
||||
@inject NavigationManager Nav
|
||||
@inject TranslationSource T
|
||||
@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. Entra users are auto-provisioned on first OIDC login; local users are created here.</p>
|
||||
<h1 class="page-title">@T["usermgmt.title"]</h1>
|
||||
<p class="page-subtitle">@T["usermgmt.subtitle"]</p>
|
||||
|
||||
@if (!UserContext.IsAuthenticated || UserContext.Role != UserRole.Admin)
|
||||
{
|
||||
<div class="alert alert-error">Access denied. Admin role required.</div>
|
||||
<div class="alert alert-error">@T["usermgmt.accessdenied"]</div>
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,18 +26,18 @@
|
||||
}
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Create local user</h2>
|
||||
<h2 class="card-title">@T["usermgmt.create.title"]</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>
|
||||
<label class="form-label" for="new-email">@T["usermgmt.col.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>
|
||||
<label class="form-label" for="new-name">@T["usermgmt.lbl.displayname"]</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>
|
||||
<label class="form-label" for="new-role">@T["usermgmt.col.role"]</label>
|
||||
<select id="new-role" class="form-input" @bind="_newRole">
|
||||
@foreach (var role in Enum.GetValues<UserRole>())
|
||||
{
|
||||
@@ -45,18 +46,18 @@
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="form-label" for="new-pw">Password</label>
|
||||
<label class="form-label" for="new-pw">@T["usermgmt.lbl.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>
|
||||
<button class="btn btn-primary" @onclick="CreateLocalUserAsync">@T["usermgmt.btn.create"]</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (_users.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info">No users provisioned yet.</div>
|
||||
<div class="alert alert-info">@T["usermgmt.empty"]</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -64,12 +65,12 @@ else
|
||||
<table style="width:100%;border-collapse:collapse">
|
||||
<thead>
|
||||
<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>
|
||||
<th style="text-align:left;padding:8px">@T["usermgmt.col.user"]</th>
|
||||
<th style="text-align:left;padding:8px">@T["usermgmt.col.email"]</th>
|
||||
<th style="text-align:left;padding:8px">@T["usermgmt.col.source"]</th>
|
||||
<th style="text-align:left;padding:8px">@T["usermgmt.col.role"]</th>
|
||||
<th style="text-align:left;padding:8px">@T["usermgmt.col.lastlogin"]</th>
|
||||
<th style="text-align:left;padding:8px">@T["usermgmt.col.actions"]</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -80,7 +81,7 @@ else
|
||||
<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")
|
||||
@(user.Provider == AuthProvider.Local ? T["usermgmt.source.local"] : T["usermgmt.source.entra"])
|
||||
</span>
|
||||
</td>
|
||||
<td style="padding:8px">
|
||||
@@ -94,19 +95,19 @@ else
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
<td style="padding:8px">@(user.LastLogin?.ToString("yyyy-MM-dd HH:mm") ?? "Never")</td>
|
||||
<td style="padding:8px">@(user.LastLogin?.ToString("yyyy-MM-dd HH:mm") ?? T["usermgmt.lastlogin.never"])</td>
|
||||
<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>
|
||||
<button class="btn btn-secondary btn-sm" @onclick="() => OpenReset(user)">@T["usermgmt.btn.resetpw"]</button>
|
||||
}
|
||||
@if (user.Email != UserContext.Email)
|
||||
{
|
||||
<button class="btn btn-danger btn-sm" @onclick="() => DeleteUserAsync(user)">Remove</button>
|
||||
<button class="btn btn-danger btn-sm" @onclick="() => DeleteUserAsync(user)">@T["usermgmt.btn.remove"]</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="chip chip-green">You</span>
|
||||
<span class="chip chip-green">@T["usermgmt.badge.you"]</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -119,12 +120,12 @@ else
|
||||
@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>
|
||||
<h2 class="card-title">@string.Format(T["usermgmt.reset.title"], _resetUser.DisplayName)</h2>
|
||||
<label class="form-label" for="reset-pw">@T["usermgmt.lbl.newpassword"]</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>
|
||||
<button class="btn btn-primary" @onclick="ResetPasswordAsync">@T["usermgmt.btn.setpw"]</button>
|
||||
<button class="btn btn-secondary" @onclick="() => _resetUser = null">@T["btn.cancel"]</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -155,14 +156,14 @@ else
|
||||
_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.";
|
||||
_message = string.Format(T["usermgmt.msg.created"], user.DisplayName);
|
||||
_isError = false;
|
||||
_newEmail = _newName = _newPassword = string.Empty;
|
||||
_newRole = UserRole.TechN0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_message = $"Error: {ex.Message}";
|
||||
_message = string.Format(T["usermgmt.msg.error"], ex.Message);
|
||||
_isError = true;
|
||||
}
|
||||
}
|
||||
@@ -181,13 +182,13 @@ else
|
||||
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}.";
|
||||
_message = string.Format(T["usermgmt.msg.pwreset"], _resetUser.DisplayName);
|
||||
_isError = false;
|
||||
_resetUser = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_message = $"Error: {ex.Message}";
|
||||
_message = string.Format(T["usermgmt.msg.error"], ex.Message);
|
||||
_isError = true;
|
||||
}
|
||||
}
|
||||
@@ -202,12 +203,12 @@ else
|
||||
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}.";
|
||||
_message = string.Format(T["usermgmt.msg.roleupdated"], user.DisplayName);
|
||||
_isError = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_message = $"Error: {ex.Message}";
|
||||
_message = string.Format(T["usermgmt.msg.error"], ex.Message);
|
||||
_isError = true;
|
||||
}
|
||||
}
|
||||
@@ -218,7 +219,7 @@ else
|
||||
_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.";
|
||||
_message = string.Format(T["usermgmt.msg.removed"], user.DisplayName);
|
||||
_isError = false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user