using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; using Nuuru.Server.Models; namespace Nuuru.Server.Services { public interface IIpBanAppealService { Task CreateAppealAsync(string ipAddress, Guid ipBanId, string reason, Guid? userId = null); Task HasPendingAppealAsync(Guid ipBanId); Task<(IEnumerable Items, int TotalCount)> GetAppealsAsync(BanAppealStatus? status, int page, int pageSize); Task AcceptAppealAsync(Guid appealId, ApplicationUser moderator, string? note); Task RejectAppealAsync(Guid appealId, ApplicationUser moderator, string? note, bool denyFurtherAppeals = false); Task> GetUserAppealsAsync(Guid userId); Task> GetIpAppealsAsync(string ipAddress); Task GetPendingAppealCountAsync(); } public class IpBanAppealService : IIpBanAppealService { private readonly ApplicationDbContext _context; private readonly IIpBanService _ipBanService; private readonly ILogger _logger; public IpBanAppealService( ApplicationDbContext context, IIpBanService ipBanService, ILogger logger) { _context = context; _ipBanService = ipBanService; _logger = logger; } public async Task CreateAppealAsync(string ipAddress, Guid ipBanId, string reason, Guid? userId = null) { var ban = await _context.IpBans .FirstOrDefaultAsync(b => b.Id == ipBanId); if (ban == null) throw new ArgumentException("Ban not found."); if (ban.IpAddress != ipAddress) throw new UnauthorizedAccessException("You can only appeal bans for your own IP address."); if (ban.AppealsDenied) throw new InvalidOperationException("Appeals have been permanently denied for this ban."); var hasPending = await HasPendingAppealAsync(ipBanId); if (hasPending) throw new InvalidOperationException("There is already a pending appeal for this ban."); var appeal = new IpBanAppeal { IpBanId = ipBanId, IpAddress = ipAddress, UserId = userId, Reason = reason, Status = BanAppealStatus.Pending, CreatedAt = DateTime.UtcNow }; _context.IpBanAppeals.Add(appeal); await _context.SaveChangesAsync(); _logger.LogInformation("IP ban appeal created for IP {IpAddress}, ban {BanId}", ipAddress, ipBanId); return appeal; } public async Task HasPendingAppealAsync(Guid ipBanId) { return await _context.IpBanAppeals .AnyAsync(a => a.IpBanId == ipBanId && a.Status == BanAppealStatus.Pending); } public async Task<(IEnumerable Items, int TotalCount)> GetAppealsAsync(BanAppealStatus? status, int page, int pageSize) { page = Math.Max(1, page); pageSize = Math.Clamp(pageSize, 1, 100); var query = _context.IpBanAppeals .Include(a => a.User) .Include(a => a.Moderator) .Include(a => a.IpBan) .AsQueryable(); if (status.HasValue) { query = query.Where(a => a.Status == status.Value); } var totalCount = await query.CountAsync(); var items = await query .OrderByDescending(a => a.CreatedAt) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return (items, totalCount); } public async Task AcceptAppealAsync(Guid appealId, ApplicationUser moderator, string? note) { var appeal = await _context.IpBanAppeals .Include(a => a.IpBan) .FirstOrDefaultAsync(a => a.Id == appealId); if (appeal == null) return null; if (appeal.Status != BanAppealStatus.Pending) throw new InvalidOperationException("This appeal has already been resolved."); if (appeal.UserId == moderator.Id) throw new UnauthorizedAccessException("You cannot resolve your own ban appeal."); appeal.Status = BanAppealStatus.Accepted; appeal.ResolvedAt = DateTime.UtcNow; appeal.ModeratorNote = note; appeal.ModeratorId = moderator.Id; // Lift the IP ban await _ipBanService.RemoveIpBanAsync(appeal.IpBanId); await _context.SaveChangesAsync(); _logger.LogInformation("IP ban appeal {AppealId} accepted by moderator {ModeratorId}, IP ban {BanId} lifted", appealId, moderator.Id, appeal.IpBanId); return appeal; } public async Task RejectAppealAsync(Guid appealId, ApplicationUser moderator, string? note, bool denyFurtherAppeals = false) { var appeal = await _context.IpBanAppeals .Include(a => a.IpBan) .FirstOrDefaultAsync(a => a.Id == appealId); if (appeal == null) return null; if (appeal.Status != BanAppealStatus.Pending) throw new InvalidOperationException("This appeal has already been resolved."); if (appeal.UserId == moderator.Id) throw new UnauthorizedAccessException("You cannot resolve your own ban appeal."); appeal.Status = BanAppealStatus.Rejected; appeal.ResolvedAt = DateTime.UtcNow; appeal.ModeratorNote = note; appeal.ModeratorId = moderator.Id; if (denyFurtherAppeals) { appeal.IpBan.AppealsDenied = true; } await _context.SaveChangesAsync(); _logger.LogInformation("IP ban appeal {AppealId} rejected by moderator {ModeratorId}{DenyNote}", appealId, moderator.Id, denyFurtherAppeals ? " (further appeals denied)" : ""); return appeal; } public async Task> GetUserAppealsAsync(Guid userId) { return await _context.IpBanAppeals .Include(a => a.IpBan) .Where(a => a.UserId == userId) .OrderByDescending(a => a.CreatedAt) .ToListAsync(); } public async Task> GetIpAppealsAsync(string ipAddress) { return await _context.IpBanAppeals .Include(a => a.IpBan) .Where(a => a.IpAddress == ipAddress) .OrderByDescending(a => a.CreatedAt) .ToListAsync(); } public async Task GetPendingAppealCountAsync() { return await _context.IpBanAppeals .CountAsync(a => a.Status == BanAppealStatus.Pending); } } }