using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Nuuru.Server.Data; using Nuuru.Server.Models; namespace Nuuru.Server.Services { public interface IIpBanService { Task CreateIpBanAsync(string ipAddress, string? reason, DateTime? until = null, Guid? createdById = null); Task IsIpBannedAsync(string ipAddress); Task GetActiveIpBanAsync(string ipAddress); Task RemoveIpBanAsync(Guid id); Task<(IEnumerable Bans, int TotalCount)> GetActiveIpBansAsync(int page, int pageSize, string? search = null, ICollection? matchIpAddresses = null); Task> GetDistinctBannedIpAddressesAsync(); void InvalidateCache(string ipAddress); } public class IpBanService : IIpBanService { private readonly ApplicationDbContext _context; private readonly ILogger _logger; private readonly IMemoryCache _cache; private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(30); public IpBanService(ApplicationDbContext context, ILogger logger, IMemoryCache cache) { _context = context; _logger = logger; _cache = cache; } private static string CacheKey(string ip) => $"ipban:{ip}"; public void InvalidateCache(string ipAddress) { _cache.Remove(CacheKey(ipAddress)); } public async Task CreateIpBanAsync(string ipAddress, string? reason, DateTime? until = null, Guid? createdById = null) { var ban = new IpBan { IpAddress = ipAddress, Reason = reason, StartTime = DateTime.UtcNow, EndTime = until ?? DateTime.MaxValue, CreatedById = createdById, }; _context.IpBans.Add(ban); await _context.SaveChangesAsync(); InvalidateCache(ipAddress); _logger.LogInformation("IP {IpAddress} banned until {Until}. Reason: {Reason}", ipAddress, until?.ToString() ?? "indefinitely", reason ?? "none"); return ban; } public async Task IsIpBannedAsync(string ipAddress) { var key = CacheKey(ipAddress); if (_cache.TryGetValue(key, out bool cached)) return cached; var result = await _context.IpBans .Where(b => b.IpAddress == ipAddress) .Where(b => b.Active && b.StartTime <= DateTime.UtcNow && b.EndTime > DateTime.UtcNow) .AnyAsync(); _cache.Set(key, result, CacheDuration); return result; } public async Task GetActiveIpBanAsync(string ipAddress) { return await _context.IpBans .Include(b => b.CreatedBy) .Where(b => b.IpAddress == ipAddress) .Where(b => b.Active && b.StartTime <= DateTime.UtcNow && b.EndTime > DateTime.UtcNow) .OrderByDescending(b => b.StartTime) .FirstOrDefaultAsync(); } public async Task RemoveIpBanAsync(Guid id) { var ban = await _context.IpBans.FindAsync(id); if (ban == null) return false; ban.Active = false; await _context.SaveChangesAsync(); InvalidateCache(ban.IpAddress); _logger.LogInformation("IP ban {BanId} for {IpAddress} removed", id, ban.IpAddress); return true; } public async Task<(IEnumerable Bans, int TotalCount)> GetActiveIpBansAsync(int page, int pageSize, string? search = null, ICollection? matchIpAddresses = null) { page = Math.Max(1, page); pageSize = Math.Clamp(pageSize, 1, 9999); var query = _context.IpBans .Include(b => b.CreatedBy) .Where(b => b.Active && b.StartTime <= DateTime.UtcNow && b.EndTime > DateTime.UtcNow); if (matchIpAddresses != null) { query = query.Where(b => matchIpAddresses.Contains(b.IpAddress)); } else if (!string.IsNullOrWhiteSpace(search)) { query = query.Where(b => b.IpAddress.Contains(search)); } var totalCount = await query.CountAsync(); var bans = await query .OrderByDescending(b => b.StartTime) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return (bans, totalCount); } public async Task> GetDistinctBannedIpAddressesAsync() { return await _context.IpBans .Where(b => b.Active && b.StartTime <= DateTime.UtcNow && b.EndTime > DateTime.UtcNow) .Select(b => b.IpAddress) .Distinct() .ToListAsync(); } } }