Initial commit
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
@page "/admin/audit"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@inject IAuditService AuditService
|
||||
@inject IUserContextAccessor UserContext
|
||||
@inject NavigationManager Nav
|
||||
@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>
|
||||
|
||||
@if (!UserContext.IsAuthenticated || UserContext.Role != UserRole.Admin)
|
||||
{
|
||||
<div class="alert alert-error">Access denied. Admin role required.</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>
|
||||
</div>
|
||||
|
||||
@if (_loading)
|
||||
{
|
||||
<div class="alert alert-info">Loading audit log...</div>
|
||||
}
|
||||
else if (_filtered.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info">No audit entries found.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card" style="overflow-x:auto">
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var e in _filtered)
|
||||
{
|
||||
<tr style="border-bottom:1px solid var(--border)">
|
||||
<td style="padding:6px;white-space:nowrap">@e.Timestamp.ToString("yyyy-MM-dd HH:mm:ss")</td>
|
||||
<td style="padding:6px">@e.UserDisplay<br /><span class="text-muted" style="font-size:11px">@e.UserEmail</span></td>
|
||||
<td style="padding:6px"><span class="chip @RoleChipClass(e.UserRole)">@e.UserRole</span></td>
|
||||
<td style="padding:6px;font-weight:600">@e.Action</td>
|
||||
<td style="padding:6px">@e.ClientName</td>
|
||||
<td style="padding:6px">@string.Join(", ", e.Sites)</td>
|
||||
<td style="padding:6px;color:var(--text-muted)">@e.Details</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<p class="text-muted" style="margin-top:8px;font-size:12px">Showing @_filtered.Count of @_entries.Count entries</p>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<AuditEntry> _entries = new();
|
||||
private List<AuditEntry> _filtered = new();
|
||||
private bool _loading = true;
|
||||
private string _filterUser = string.Empty;
|
||||
private string _filterClient = string.Empty;
|
||||
private string _filterAction = string.Empty;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_entries = (await AuditService.GetAllAsync())
|
||||
.OrderByDescending(e => e.Timestamp)
|
||||
.ToList();
|
||||
_loading = false;
|
||||
ApplyFilters();
|
||||
}
|
||||
|
||||
private void ApplyFilters()
|
||||
{
|
||||
_filtered = _entries.Where(e =>
|
||||
(string.IsNullOrEmpty(_filterUser) || e.UserEmail.Contains(_filterUser, StringComparison.OrdinalIgnoreCase) || e.UserDisplay.Contains(_filterUser, StringComparison.OrdinalIgnoreCase)) &&
|
||||
(string.IsNullOrEmpty(_filterClient) || e.ClientName.Contains(_filterClient, StringComparison.OrdinalIgnoreCase)) &&
|
||||
(string.IsNullOrEmpty(_filterAction) || e.Action.Contains(_filterAction, StringComparison.OrdinalIgnoreCase))
|
||||
).ToList();
|
||||
}
|
||||
|
||||
private static string RoleChipClass(UserRole role) => role switch
|
||||
{
|
||||
UserRole.Admin => "chip-red",
|
||||
UserRole.TechN1 => "chip-green",
|
||||
_ => "chip-blue"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
@page "/admin/users"
|
||||
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
||||
@inject IUserService UserService
|
||||
@inject IUserContextAccessor UserContext
|
||||
@inject NavigationManager Nav
|
||||
@rendermode InteractiveServer
|
||||
@using SharepointToolbox.Web.Core.Models
|
||||
@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>
|
||||
|
||||
@if (!UserContext.IsAuthenticated || UserContext.Role != UserRole.Admin)
|
||||
{
|
||||
<div class="alert alert-error">Access denied. Admin role required.</div>
|
||||
return;
|
||||
}
|
||||
|
||||
@if (!string.IsNullOrEmpty(_message))
|
||||
{
|
||||
<div class="alert @(_isError ? "alert-error" : "alert-info")">@_message</div>
|
||||
}
|
||||
|
||||
@if (_users.Count == 0)
|
||||
{
|
||||
<div class="alert alert-info">No users provisioned yet.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="card" style="overflow-x:auto">
|
||||
<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">Role</th>
|
||||
<th style="text-align:left;padding:8px">Last Login</th>
|
||||
<th style="text-align:left;padding:8px">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var user in _users)
|
||||
{
|
||||
<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">
|
||||
<select class="form-input" style="width:130px"
|
||||
value="@user.Role"
|
||||
@onchange="e => OnRoleChange(user, e)"
|
||||
disabled="@(user.Email == UserContext.Email)">
|
||||
@foreach (var role in Enum.GetValues<UserRole>())
|
||||
{
|
||||
<option value="@role" selected="@(user.Role == role)">@role</option>
|
||||
}
|
||||
</select>
|
||||
</td>
|
||||
<td style="padding:8px">@(user.LastLogin?.ToString("yyyy-MM-dd HH:mm") ?? "Never")</td>
|
||||
<td style="padding:8px">
|
||||
@if (user.Email != UserContext.Email)
|
||||
{
|
||||
<button class="btn btn-danger btn-sm" @onclick="() => DeleteUserAsync(user)">Remove</button>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="chip chip-green">You</span>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
private List<AppUser> _users = new();
|
||||
private string _message = string.Empty;
|
||||
private bool _isError;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
_users = (await UserService.GetAllAsync()).ToList();
|
||||
}
|
||||
|
||||
private async Task OnRoleChange(AppUser user, ChangeEventArgs e)
|
||||
{
|
||||
if (!Enum.TryParse<UserRole>(e.Value?.ToString(), out var newRole)) return;
|
||||
try
|
||||
{
|
||||
await UserService.UpdateRoleAsync(user.Id, newRole);
|
||||
user.Role = newRole;
|
||||
_message = $"Role updated for {user.DisplayName}.";
|
||||
_isError = false;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_message = $"Error: {ex.Message}";
|
||||
_isError = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteUserAsync(AppUser user)
|
||||
{
|
||||
await UserService.DeleteAsync(user.Id);
|
||||
_users.Remove(user);
|
||||
_message = $"User {user.DisplayName} removed.";
|
||||
_isError = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user