@page "/admin" @rendermode InteractiveServer @inject IRadiusService RadiusService @inject AdminAuthService AuthService @inject NavigationManager NavManager @implements IDisposable
@if (!AuthService.IsAuthenticated) {

Verifying Administrator Session...

} else {

RADIUS Controller Dashboard

Manage guest bandwidth caps, session durations, and monitor live PostgreSQL accounting.

@if (notificationMessage != null) {
@notificationMessage
}
@users.Count
Total Registered Guests
@activeSessions.Count
Active Sessions Now
@FormatTotalDataUsed()
Total Data Consumed
@(config?.DefaultSessionTimeMinutes ?? 0)m / @(config?.DefaultDataLimitMb ?? 0)MB
Default Policy (Time / Cap)
@if (activeTab == "users") {
@if (FilteredUsers.Count == 0) { } @foreach (var user in FilteredUsers) { double percentage = user.DataLimitMb > 0 ? Math.Min(100, Math.Round((user.UsedDataMb / user.DataLimitMb) * 100, 1)) : 0; string fillClass = percentage >= 85 ? "warning" : "normal"; }
Guest Name RADIUS Credentials Status Session Duration Limit Data Used / Cap Total Online Time Actions
No guest accounts found.
@user.GuestName
Registered: @user.CreatedAt.ToLocalTime().ToString("MMM dd, yyyy HH:mm")
@user.Username
Pwd: @user.Password
@if (user.IsCurrentlyConnected) { Online } else if (user.Status == "Revoked") { Revoked } else { @user.Status } @user.SessionTimeMinutes mins
Expires: @user.CreatedAt.AddMinutes(user.SessionTimeMinutes).ToLocalTime().ToString("MMM dd, HH:mm")
@user.UsedDataMb MB / @user.DataLimitMb MB
Upload: @FormatMb(user.TotalInputOctets) | Down: @FormatMb(user.TotalOutputOctets)
@FormatSeconds(user.TotalSessionTimeSeconds)
@if (user.Status == "Active") { }
} @if (activeTab == "config") {

Default RADIUS Policy Settings

These parameters determine the initial session duration limit and total bandwidth quota assigned when a new guest registers.

Translates to RADIUS attribute Session-Timeout (in seconds) for FreeRADIUS reply.
Translates to RADIUS attribute Max-Octets (in bytes) for FreeRADIUS reply.
} @if (activeTab == "active") {

Live Connected Devices

Real-time active RADIUS sessions recorded in PostgreSQL radacct table.

@if (activeSessions.Count == 0) { } @foreach (var session in activeSessions) { }
Username NAS IP Address Calling Station (MAC) Framed IP Start Time Online Duration Downloaded / Uploaded
No active sessions currently online.
@session.Username @session.NasIpAddress @(string.IsNullOrEmpty(session.CallingStationId) ? "N/A" : session.CallingStationId) @(string.IsNullOrEmpty(session.FramedIpAddress) ? "DHCP Assigned" : session.FramedIpAddress) @session.AcctStartTime?.ToLocalTime().ToString("HH:mm:ss MMM dd") @FormatSeconds(session.AcctSessionTime ?? 0) ↓ @FormatMb(session.AcctOutputOctets ?? 0) MB / ↑ @FormatMb(session.AcctInputOctets ?? 0) MB
} @if (selectedUserForEdit != null) {

Edit Guest Policy

Guest: @selectedUserForEdit.GuestName
Username: @selectedUserForEdit.Username
} @if (selectedUserForDelete != null) {

Delete Guest Account

Are you sure you want to permanently delete guest account @selectedUserForDelete.GuestName (@selectedUserForDelete.Username)?

This will remove their RADIUS credentials from radcheck and radreply tables. This action cannot be undone.

} }
@code { private bool isLoading = true; private string activeTab = "users"; private string searchQuery = string.Empty; private string? notificationMessage; private System.Threading.Timer? autoRefreshTimer; private SystemConfigDto? config; private List users = new(); private List activeSessions = new(); private int editConfigSessionTime; private long editConfigDataLimit; private UserUsageDto? selectedUserForEdit; private int editModalSessionTime; private long editModalDataLimit; private string editModalNewPassword = string.Empty; private UserUsageDto? selectedUserForDelete; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { await AuthService.InitializeAsync(); if (!AuthService.IsAuthenticated) { NavManager.NavigateTo("/admin/login"); return; } await LoadDataAsync(); StateHasChanged(); autoRefreshTimer = new System.Threading.Timer(async _ => { await InvokeAsync(async () => { if (AuthService.IsAuthenticated) { await LoadDataAsync(); StateHasChanged(); } }); }, null, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10)); } } public void Dispose() { autoRefreshTimer?.Dispose(); } private async Task LogoutAdmin() { await AuthService.LogoutAsync(); NavManager.NavigateTo("/admin/login"); } private async Task LoadDataAsync() { isLoading = true; try { config = await RadiusService.GetSystemConfigAsync(); editConfigSessionTime = config.DefaultSessionTimeMinutes; editConfigDataLimit = config.DefaultDataLimitMb; users = await RadiusService.GetAllUsersWithUsageAsync(); activeSessions = await RadiusService.GetActiveSessionsAsync(); } catch (Exception ex) { notificationMessage = "Error loading data: " + ex.Message; } finally { isLoading = false; } } private async Task RefreshData() { notificationMessage = null; await LoadDataAsync(); ShowNotification("Data refreshed successfully."); } private List FilteredUsers { get { if (string.IsNullOrWhiteSpace(searchQuery)) return users; string q = searchQuery.Trim().ToLowerInvariant(); return users.Where(u => u.GuestName.ToLowerInvariant().Contains(q) || u.Username.ToLowerInvariant().Contains(q)).ToList(); } } private async Task SaveGlobalConfig() { try { await RadiusService.UpdateSystemConfigAsync(editConfigSessionTime, editConfigDataLimit); config = await RadiusService.GetSystemConfigAsync(); ShowNotification("Global default session time & data cap policy updated."); } catch (Exception ex) { ShowNotification("Failed to update policy: " + ex.Message); } } private void OpenEditModal(UserUsageDto user) { selectedUserForEdit = user; editModalSessionTime = user.SessionTimeMinutes; editModalDataLimit = user.DataLimitMb; editModalNewPassword = string.Empty; } private async Task SaveUserEdit() { if (selectedUserForEdit == null) return; try { await RadiusService.UpdateUserLimitsAsync(selectedUserForEdit.ProfileId, editModalSessionTime, editModalDataLimit); if (!string.IsNullOrWhiteSpace(editModalNewPassword)) { await RadiusService.ResetUserPasswordAsync(selectedUserForEdit.ProfileId, editModalNewPassword.Trim()); } selectedUserForEdit = null; await LoadDataAsync(); ShowNotification("User limits updated successfully."); } catch (Exception ex) { ShowNotification("Error updating user: " + ex.Message); } } private async Task RevokeUser(int profileId) { try { await RadiusService.RevokeUserAsync(profileId); await LoadDataAsync(); ShowNotification("User access revoked."); } catch (Exception ex) { ShowNotification("Error revoking user: " + ex.Message); } } private void OpenDeleteModal(UserUsageDto user) { selectedUserForDelete = user; } private async Task ConfirmDeleteUser() { if (selectedUserForDelete == null) return; int profileId = selectedUserForDelete.ProfileId; selectedUserForDelete = null; try { await RadiusService.DeleteUserAsync(profileId); await LoadDataAsync(); ShowNotification("User account deleted successfully."); } catch (Exception ex) { ShowNotification("Error deleting user: " + ex.Message); } } private void ShowNotification(string message) { notificationMessage = message; StateHasChanged(); } private string FormatTotalDataUsed() { long totalOctets = users.Sum(u => u.TotalOctets); double gb = (double)totalOctets / (1024 * 1024 * 1024); if (gb >= 1.0) return $"{Math.Round(gb, 2)} GB"; double mb = (double)totalOctets / (1024 * 1024); return $"{Math.Round(mb, 1)} MB"; } private static double FormatMb(long bytes) => Math.Round((double)bytes / (1024 * 1024), 2); private static string FormatSeconds(long seconds) { if (seconds <= 0) return "0s"; TimeSpan t = TimeSpan.FromSeconds(seconds); if (t.TotalHours >= 1) return $"{Math.Floor(t.TotalHours)}h {t.Minutes}m"; if (t.TotalMinutes >= 1) return $"{t.Minutes}m {t.Seconds}s"; return $"{t.Seconds}s"; } }