6371826f5e
Build and Push Docker Image to Gitea Container Registry / build-and-push (push) Successful in 1m8s
616 lines
29 KiB
Plaintext
616 lines
29 KiB
Plaintext
@page "/admin"
|
|
@rendermode InteractiveServer
|
|
@inject IRadiusService RadiusService
|
|
@inject AdminAuthService AuthService
|
|
@inject NavigationManager NavManager
|
|
@implements IDisposable
|
|
|
|
<div style="max-width: 1280px; margin: 0 auto;">
|
|
@if (!AuthService.IsAuthenticated)
|
|
{
|
|
<div class="glass-card" style="text-align: center; padding: 3rem;">
|
|
<div class="spinner-border text-primary" role="status"></div>
|
|
<p style="margin-top: 1rem; color: var(--text-muted);">Verifying Administrator Session...</p>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 2rem; flex-wrap: wrap; gap: 1rem;">
|
|
<div>
|
|
<h1 style="font-size: 2rem; font-weight: 800; letter-spacing: -0.02em;">
|
|
<i class="bi bi-speedometer2" style="color: var(--primary);"></i> RADIUS Controller Dashboard
|
|
</h1>
|
|
<p style="color: var(--text-muted); font-size: 0.95rem;">
|
|
Manage guest bandwidth caps, session durations, and monitor live PostgreSQL accounting.
|
|
</p>
|
|
</div>
|
|
<div style="display: flex; gap: 0.5rem; align-items: center;">
|
|
<button class="btn-sm btn-secondary" @onclick="RefreshData" disabled="@isLoading">
|
|
<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>
|
|
|
|
@if (notificationMessage != null)
|
|
{
|
|
<div class="glass-card" style="padding: 1rem 1.25rem; margin-bottom: 1.5rem; border-color: rgba(20, 184, 166, 0.4); background: rgba(20, 184, 166, 0.1);">
|
|
<i class="bi bi-info-circle-fill" style="color: var(--accent-teal);"></i> @notificationMessage
|
|
</div>
|
|
}
|
|
|
|
<!-- Top Statistics Cards -->
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="stat-icon primary">
|
|
<i class="bi bi-people-fill"></i>
|
|
</div>
|
|
<div>
|
|
<div class="stat-number">@users.Count</div>
|
|
<div class="stat-label">Total Registered Guests</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon emerald">
|
|
<i class="bi bi-wifi"></i>
|
|
</div>
|
|
<div>
|
|
<div class="stat-number" style="color: var(--accent-emerald);">@activeSessions.Count</div>
|
|
<div class="stat-label">Active Sessions Now</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon teal">
|
|
<i class="bi bi-cloud-arrow-down-fill"></i>
|
|
</div>
|
|
<div>
|
|
<div class="stat-number">@FormatTotalDataUsed()</div>
|
|
<div class="stat-label">Total Data Consumed</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-icon amber">
|
|
<i class="bi bi-sliders"></i>
|
|
</div>
|
|
<div>
|
|
<div class="stat-number">@(config?.DefaultSessionTimeMinutes ?? 0)m / @(config?.DefaultDataLimitMb ?? 0)MB</div>
|
|
<div class="stat-label">Default Policy (Time / Cap)</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Dashboard Navigation Tabs -->
|
|
<div class="tab-nav">
|
|
<button class="tab-btn @(activeTab == "users" ? "active" : "")" @onclick='() => activeTab = "users"'>
|
|
<i class="bi bi-person-lines-fill"></i> Guest Users & Usage (@users.Count)
|
|
</button>
|
|
<button class="tab-btn @(activeTab == "config" ? "active" : "")" @onclick='() => activeTab = "config"'>
|
|
<i class="bi bi-gear-wide-connected"></i> Global RADIUS Settings
|
|
</button>
|
|
<button class="tab-btn @(activeTab == "active" ? "active" : "")" @onclick='() => activeTab = "active"'>
|
|
<i class="bi bi-activity"></i> Live Connections (@activeSessions.Count)
|
|
</button>
|
|
</div>
|
|
|
|
<!-- TAB 1: User Management & Accounting Data -->
|
|
@if (activeTab == "users")
|
|
{
|
|
<div class="glass-card">
|
|
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1.5rem; flex-wrap: wrap; gap: 1rem;">
|
|
<div style="position: relative; max-width: 320px; width: 100%;">
|
|
<input type="text"
|
|
class="form-control-glass"
|
|
placeholder="Search guest name or username..."
|
|
@bind="searchQuery"
|
|
@bind:event="oninput"
|
|
style="padding-left: 2.5rem; font-size: 0.9rem;" />
|
|
<i class="bi bi-search" style="position: absolute; left: 1rem; top: 50%; transform: translateY(-50%); color: var(--text-muted);"></i>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="custom-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Guest Name</th>
|
|
<th>RADIUS Credentials</th>
|
|
<th>Status</th>
|
|
<th>Session Duration Limit</th>
|
|
<th>Data Used / Cap</th>
|
|
<th>Total Online Time</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (FilteredUsers.Count == 0)
|
|
{
|
|
<tr>
|
|
<td colspan="7" style="text-align: center; color: var(--text-muted); padding: 3rem;">
|
|
<i class="bi bi-inbox" style="font-size: 2rem; display: block; margin-bottom: 0.5rem;"></i>
|
|
No guest accounts found.
|
|
</td>
|
|
</tr>
|
|
}
|
|
@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";
|
|
|
|
<tr>
|
|
<td>
|
|
<div style="font-weight: 700;">@user.GuestName</div>
|
|
<div style="font-size: 0.75rem; color: var(--text-dim);">
|
|
Registered: @user.CreatedAt.ToLocalTime().ToString("MMM dd, yyyy HH:mm")
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div style="font-family: var(--font-mono); font-weight: 600; color: var(--accent-teal);">
|
|
@user.Username
|
|
</div>
|
|
<div style="font-family: var(--font-mono); font-size: 0.8rem; color: var(--text-muted);">
|
|
Pwd: <span style="color: var(--accent-purple);">@user.Password</span>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
@if (user.IsCurrentlyConnected)
|
|
{
|
|
<span class="badge badge-emerald"><i class="bi bi-broadcast"></i> Online</span>
|
|
}
|
|
else if (user.Status == "Revoked")
|
|
{
|
|
<span class="badge badge-rose"><i class="bi bi-x-circle"></i> Revoked</span>
|
|
}
|
|
else
|
|
{
|
|
<span class="badge badge-primary"><i class="bi bi-check-circle"></i> @user.Status</span>
|
|
}
|
|
</td>
|
|
<td>
|
|
<strong>@user.SessionTimeMinutes mins</strong>
|
|
<div style="font-size: 0.75rem; color: var(--text-dim);">
|
|
Expires: @user.CreatedAt.AddMinutes(user.SessionTimeMinutes).ToLocalTime().ToString("MMM dd, HH:mm")
|
|
</div>
|
|
</td>
|
|
<td style="min-width: 180px;">
|
|
<div style="display: flex; justify-content: space-between; font-size: 0.85rem; font-weight: 600;">
|
|
<span>@user.UsedDataMb MB</span>
|
|
<span style="color: var(--text-muted);">/ @user.DataLimitMb MB</span>
|
|
</div>
|
|
<div class="progress-bar-bg">
|
|
<div class="progress-bar-fill @fillClass" style="width: @percentage%;"></div>
|
|
</div>
|
|
<div style="font-size: 0.725rem; color: var(--text-dim); margin-top: 0.2rem;">
|
|
Upload: @FormatMb(user.TotalInputOctets) | Down: @FormatMb(user.TotalOutputOctets)
|
|
</div>
|
|
</td>
|
|
<td>
|
|
@FormatSeconds(user.TotalSessionTimeSeconds)
|
|
</td>
|
|
<td>
|
|
<div style="display: flex; gap: 0.35rem;">
|
|
<button class="btn-sm btn-secondary" title="Edit Session & Data Limits" @onclick="() => OpenEditModal(user)">
|
|
<i class="bi bi-pencil"></i>
|
|
</button>
|
|
@if (user.Status == "Active")
|
|
{
|
|
<button class="btn-sm btn-danger" title="Revoke Access" @onclick="() => RevokeUser(user.ProfileId)">
|
|
<i class="bi bi-slash-circle"></i>
|
|
</button>
|
|
}
|
|
<button class="btn-sm btn-secondary" style="color: var(--accent-rose);" title="Delete User" @onclick="() => OpenDeleteModal(user)">
|
|
<i class="bi bi-trash"></i>
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- TAB 2: Global Configuration Settings -->
|
|
@if (activeTab == "config")
|
|
{
|
|
<div class="glass-card" style="max-width: 680px;">
|
|
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 1rem;">
|
|
<i class="bi bi-sliders" style="color: var(--primary);"></i> Default RADIUS Policy Settings
|
|
</h3>
|
|
<p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 1.5rem;">
|
|
These parameters determine the initial session duration limit and total bandwidth quota assigned when a new guest registers.
|
|
</p>
|
|
|
|
<EditForm Model="this" OnValidSubmit="SaveGlobalConfig">
|
|
<div class="form-group">
|
|
<label class="form-label">Default Session Time (Minutes)</label>
|
|
<input type="number"
|
|
class="form-control-glass"
|
|
@bind="editConfigSessionTime"
|
|
min="1"
|
|
required />
|
|
<span style="font-size: 0.775rem; color: var(--text-dim);">
|
|
Translates to RADIUS attribute <code style="color: var(--accent-teal);">Session-Timeout</code> (in seconds) for FreeRADIUS reply.
|
|
</span>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Default Data Usage Cap (Megabytes)</label>
|
|
<input type="number"
|
|
class="form-control-glass"
|
|
@bind="editConfigDataLimit"
|
|
min="1"
|
|
required />
|
|
<span style="font-size: 0.775rem; color: var(--text-dim);">
|
|
Translates to RADIUS attribute <code style="color: var(--accent-teal);">Max-Octets</code> (in bytes) for FreeRADIUS reply.
|
|
</span>
|
|
</div>
|
|
|
|
<button type="submit" class="btn-gradient-primary" style="width: auto; padding: 0.8rem 2rem;">
|
|
<i class="bi bi-save-fill"></i> Save Default Policy
|
|
</button>
|
|
</EditForm>
|
|
</div>
|
|
}
|
|
|
|
<!-- TAB 3: Live Active Sessions -->
|
|
@if (activeTab == "active")
|
|
{
|
|
<div class="glass-card">
|
|
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 1rem;">
|
|
<i class="bi bi-broadcast" style="color: var(--accent-emerald);"></i> Live Connected Devices
|
|
</h3>
|
|
<p style="color: var(--text-muted); font-size: 0.9rem; margin-bottom: 1.5rem;">
|
|
Real-time active RADIUS sessions recorded in PostgreSQL <code style="color: var(--accent-teal);">radacct</code> table.
|
|
</p>
|
|
|
|
<div class="table-responsive">
|
|
<table class="custom-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Username</th>
|
|
<th>NAS IP Address</th>
|
|
<th>Calling Station (MAC)</th>
|
|
<th>Framed IP</th>
|
|
<th>Start Time</th>
|
|
<th>Online Duration</th>
|
|
<th>Downloaded / Uploaded</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@if (activeSessions.Count == 0)
|
|
{
|
|
<tr>
|
|
<td colspan="7" style="text-align: center; color: var(--text-muted); padding: 3rem;">
|
|
<i class="bi bi-wifi-off" style="font-size: 2rem; display: block; margin-bottom: 0.5rem;"></i>
|
|
No active sessions currently online.
|
|
</td>
|
|
</tr>
|
|
}
|
|
@foreach (var session in activeSessions)
|
|
{
|
|
<tr>
|
|
<td>
|
|
<span style="font-family: var(--font-mono); font-weight: 700; color: var(--accent-teal);">
|
|
@session.Username
|
|
</span>
|
|
</td>
|
|
<td>@session.NasIpAddress</td>
|
|
<td>
|
|
<span style="font-family: var(--font-mono); font-size: 0.85rem;">
|
|
@(string.IsNullOrEmpty(session.CallingStationId) ? "N/A" : session.CallingStationId)
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span style="font-family: var(--font-mono); font-size: 0.85rem;">
|
|
@(string.IsNullOrEmpty(session.FramedIpAddress) ? "DHCP Assigned" : session.FramedIpAddress)
|
|
</span>
|
|
</td>
|
|
<td>@session.AcctStartTime?.ToLocalTime().ToString("HH:mm:ss MMM dd")</td>
|
|
<td>@FormatSeconds(session.AcctSessionTime ?? 0)</td>
|
|
<td>
|
|
<span style="color: #34d399;">↓ @FormatMb(session.AcctOutputOctets ?? 0) MB</span> /
|
|
<span style="color: #60a5fa;">↑ @FormatMb(session.AcctInputOctets ?? 0) MB</span>
|
|
</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Edit User Limits Modal Overlay -->
|
|
@if (selectedUserForEdit != 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: 500px; width: 100%; border-color: rgba(99, 102, 241, 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;">
|
|
<i class="bi bi-pencil-square" style="color: var(--primary);"></i> Edit Guest Policy
|
|
</h3>
|
|
<button class="btn-sm btn-secondary" @onclick="() => selectedUserForEdit = null">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div style="background: rgba(255,255,255,0.03); padding: 0.85rem; border-radius: var(--radius-sm); margin-bottom: 1.5rem; font-size: 0.9rem;">
|
|
<div>Guest: <strong>@selectedUserForEdit.GuestName</strong></div>
|
|
<div style="font-family: var(--font-mono); color: var(--accent-teal);">Username: @selectedUserForEdit.Username</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Session Time Limit (Minutes)</label>
|
|
<input type="number" class="form-control-glass" @bind="editModalSessionTime" min="1" required />
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Data Usage Limit (MB)</label>
|
|
<input type="number" class="form-control-glass" @bind="editModalDataLimit" min="1" required />
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label class="form-label">Reset Password (Optional)</label>
|
|
<input type="text" class="form-control-glass" placeholder="Leave blank to keep existing password" @bind="editModalNewPassword" />
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 0.75rem; justify-content: flex-end; margin-top: 1.5rem;">
|
|
<button class="btn-sm btn-secondary" @onclick="() => selectedUserForEdit = null">Cancel</button>
|
|
<button class="btn-gradient-primary" style="width: auto;" @onclick="SaveUserEdit">
|
|
<i class="bi bi-check-lg"></i> Save Changes
|
|
</button>
|
|
</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>
|
|
|
|
@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<UserUsageDto> users = new();
|
|
private List<RadAcct> 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<UserUsageDto> 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";
|
|
}
|
|
}
|