using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Security.Claims; using System.Text.Json; using System.Text.Json.Serialization; using System.IdentityModel.Tokens.Jwt; using line_gestao_api.Dtos; using line_gestao_api.Models; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; namespace line_gestao_api.Services; public class AuditLogBuilder : IAuditLogBuilder { private static readonly Dictionary PageByEntity = new(StringComparer.OrdinalIgnoreCase) { { nameof(MobileLine), "Geral" }, { nameof(MuregLine), "Mureg" }, { nameof(BillingClient), "Faturamento" }, { nameof(ParcelamentoLine), "Parcelamentos" }, { nameof(ParcelamentoMonthValue), "Parcelamentos" }, { nameof(UserData), "Dados e Usuários" }, { nameof(ApplicationUser), "Dados e Usuários" }, { nameof(VigenciaLine), "Vigência" }, { nameof(ChipVirgemLine), "Chips Virgens e Recebidos" }, { nameof(ControleRecebidoLine), "Chips Virgens e Recebidos" }, { nameof(TrocaNumeroLine), "Troca de número" } }; private static readonly HashSet IgnoredProperties = new(StringComparer.OrdinalIgnoreCase) { "TenantId", "CreatedAt", "UpdatedAt", "PasswordHash", "SecurityStamp", "ConcurrencyStamp", "NormalizedEmail", "NormalizedUserName", "LockoutEnd", "AccessFailedCount", "LockoutEnabled", "EmailConfirmed", "PhoneNumberConfirmed", "TwoFactorEnabled" }; private static readonly JsonSerializerOptions JsonOptions = new() { DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ITenantProvider _tenantProvider; public AuditLogBuilder(IHttpContextAccessor httpContextAccessor, ITenantProvider tenantProvider) { _httpContextAccessor = httpContextAccessor; _tenantProvider = tenantProvider; } public List BuildAuditLogs(ChangeTracker changeTracker) { var tenantId = _tenantProvider.TenantId; if (tenantId == null) { return new List(); } changeTracker.DetectChanges(); var httpContext = _httpContextAccessor.HttpContext; var userInfo = ResolveUserInfo(httpContext?.User); var request = httpContext?.Request; var requestPath = request?.Path.Value; var requestMethod = request?.Method; var ipAddress = httpContext?.Connection?.RemoteIpAddress?.ToString(); var logs = new List(); foreach (var entry in changeTracker.Entries()) { if (entry.State == EntityState.Detached || entry.State == EntityState.Unchanged) continue; if (entry.Entity is AuditLog) continue; var entityName = entry.Metadata.ClrType.Name; if (!PageByEntity.TryGetValue(entityName, out var page)) continue; var changes = BuildChanges(entry); if (changes.Count == 0) continue; logs.Add(new AuditLog { TenantId = tenantId.Value, OccurredAtUtc = DateTime.UtcNow, UserId = userInfo.UserId, UserName = userInfo.UserName, UserEmail = userInfo.UserEmail, Action = ResolveAction(entry.State), Page = page, EntityName = entityName, EntityId = BuildEntityId(entry), EntityLabel = BuildEntityLabel(entry), ChangesJson = JsonSerializer.Serialize(changes, JsonOptions), RequestPath = requestPath, RequestMethod = requestMethod, IpAddress = ipAddress }); } return logs; } private static string ResolveAction(EntityState state) => state switch { EntityState.Added => "CREATE", EntityState.Modified => "UPDATE", EntityState.Deleted => "DELETE", _ => "UNKNOWN" }; private static List BuildChanges(EntityEntry entry) { var changes = new List(); foreach (var property in entry.Properties) { if (IgnoredProperties.Contains(property.Metadata.Name)) continue; if (entry.State == EntityState.Added) { var newValue = FormatValue(property.CurrentValue); if (newValue == null) continue; changes.Add(new AuditFieldChangeDto { Field = property.Metadata.Name, ChangeType = "added", NewValue = newValue }); } else if (entry.State == EntityState.Deleted) { var oldValue = FormatValue(property.OriginalValue); if (oldValue == null) continue; changes.Add(new AuditFieldChangeDto { Field = property.Metadata.Name, ChangeType = "removed", OldValue = oldValue }); } else if (entry.State == EntityState.Modified) { if (!property.IsModified) continue; var oldValue = FormatValue(property.OriginalValue); var newValue = FormatValue(property.CurrentValue); if (string.Equals(oldValue, newValue, StringComparison.Ordinal)) continue; changes.Add(new AuditFieldChangeDto { Field = property.Metadata.Name, ChangeType = "modified", OldValue = oldValue, NewValue = newValue }); } } return changes; } private static string? BuildEntityId(EntityEntry entry) { var key = entry.Metadata.FindPrimaryKey(); if (key == null) return null; var parts = new List(); foreach (var keyProperty in key.Properties) { var property = entry.Property(keyProperty.Name); var value = FormatValue(property.CurrentValue ?? property.OriginalValue); if (value == null) continue; parts.Add($"{keyProperty.Name}={value}"); } return parts.Count == 0 ? null : string.Join(";", parts); } private static string? BuildEntityLabel(EntityEntry entry) { var entityName = entry.Metadata.ClrType.Name; return entityName switch { nameof(MobileLine) => GetValue(entry, "Linha") ?? GetValue(entry, "Item"), nameof(MuregLine) => GetValue(entry, "LinhaAntiga") ?? GetValue(entry, "LinhaNova") ?? GetValue(entry, "Item"), nameof(TrocaNumeroLine) => GetValue(entry, "LinhaAntiga") ?? GetValue(entry, "LinhaNova") ?? GetValue(entry, "Item"), nameof(ChipVirgemLine) => GetValue(entry, "NumeroDoChip") ?? GetValue(entry, "Item"), nameof(ControleRecebidoLine) => GetValue(entry, "NotaFiscal") ?? GetValue(entry, "Serial") ?? GetValue(entry, "Item"), nameof(BillingClient) => GetValue(entry, "Cliente") ?? GetValue(entry, "Item"), nameof(ParcelamentoLine) => GetValue(entry, "Linha") ?? GetValue(entry, "Cliente") ?? GetValue(entry, "Item"), nameof(ParcelamentoMonthValue) => GetValue(entry, "Competencia") ?? GetValue(entry, "ParcelamentoLineId"), nameof(UserData) => GetValue(entry, "Linha") ?? GetValue(entry, "Cliente") ?? GetValue(entry, "Item"), nameof(VigenciaLine) => GetValue(entry, "Linha") ?? GetValue(entry, "Cliente") ?? GetValue(entry, "Item"), nameof(ApplicationUser) => GetValue(entry, "Email") ?? GetValue(entry, "Name") ?? GetValue(entry, "Id"), _ => null }; } private static string? GetValue(EntityEntry entry, string propertyName) { var property = entry.Properties.FirstOrDefault(p => p.Metadata.Name == propertyName); return property == null ? null : FormatValue(property.CurrentValue ?? property.OriginalValue); } private static string? FormatValue(object? value) { if (value == null) return null; switch (value) { case DateTime dt: var normalized = dt.Kind == DateTimeKind.Utc ? dt : (dt.Kind == DateTimeKind.Local ? dt.ToUniversalTime() : DateTime.SpecifyKind(dt, DateTimeKind.Utc)); return normalized.ToString("O", CultureInfo.InvariantCulture); case DateTimeOffset dto: return dto.ToUniversalTime().ToString("O", CultureInfo.InvariantCulture); case IFormattable formattable: return formattable.ToString(null, CultureInfo.InvariantCulture); default: return value.ToString(); } } private static (Guid? UserId, string? UserName, string? UserEmail) ResolveUserInfo(ClaimsPrincipal? user) { if (user?.Identity?.IsAuthenticated != true) { return (null, "SYSTEM", null); } var idValue = user.FindFirstValue(ClaimTypes.NameIdentifier) ?? user.FindFirstValue(JwtRegisteredClaimNames.Sub) ?? user.FindFirstValue("sub"); var name = user.FindFirstValue("name") ?? user.FindFirstValue(ClaimTypes.Name) ?? user.Identity?.Name; var email = user.FindFirstValue(ClaimTypes.Email) ?? user.FindFirstValue(JwtRegisteredClaimNames.Email) ?? user.FindFirstValue("email"); var userId = Guid.TryParse(idValue, out var parsed) ? parsed : (Guid?)null; var userName = string.IsNullOrWhiteSpace(name) ? email : name; return (userId, userName, email); } }