using Microsoft.EntityFrameworkCore; using Nuuru.Server.Data; using Nuuru.Server.Models; namespace Nuuru.Server.Services { public interface IAuditLogService { Task<(IEnumerable Items, int TotalCount)> GetLogsAsync( string? action = null, string? category = null, Guid? userId = null, string? username = null, string? ipAddress = null, ICollection? matchIpAddresses = null, string? browserHash = null, string? targetType = null, string? targetId = null, DateTime? dateFrom = null, DateTime? dateTo = null, int page = 1, int pageSize = 50, bool exact = false); Task> GetDistinctIpAddressesAsync(); Task> GetCategoriesAsync(); } public class AuditLogService : IAuditLogService { private readonly ApplicationDbContext _context; public AuditLogService(ApplicationDbContext context) { _context = context; } public async Task<(IEnumerable Items, int TotalCount)> GetLogsAsync( string? action = null, string? category = null, Guid? userId = null, string? username = null, string? ipAddress = null, ICollection? matchIpAddresses = null, string? browserHash = null, string? targetType = null, string? targetId = null, DateTime? dateFrom = null, DateTime? dateTo = null, int page = 1, int pageSize = 50, bool exact = false) { var query = _context.AuditLogs .Include(a => a.User) .AsQueryable(); if (!string.IsNullOrEmpty(action)) { if (exact) query = query.Where(a => a.Action.ToLower() == action.ToLower()); else query = query.Where(a => a.Action.ToLower().Contains(action.ToLower())); } if (!string.IsNullOrEmpty(category)) query = query.Where(a => a.Category == category); if (userId.HasValue) query = query.Where(a => a.UserId == userId.Value); if (!string.IsNullOrEmpty(username)) { if (exact) query = query.Where(a => a.User != null && a.User.UserName!.ToLower() == username.ToLower()); else query = query.Where(a => a.User != null && a.User.UserName!.ToLower().Contains(username.ToLower())); } if (!string.IsNullOrEmpty(ipAddress)) { if (exact) query = query.Where(a => a.IpAddress != null && a.IpAddress.ToLower() == ipAddress.ToLower()); else query = query.Where(a => a.IpAddress != null && a.IpAddress.ToLower().Contains(ipAddress.ToLower())); } else if (matchIpAddresses != null) query = query.Where(a => a.IpAddress != null && matchIpAddresses.Contains(a.IpAddress)); if (!string.IsNullOrEmpty(browserHash)) { // Always apply exact match query = query.Where(a => a.BrowserHash == browserHash); } if (!string.IsNullOrEmpty(targetType)) query = query.Where(a => a.TargetType == targetType); if (!string.IsNullOrEmpty(targetId)) { var lowerTargetId = targetId.ToLower(); // First pass: Broad database filter to keep results manageable query = query.Where(a => a.TargetId != null && a.TargetId.ToLower().Contains(lowerTargetId)); // Note: We'll apply the stricter "only last part" or "either arrow part" logic // after ToListAsync to ensure correctness across all DB providers without complex SQL. } if (dateFrom.HasValue) query = query.Where(a => a.Timestamp >= dateFrom.Value); if (dateTo.HasValue) query = query.Where(a => a.Timestamp <= dateTo.Value); // We need to fetch and filter in memory if targetId is provided to ensure "last part" logic if (!string.IsNullOrEmpty(targetId)) { var lowerTargetId = targetId.ToLower(); var allItems = await query .OrderByDescending(a => a.Timestamp) .ToListAsync(); var filteredItems = allItems.Where(a => { if (a.TargetId == null) return false; var tid = a.TargetId.ToLower(); if (exact) { if (tid == lowerTargetId) return true; if (tid.Contains(":") && tid.Split(':').Last() == lowerTargetId) return true; if (tid.Contains(" -> ")) { var parts = tid.Split(" -> "); return parts.Any(p => p.Trim() == lowerTargetId); } return false; } else { // Hierarchical: only look at the last part if (tid.Contains(":")) { return tid.Split(':').Last().Contains(lowerTargetId); } // Arrow: look at either part if (tid.Contains(" -> ")) { var parts = tid.Split(" -> "); return parts.Any(p => p.Trim().Contains(lowerTargetId)); } // Simple return tid.Contains(lowerTargetId); } }).ToList(); var count = filteredItems.Count; var pagedItems = filteredItems .Skip((page - 1) * pageSize) .Take(pageSize); return (pagedItems, count); } var totalCount = await query.CountAsync(); var items = await query .OrderByDescending(a => a.Timestamp) .Skip((page - 1) * pageSize) .Take(pageSize) .ToListAsync(); return (items, totalCount); } public async Task> GetDistinctIpAddressesAsync() { return await _context.AuditLogs .Where(a => a.IpAddress != null) .Select(a => a.IpAddress!) .Distinct() .ToListAsync(); } public async Task> GetCategoriesAsync() { return await _context.AuditLogs .Select(a => a.Category) .Distinct() .OrderBy(c => c) .ToListAsync(); } } }