feat: initialize web host and implement background AccountEnforcementService for RADIUS account monitoring
Build and Push Docker Image to Gitea Container Registry / build-and-push (push) Successful in 1m13s

This commit is contained in:
Tygozwolle
2026-07-24 15:50:28 +02:00
parent 77a4e31626
commit c85b598b82
2 changed files with 120 additions and 0 deletions
+1
View File
@@ -15,6 +15,7 @@ builder.Services.AddDbContext<RadiusDbContext>(options =>
// Add services
builder.Services.AddScoped<IRadiusService, RadiusService>();
builder.Services.AddScoped<AdminAuthService>();
builder.Services.AddHostedService<AccountEnforcementService>();
// Add Razor components
builder.Services.AddRazorComponents()
@@ -0,0 +1,119 @@
using Microsoft.EntityFrameworkCore;
using radiuscontroller.Data;
using radiuscontroller.Models;
namespace radiuscontroller.Services;
/// <summary>
/// Background service that periodically checks all active guest accounts
/// and disables them if their time has expired or data cap has been reached.
/// Time starts from the moment the user was created (guest_profiles.created_at).
/// </summary>
public class AccountEnforcementService : BackgroundService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<AccountEnforcementService> _logger;
private static readonly TimeSpan CheckInterval = TimeSpan.FromSeconds(30);
public AccountEnforcementService(IServiceScopeFactory scopeFactory, ILogger<AccountEnforcementService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("AccountEnforcementService started. Checking every {Interval}s.", CheckInterval.TotalSeconds);
// Wait a bit for the app to fully start
await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
try
{
await EnforceAccountLimitsAsync(stoppingToken);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error during account enforcement check.");
}
await Task.Delay(CheckInterval, stoppingToken);
}
}
private async Task EnforceAccountLimitsAsync(CancellationToken ct)
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<RadiusDbContext>();
var activeProfiles = await db.GuestProfiles
.Where(p => p.Status == "Active")
.ToListAsync(ct);
if (activeProfiles.Count == 0) return;
var now = DateTime.UtcNow;
var disabledCount = 0;
foreach (var profile in activeProfiles)
{
string? reason = null;
// 1. Check time expiration: created_at + session_time_minutes
var expiresAt = profile.CreatedAt.AddMinutes(profile.SessionTimeMinutes);
if (now >= expiresAt)
{
reason = "Time expired";
}
// 2. Check data cap: sum acctinputoctets + acctoutputoctets from radacct
if (reason == null && profile.DataLimitMb > 0)
{
var totalOctets = await db.RadAcct
.Where(ra => ra.Username == profile.Username)
.SumAsync(ra => (ra.AcctInputOctets ?? 0) + (ra.AcctOutputOctets ?? 0), ct);
long dataLimitBytes = profile.DataLimitMb * 1024 * 1024;
if (totalOctets >= dataLimitBytes)
{
reason = $"Data cap reached ({totalOctets / (1024 * 1024)} MB / {profile.DataLimitMb} MB)";
}
}
if (reason != null)
{
// Disable the account
profile.Status = "Expired";
// Remove Cleartext-Password from radcheck so FreeRADIUS rejects future auth
var checks = await db.RadCheck
.Where(rc => rc.Username == profile.Username)
.ToListAsync(ct);
db.RadCheck.RemoveRange(checks);
// Insert Auth-Type := Reject to explicitly deny
db.RadCheck.Add(new RadCheck
{
Username = profile.Username,
Attribute = "Auth-Type",
Op = ":=",
Value = "Reject"
});
_logger.LogInformation(
"Account '{Username}' disabled. Reason: {Reason}",
profile.Username, reason);
disabledCount++;
}
}
if (disabledCount > 0)
{
await db.SaveChangesAsync(ct);
_logger.LogInformation("Enforcement check complete. {Count} account(s) disabled.", disabledCount);
}
}
}