using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Caching.Memory; using Nuuru.Server.Data; using Nuuru.Server.Models; namespace Nuuru.Server.Services { public interface IBanService { Task CreateBanAsync(Guid userId, string reason, DateTime? until = null, BanZone zone = BanZone.Sitewide, ApplicationUser? bannedBy = null); Task IsUserBannedAsync(Guid userId, BanZone? zone = null); Task> GetActiveBansAsync(Guid userId); Task GetActiveBanForZoneAsync(Guid userId, BanZone zone); Task> GetBanHistoryAsync(Guid userId); Task UnbanUserAsync(Guid userId, BanZone? zone = null); Task<(IEnumerable Bans, int TotalCount)> GetAllActiveBansAsync(int page, int pageSize, BanZone? zone = null); void InvalidateCache(Guid userId); } public class BanService : IBanService { private readonly ApplicationDbContext _context; private readonly ILogger _logger; private readonly IMemoryCache _cache; private static readonly TimeSpan CacheDuration = TimeSpan.FromSeconds(30); public BanService(ApplicationDbContext context, ILogger logger, IMemoryCache cache) { _context = context; _logger = logger; _cache = cache; } private static string BanCacheKey(Guid userId, BanZone? zone) => $"ban:{userId}:{zone?.ToString() ?? "any"}"; public void InvalidateCache(Guid userId) { _cache.Remove(BanCacheKey(userId, null)); _cache.Remove(BanCacheKey(userId, BanZone.Sitewide)); _cache.Remove(BanCacheKey(userId, BanZone.Booruwide)); _cache.Remove(BanCacheKey(userId, BanZone.Forumwide)); } public async Task CreateBanAsync(Guid userId, string reason, DateTime? until = null, BanZone zone = BanZone.Sitewide, ApplicationUser? bannedBy = null) { var user = await _context.Users.FindAsync(userId); if (user == null) throw new ArgumentException($"User with ID {userId} not found.", nameof(userId)); if (user.IsSystemAccount) throw new InvalidOperationException("Cannot ban system accounts."); var ban = new Ban { User = user, BannedBy = bannedBy, Reason = reason, StartTime = DateTime.UtcNow, EndTime = until ?? DateTime.MaxValue, Zone = zone }; _context.Bans.Add(ban); await _context.SaveChangesAsync(); InvalidateCache(userId); _logger.LogInformation("User {UserId} banned in zone {Zone} until {Until}. Reason: {Reason}", userId, zone, until?.ToString() ?? "indefinitely", reason); return ban; } public async Task IsUserBannedAsync(Guid userId, BanZone? zone = null) { var key = BanCacheKey(userId, zone); if (_cache.TryGetValue(key, out bool cached)) return cached; var query = _context.Bans .Where(b => b.User.Id == userId) .Where(b => b.Active && b.StartTime <= DateTime.UtcNow && b.EndTime > DateTime.UtcNow); if (zone.HasValue) { query = query.Where(b => b.Zone == zone.Value); } var result = await query.AnyAsync(); _cache.Set(key, result, CacheDuration); return result; } public async Task> GetActiveBansAsync(Guid userId) { return await _context.Bans .Include(b => b.User) .Where(b => b.User.Id == userId) .Where(b => b.Active && b.StartTime <= DateTime.UtcNow && b.EndTime > DateTime.UtcNow) .OrderByDescending(b => b.StartTime) .ToListAsync(); } public async Task GetActiveBanForZoneAsync(Guid userId, BanZone zone) { return await _context.Bans .Include(b => b.User) .Where(b => b.User.Id == userId && b.Zone == zone) .Where(b => b.Active && b.StartTime <= DateTime.UtcNow && b.EndTime > DateTime.UtcNow) .OrderByDescending(b => b.StartTime) .FirstOrDefaultAsync(); } public async Task> GetBanHistoryAsync(Guid userId) { return await _context.Bans .Include(b => b.User) .Where(b => b.User.Id == userId) .OrderByDescending(b => b.StartTime) .ToListAsync(); } public async Task UnbanUserAsync(Guid userId, BanZone? zone = null) { var query = _context.Bans .Where(b => b.User.Id == userId) .Where(b => b.Active && b.EndTime > DateTime.UtcNow); if (zone.HasValue) { query = query.Where(b => b.Zone == zone.Value); } var activeBans = await query.ToListAsync(); if (!activeBans.Any()) return false; foreach (var ban in activeBans) { ban.Active = false; } await _context.SaveChangesAsync(); InvalidateCache(userId); _logger.LogInformation("User {UserId} unbanned from {ZoneInfo}", userId, zone.HasValue ? $"zone {zone.Value}" : "all zones"); return true; } public async Task<(IEnumerable Bans, int TotalCount)> GetAllActiveBansAsync(int page, int pageSize, BanZone? zone = null) { page = Math.Max(1, page); pageSize = Math.Clamp(pageSize, 1, 100); var query = _context.Bans .Include(b => b.User) .Where(b => b.Active && b.StartTime <= DateTime.UtcNow && b.EndTime > DateTime.UtcNow); if (zone.HasValue) { query = query.Where(b => b.Zone == zone.Value); } var totalCount = await query.CountAsync(); var bans = await query .OrderByDescending(b => b.StartTime) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return (bans, totalCount); } } }