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

Reviewed-on: #5
This commit was merged in pull request #5.
This commit is contained in:
2026-07-24 12:49:17 +02:00
3 changed files with 104 additions and 34 deletions
@@ -45,10 +45,18 @@
</div> </div>
@code { @code {
protected override async Task OnInitializedAsync() protected override void OnInitialized()
{ {
AuthService.OnAuthStateChanged += StateHasChanged; AuthService.OnAuthStateChanged += StateHasChanged;
await AuthService.InitializeAsync(); }
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
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>
<button class="btn-sm btn-secondary" @onclick="RefreshData" disabled="@isLoading"> <div style="display: flex; gap: 0.5rem; align-items: center;">
<i class="bi bi-arrow-clockwise"></i> Refresh Data <button class="btn-sm btn-secondary" @onclick="RefreshData" disabled="@isLoading">
</button> <i class="bi bi-arrow-clockwise"></i> Refresh Data
</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,16 +421,28 @@
private long editModalDataLimit; private long editModalDataLimit;
private string editModalNewPassword = string.Empty; private string editModalNewPassword = string.Empty;
protected override async Task OnInitializedAsync() private UserUsageDto? selectedUserForDelete;
{
await AuthService.InitializeAsync();
if (!AuthService.IsAuthenticated)
{
NavManager.NavigateTo("/admin/login");
return;
}
await LoadDataAsync(); protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await AuthService.InitializeAsync();
if (!AuthService.IsAuthenticated)
{
NavManager.NavigateTo("/admin/login");
return;
}
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,26 +143,30 @@
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)
{ {
await AuthService.InitializeAsync(); if (firstRender)
if (AuthService.IsAuthenticated)
{ {
NavManager.NavigateTo("/admin"); try
return; {
} await AuthService.InitializeAsync();
if (AuthService.IsAuthenticated)
{
NavManager.NavigateTo("/admin");
return;
}
try isFirstRun = !await AuthService.HasAnyAdminAsync();
{ }
isFirstRun = !await AuthService.HasAnyAdminAsync(); catch
} {
catch isFirstRun = false;
{ }
isFirstRun = false; finally
} {
finally isCheckingFirstRun = false;
{ StateHasChanged();
isCheckingFirstRun = false; }
} }
} }