371 lines
12 KiB
C#
371 lines
12 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text.RegularExpressions;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using radiuscontroller.Data;
|
|
using radiuscontroller.Models;
|
|
|
|
namespace radiuscontroller.Services;
|
|
|
|
public interface IRadiusService
|
|
{
|
|
Task<SystemConfigDto> GetSystemConfigAsync();
|
|
Task UpdateSystemConfigAsync(int defaultSessionTimeMinutes, long defaultDataLimitMb);
|
|
Task<RegistrationResultDto> RegisterGuestAsync(string guestName);
|
|
Task<List<UserUsageDto>> GetAllUsersWithUsageAsync();
|
|
Task<List<RadAcct>> 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<RadiusService> _logger;
|
|
|
|
public RadiusService(RadiusDbContext db, ILogger<RadiusService> logger)
|
|
{
|
|
_db = db;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task<SystemConfigDto> 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<RegistrationResultDto> 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<List<UserUsageDto>> 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<UserUsageDto>();
|
|
|
|
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<List<RadAcct>> 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.AdminUsers.AnyAsync())
|
|
{
|
|
// Default admin user: admin / admin123
|
|
string hash = BCrypt.Net.BCrypt.HashPassword("admin123");
|
|
_db.AdminUsers.Add(new AdminUser
|
|
{
|
|
Username = "admin",
|
|
PasswordHash = hash,
|
|
CreatedAt = DateTime.UtcNow
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|