using System.ComponentModel.DataAnnotations; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Nuuru.Server.Auth; using Nuuru.Server.DTOs; using Nuuru.Server.DTOs.Moderation; using Nuuru.Server.Extensions; using Nuuru.Server.Services; namespace Nuuru.Server.Controllers { [ApiController] [Route("api/audit-logs")] [Authorize(Policy = Permissions.Moderation.ViewAuditLog)] public class AuditLogController : ControllerBase { private readonly IAuditLogService _auditLogService; private readonly IIpCloakService _ipCloakService; public AuditLogController(IAuditLogService auditLogService, IIpCloakService ipCloakService) { _auditLogService = auditLogService; _ipCloakService = ipCloakService; } [HttpGet] public async Task GetLogs([FromQuery] AuditLogQueryParams query) { var canViewIps = User.HasPermission(Permissions.Moderation.ViewIps); var canViewAssessments = User.HasPermission(Permissions.Moderation.ViewIntegrityAssessments); var canViewBrowserHashes = User.HasPermission(Permissions.Moderation.ViewIntegrityBrowserHashes); // When user can't see raw IPs and is filtering by IP, resolve cloaked values to raw IPs string? ipFilter = null; ICollection? matchIpAddresses = null; if (!string.IsNullOrEmpty(query.IpAddress)) { if (canViewIps) { ipFilter = query.IpAddress; } else { // Try exact uncloak first, fall back to substring search var uncloaked = await _ipCloakService.UncloakAsync(query.IpAddress); if (uncloaked != null) { ipFilter = uncloaked; } else { matchIpAddresses = await _ipCloakService.FindRawIpsByCloakedSubstringAsync(query.IpAddress); } } } if (!canViewBrowserHashes) { query.BrowserHash = null; } var (items, totalCount) = await _auditLogService.GetLogsAsync( action: query.Action, category: query.Category, userId: query.UserId, username: query.Username, ipAddress: ipFilter, matchIpAddresses: matchIpAddresses, browserHash: query.BrowserHash, targetType: query.TargetType, targetId: query.TargetId, dateFrom: query.DateFrom, dateTo: query.DateTo, page: query.Page, pageSize: query.PageSize, exact: query.Exact); var dtos = new List(items.Count()); foreach (var item in items) { var dto = item.ToAuditLogDto(); if (!canViewIps && dto.IpAddress != null) dto.IpAddress = await _ipCloakService.CloakAsync(dto.IpAddress); if (!canViewAssessments && dto.AssessmentId != null) dto.AssessmentId = null; if (!canViewBrowserHashes && dto.BrowserHash != null) dto.BrowserHash = null; dtos.Add(dto); } return Ok(new PagedResult { Items = dtos, Page = query.Page, PageSize = query.PageSize, TotalCount = totalCount, }); } [HttpGet("categories")] public async Task GetCategories() { var categories = await _auditLogService.GetCategoriesAsync(); return Ok(categories); } } public class AuditLogQueryParams { public string? Action { get; set; } public string? Category { get; set; } public Guid? UserId { get; set; } public string? Username { get; set; } public string? IpAddress { get; set; } public string? BrowserHash { get; set; } public string? TargetType { get; set; } public string? TargetId { get; set; } public DateTime? DateFrom { get; set; } public DateTime? DateTo { get; set; } public bool Exact { get; set; } = false; [Range(1, int.MaxValue)] public int Page { get; set; } = 1; [Range(1, 1000)] public int PageSize { get; set; } = 50; } }