Merge pull request 'feat: implement admin dashboard with authentication, session monitoring, and user management UI' (#5) from admin into master
Build and Push Docker Image to Gitea Container Registry / build-and-push (push) Successful in 1m31s
Build and Push Docker Image to Gitea Container Registry / build-and-push (push) Successful in 1m31s
Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
@@ -45,10 +45,18 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
protected override async Task OnInitializedAsync()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
AuthService.OnAuthStateChanged += StateHasChanged;
|
AuthService.OnAuthStateChanged += StateHasChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
await AuthService.InitializeAsync();
|
await AuthService.InitializeAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LogoutAdmin()
|
private async Task LogoutAdmin()
|
||||||
|
|||||||
@@ -23,9 +23,14 @@
|
|||||||
Manage guest bandwidth caps, session durations, and monitor live PostgreSQL accounting.
|
Manage guest bandwidth caps, session durations, and monitor live PostgreSQL accounting.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
||||||
<button class="btn-sm btn-secondary" @onclick="RefreshData" disabled="@isLoading">
|
<button class="btn-sm btn-secondary" @onclick="RefreshData" disabled="@isLoading">
|
||||||
<i class="bi bi-arrow-clockwise"></i> Refresh Data
|
<i class="bi bi-arrow-clockwise"></i> Refresh Data
|
||||||
</button>
|
</button>
|
||||||
|
<button class="btn-sm btn-danger" @onclick="LogoutAdmin">
|
||||||
|
<i class="bi bi-box-arrow-right"></i> Logout (@AuthService.CurrentAdminUsername)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (notificationMessage != null)
|
@if (notificationMessage != null)
|
||||||
@@ -196,7 +201,7 @@
|
|||||||
<i class="bi bi-slash-circle"></i>
|
<i class="bi bi-slash-circle"></i>
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button class="btn-sm btn-secondary" style="color: var(--accent-rose);" title="Delete User" @onclick="() => DeleteUser(user.ProfileId)">
|
<button class="btn-sm btn-secondary" style="color: var(--accent-rose);" title="Delete User" @onclick="() => OpenDeleteModal(user)">
|
||||||
<i class="bi bi-trash"></i>
|
<i class="bi bi-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -364,6 +369,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal Overlay -->
|
||||||
|
@if (selectedUserForDelete != null)
|
||||||
|
{
|
||||||
|
<div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.75); backdrop-filter: blur(8px); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 1rem;">
|
||||||
|
<div class="glass-card" style="max-width: 480px; width: 100%; border-color: rgba(244, 63, 94, 0.4);">
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||||
|
<h3 style="font-size: 1.25rem; font-weight: 700; color: var(--accent-rose);">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill"></i> Delete Guest Account
|
||||||
|
</h3>
|
||||||
|
<button class="btn-sm btn-secondary" @onclick="() => selectedUserForDelete = null">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p style="color: var(--text-muted); font-size: 0.95rem; margin-bottom: 1rem;">
|
||||||
|
Are you sure you want to permanently delete guest account <strong style="color: var(--text-primary);">@selectedUserForDelete.GuestName</strong> (<code style="color: var(--accent-teal);">@selectedUserForDelete.Username</code>)?
|
||||||
|
</p>
|
||||||
|
<p style="color: var(--text-dim); font-size: 0.825rem; margin-bottom: 1.5rem;">
|
||||||
|
This will remove their RADIUS credentials from <code style="color: var(--accent-teal);">radcheck</code> and <code style="color: var(--accent-teal);">radreply</code> tables. This action cannot be undone.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div style="display: flex; gap: 0.75rem; justify-content: flex-end;">
|
||||||
|
<button class="btn-sm btn-secondary" @onclick="() => selectedUserForDelete = null">Cancel</button>
|
||||||
|
<button class="btn-sm btn-danger" style="padding: 0.5rem 1.25rem;" @onclick="ConfirmDeleteUser">
|
||||||
|
<i class="bi bi-trash-fill"></i> Delete User
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -385,7 +421,11 @@
|
|||||||
private long editModalDataLimit;
|
private long editModalDataLimit;
|
||||||
private string editModalNewPassword = string.Empty;
|
private string editModalNewPassword = string.Empty;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
private UserUsageDto? selectedUserForDelete;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
{
|
{
|
||||||
await AuthService.InitializeAsync();
|
await AuthService.InitializeAsync();
|
||||||
if (!AuthService.IsAuthenticated)
|
if (!AuthService.IsAuthenticated)
|
||||||
@@ -395,6 +435,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LogoutAdmin()
|
||||||
|
{
|
||||||
|
await AuthService.LogoutAsync();
|
||||||
|
NavManager.NavigateTo("/admin/login");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadDataAsync()
|
private async Task LoadDataAsync()
|
||||||
@@ -495,13 +543,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteUser(int profileId)
|
private void OpenDeleteModal(UserUsageDto user)
|
||||||
{
|
{
|
||||||
|
selectedUserForDelete = user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ConfirmDeleteUser()
|
||||||
|
{
|
||||||
|
if (selectedUserForDelete == null) return;
|
||||||
|
|
||||||
|
int profileId = selectedUserForDelete.ProfileId;
|
||||||
|
selectedUserForDelete = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await RadiusService.DeleteUserAsync(profileId);
|
await RadiusService.DeleteUserAsync(profileId);
|
||||||
await LoadDataAsync();
|
await LoadDataAsync();
|
||||||
ShowNotification("User deleted.");
|
ShowNotification("User account deleted successfully.");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -143,7 +143,11 @@
|
|||||||
private bool isSubmitting = false;
|
private bool isSubmitting = false;
|
||||||
private string errorMessage = string.Empty;
|
private string errorMessage = string.Empty;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (firstRender)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await AuthService.InitializeAsync();
|
await AuthService.InitializeAsync();
|
||||||
if (AuthService.IsAuthenticated)
|
if (AuthService.IsAuthenticated)
|
||||||
@@ -152,8 +156,6 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
isFirstRun = !await AuthService.HasAnyAdminAsync();
|
isFirstRun = !await AuthService.HasAnyAdminAsync();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -163,6 +165,8 @@
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
isCheckingFirstRun = false;
|
isCheckingFirstRun = false;
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user