using Microsoft.EntityFrameworkCore; using radiuscontroller.Data; using radiuscontroller.Models; namespace radiuscontroller.Services; /// /// 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). /// public class AccountEnforcementService : BackgroundService { private readonly IServiceScopeFactory _scopeFactory; private readonly ILogger _logger; private static readonly TimeSpan CheckInterval = TimeSpan.FromSeconds(30); public AccountEnforcementService(IServiceScopeFactory scopeFactory, ILogger 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(); 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); } } }