using System.Security.Cryptography; using System.Text.RegularExpressions; using Microsoft.EntityFrameworkCore; using radiuscontroller.Data; using radiuscontroller.Models; namespace radiuscontroller.Services; public interface IRadiusService { Task GetSystemConfigAsync(); Task UpdateSystemConfigAsync(int defaultSessionTimeMinutes, long defaultDataLimitMb); Task RegisterGuestAsync(string guestName); Task> GetAllUsersWithUsageAsync(); Task> GetActiveSessionsAsync(); Task UpdateUserLimitsAsync(int profileId, int newSessionTimeMinutes, long newDataLimitMb); Task RevokeUserAsync(int profileId); Task DeleteUserAsync(int profileId); Task ResetUserPasswordAsync(int profileId, string newPassword); Task EnsureDatabaseCreatedAndSeededAsync(); } public class RadiusService : IRadiusService { private readonly RadiusDbContext _db; private readonly ILogger _logger; public RadiusService(RadiusDbContext db, ILogger logger) { _db = db; _logger = logger; } public async Task GetSystemConfigAsync() { var settings = await _db.SystemSettings.ToDictionaryAsync(s => s.Key, s => s.Value); int sessionTime = 60; // 60 minutes default if (settings.TryGetValue("default_session_time", out var sessionStr) && int.TryParse(sessionStr, out var parsedSession)) { sessionTime = parsedSession; } long dataLimit = 500; // 500 MB default if (settings.TryGetValue("default_data_limit", out var dataStr) && long.TryParse(dataStr, out var parsedData)) { dataLimit = parsedData; } return new SystemConfigDto { DefaultSessionTimeMinutes = sessionTime, DefaultDataLimitMb = dataLimit }; } public async Task UpdateSystemConfigAsync(int defaultSessionTimeMinutes, long defaultDataLimitMb) { await SetOrUpdateSettingAsync("default_session_time", defaultSessionTimeMinutes.ToString()); await SetOrUpdateSettingAsync("default_data_limit", defaultDataLimitMb.ToString()); await _db.SaveChangesAsync(); } private async Task SetOrUpdateSettingAsync(string key, string value) { var existing = await _db.SystemSettings.FirstOrDefaultAsync(s => s.Key == key); if (existing != null) { existing.Value = value; existing.UpdatedAt = DateTime.UtcNow; } else { _db.SystemSettings.Add(new SystemSetting { Key = key, Value = value, UpdatedAt = DateTime.UtcNow }); } } public async Task RegisterGuestAsync(string guestName) { if (string.IsNullOrWhiteSpace(guestName)) { throw new ArgumentException("Guest name cannot be empty.", nameof(guestName)); } var config = await GetSystemConfigAsync(); // Format clean username base string cleanName = Regex.Replace(guestName.Trim().ToLowerInvariant(), @"[^a-z0-9]", ""); if (string.IsNullOrEmpty(cleanName)) cleanName = "guest"; if (cleanName.Length > 12) cleanName = cleanName[..12]; string username = $"{cleanName}_{RandomNumberGenerator.GetInt32(1000, 9999)}"; string password = GenerateReadablePassword(8); // Save Cleartext-Password for FreeRADIUS _db.RadCheck.Add(new RadCheck { Username = username, Attribute = "Cleartext-Password", Op = ":=", Value = password }); // Set Session-Timeout (in seconds) long sessionTimeoutSeconds = (long)config.DefaultSessionTimeMinutes * 60; _db.RadReply.Add(new RadReply { Username = username, Attribute = "Session-Timeout", Op = "=", Value = sessionTimeoutSeconds.ToString() }); // Set Max-Octets (in bytes) long maxOctetsBytes = config.DefaultDataLimitMb * 1024 * 1024; _db.RadReply.Add(new RadReply { Username = username, Attribute = "Max-Octets", Op = "=", Value = maxOctetsBytes.ToString() }); // Create Guest Profile record var profile = new GuestProfile { GuestName = guestName.Trim(), Username = username, CreatedAt = DateTime.UtcNow, SessionTimeMinutes = config.DefaultSessionTimeMinutes, DataLimitMb = config.DefaultDataLimitMb, Status = "Active" }; _db.GuestProfiles.Add(profile); await _db.SaveChangesAsync(); return new RegistrationResultDto { GuestName = profile.GuestName, Username = username, Password = password, SessionTimeMinutes = config.DefaultSessionTimeMinutes, DataLimitMb = config.DefaultDataLimitMb, ExpirationEstimate = DateTime.UtcNow.AddMinutes(config.DefaultSessionTimeMinutes) }; } public async Task> GetAllUsersWithUsageAsync() { var profiles = await _db.GuestProfiles.OrderByDescending(p => p.CreatedAt).ToListAsync(); var passwords = await _db.RadCheck .Where(rc => rc.Attribute == "Cleartext-Password") .ToDictionaryAsync(rc => rc.Username, rc => rc.Value); // Group accounting data by username var acctStats = await _db.RadAcct .GroupBy(ra => ra.Username) .Select(g => new { Username = g.Key, TotalInput = g.Sum(x => x.AcctInputOctets ?? 0), TotalOutput = g.Sum(x => x.AcctOutputOctets ?? 0), TotalSessionTime = g.Sum(x => x.AcctSessionTime ?? 0), IsConnected = g.Any(x => x.AcctStopTime == null) }) .ToDictionaryAsync(g => g.Username); var result = new List(); foreach (var p in profiles) { passwords.TryGetValue(p.Username, out var pwd); acctStats.TryGetValue(p.Username, out var acct); result.Add(new UserUsageDto { ProfileId = p.Id, GuestName = p.GuestName, Username = p.Username, Password = pwd ?? "••••••••", CreatedAt = p.CreatedAt, SessionTimeMinutes = p.SessionTimeMinutes, DataLimitMb = p.DataLimitMb, Status = p.Status, TotalInputOctets = acct?.TotalInput ?? 0, TotalOutputOctets = acct?.TotalOutput ?? 0, TotalSessionTimeSeconds = acct?.TotalSessionTime ?? 0, IsCurrentlyConnected = acct?.IsConnected ?? false }); } return result; } public async Task> GetActiveSessionsAsync() { return await _db.RadAcct .Where(ra => ra.AcctStopTime == null) .OrderByDescending(ra => ra.AcctStartTime) .ToListAsync(); } public async Task UpdateUserLimitsAsync(int profileId, int newSessionTimeMinutes, long newDataLimitMb) { var profile = await _db.GuestProfiles.FindAsync(profileId); if (profile == null) return; profile.SessionTimeMinutes = newSessionTimeMinutes; profile.DataLimitMb = newDataLimitMb; // Update RadReply Session-Timeout var sessionReply = await _db.RadReply.FirstOrDefaultAsync(rr => rr.Username == profile.Username && rr.Attribute == "Session-Timeout"); if (sessionReply != null) { sessionReply.Value = (newSessionTimeMinutes * 60).ToString(); } else { _db.RadReply.Add(new RadReply { Username = profile.Username, Attribute = "Session-Timeout", Op = "=", Value = (newSessionTimeMinutes * 60).ToString() }); } // Update RadReply Max-Octets var dataReply = await _db.RadReply.FirstOrDefaultAsync(rr => rr.Username == profile.Username && rr.Attribute == "Max-Octets"); long maxOctetsBytes = newDataLimitMb * 1024 * 1024; if (dataReply != null) { dataReply.Value = maxOctetsBytes.ToString(); } else { _db.RadReply.Add(new RadReply { Username = profile.Username, Attribute = "Max-Octets", Op = "=", Value = maxOctetsBytes.ToString() }); } await _db.SaveChangesAsync(); } public async Task RevokeUserAsync(int profileId) { var profile = await _db.GuestProfiles.FindAsync(profileId); if (profile == null) return; profile.Status = "Revoked"; // Remove Cleartext-Password and insert Auth-Type := Reject var checks = await _db.RadCheck.Where(rc => rc.Username == profile.Username).ToListAsync(); _db.RadCheck.RemoveRange(checks); _db.RadCheck.Add(new RadCheck { Username = profile.Username, Attribute = "Auth-Type", Op = ":=", Value = "Reject" }); await _db.SaveChangesAsync(); } public async Task DeleteUserAsync(int profileId) { var profile = await _db.GuestProfiles.FindAsync(profileId); if (profile == null) return; var username = profile.Username; var checks = await _db.RadCheck.Where(rc => rc.Username == username).ToListAsync(); var replies = await _db.RadReply.Where(rr => rr.Username == username).ToListAsync(); _db.RadCheck.RemoveRange(checks); _db.RadReply.RemoveRange(replies); _db.GuestProfiles.Remove(profile); await _db.SaveChangesAsync(); } public async Task ResetUserPasswordAsync(int profileId, string newPassword) { var profile = await _db.GuestProfiles.FindAsync(profileId); if (profile == null) return; var check = await _db.RadCheck.FirstOrDefaultAsync(rc => rc.Username == profile.Username && rc.Attribute == "Cleartext-Password"); if (check != null) { check.Value = newPassword; } else { _db.RadCheck.Add(new RadCheck { Username = profile.Username, Attribute = "Cleartext-Password", Op = ":=", Value = newPassword }); } if (profile.Status == "Revoked") { profile.Status = "Active"; var rejectCheck = await _db.RadCheck.FirstOrDefaultAsync(rc => rc.Username == profile.Username && rc.Attribute == "Auth-Type"); if (rejectCheck != null) { _db.RadCheck.Remove(rejectCheck); } } await _db.SaveChangesAsync(); } public async Task EnsureDatabaseCreatedAndSeededAsync() { try { await _db.Database.EnsureCreatedAsync(); if (!await _db.SystemSettings.AnyAsync()) { _db.SystemSettings.Add(new SystemSetting { Key = "default_session_time", Value = "60", UpdatedAt = DateTime.UtcNow }); _db.SystemSettings.Add(new SystemSetting { Key = "default_data_limit", Value = "500", UpdatedAt = DateTime.UtcNow }); } await _db.SaveChangesAsync(); } catch (Exception ex) { _logger.LogError(ex, "Database initialization warning (Database might be populated via Docker init.sql script)."); } } private static string GenerateReadablePassword(int length) { const string chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; var result = new char[length]; for (int i = 0; i < length; i++) { result[i] = chars[RandomNumberGenerator.GetInt32(chars.Length)]; } return new string(result); } }