From a5be1a051f431a381041d07c67da19b9ffd8f9de Mon Sep 17 00:00:00 2001 From: Jerry Phillips Date: Sat, 23 May 2026 17:49:28 -0400 Subject: [PATCH] feat(api): [AB#266] enrich audit log with user display name and org name - Left-join User on FirebaseUid to resolve display name (FirstName + LastName) - Left-join Organization on OrganizationId to include OrganizationName and EmailAddress - Added UserDisplayName, OrganizationName, OrganizationEmail to AuditLogDto - Support hub audit log view now shows readable names instead of raw Firebase UIDs --- JobFlow.Business/Models/DTOs/AuditLogDto.cs | 3 ++ .../Security/AuditLogService.cs | 31 ++++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/JobFlow.Business/Models/DTOs/AuditLogDto.cs b/JobFlow.Business/Models/DTOs/AuditLogDto.cs index ca9465d..78becd3 100644 --- a/JobFlow.Business/Models/DTOs/AuditLogDto.cs +++ b/JobFlow.Business/Models/DTOs/AuditLogDto.cs @@ -4,7 +4,10 @@ public class AuditLogDto { public Guid Id { get; set; } public Guid? OrganizationId { get; set; } + public string? OrganizationName { get; set; } + public string? OrganizationEmail { get; set; } public string? UserId { get; set; } + public string? UserDisplayName { get; set; } public string Category { get; set; } = string.Empty; public string Action { get; set; } = string.Empty; public string ResourceType { get; set; } = string.Empty; diff --git a/JobFlow.Infrastructure/Security/AuditLogService.cs b/JobFlow.Infrastructure/Security/AuditLogService.cs index 75dc65e..6c09259 100644 --- a/JobFlow.Infrastructure/Security/AuditLogService.cs +++ b/JobFlow.Infrastructure/Security/AuditLogService.cs @@ -62,32 +62,40 @@ public async Task>> GetAuditLogsAsync string? cursor = null, CancellationToken cancellationToken = default) { - var repo = _unitOfWork.RepositoryOf(); - var query = repo.QueryWithNoTracking(); + var auditQuery = _unitOfWork.RepositoryOf().QueryWithNoTracking(); + var userQuery = _unitOfWork.RepositoryOf().QueryWithNoTracking(); + var orgQuery = _unitOfWork.RepositoryOf().QueryWithNoTracking(); if (fromUtc.HasValue) - query = query.Where(a => a.CreatedAt >= fromUtc.Value); + auditQuery = auditQuery.Where(a => a.CreatedAt >= fromUtc.Value); if (toUtc.HasValue) - query = query.Where(a => a.CreatedAt <= toUtc.Value); + auditQuery = auditQuery.Where(a => a.CreatedAt <= toUtc.Value); if (!string.IsNullOrWhiteSpace(category)) - query = query.Where(a => a.Category == category); + auditQuery = auditQuery.Where(a => a.Category == category); if (success.HasValue) - query = query.Where(a => a.Success == success.Value); + auditQuery = auditQuery.Where(a => a.Success == success.Value); if (!string.IsNullOrWhiteSpace(cursor) && DateTime.TryParse(cursor, null, System.Globalization.DateTimeStyles.RoundtripKind, out var cursorDate)) - query = query.Where(a => a.CreatedAt < cursorDate); + auditQuery = auditQuery.Where(a => a.CreatedAt < cursorDate); - var items = await query - .OrderByDescending(a => a.CreatedAt) - .Take(pageSize + 1) - .Select(a => new AuditLogDto + var items = await ( + from a in auditQuery + join u in userQuery on a.UserId equals u.FirebaseUid into users + from u in users.DefaultIfEmpty() + join o in orgQuery on a.OrganizationId equals o.Id into orgs + from o in orgs.DefaultIfEmpty() + orderby a.CreatedAt descending + select new AuditLogDto { Id = a.Id, OrganizationId = a.OrganizationId, + OrganizationName = o != null ? o.OrganizationName : null, + OrganizationEmail = o != null ? o.EmailAddress : null, UserId = a.UserId, + UserDisplayName = u != null ? (u.FirstName + " " + u.LastName).Trim() : null, Category = a.Category, Action = a.Action, ResourceType = a.ResourceType, @@ -100,6 +108,7 @@ public async Task>> GetAuditLogsAsync DetailsJson = a.DetailsJson, CreatedAt = a.CreatedAt }) + .Take(pageSize + 1) .ToListAsync(cancellationToken); string? nextCursor = null;