299 lines
11 KiB
C#
299 lines
11 KiB
C#
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
|
|
{
|
|
public const string SpreadsheetImportPageName = "Importação de Planilha";
|
|
|
|
private static readonly Dictionary<string, string> 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<string> 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<AuditLog> BuildAuditLogs(ChangeTracker changeTracker)
|
|
{
|
|
var tenantId = _tenantProvider.TenantId;
|
|
if (tenantId == null)
|
|
{
|
|
return new List<AuditLog>();
|
|
}
|
|
|
|
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();
|
|
|
|
if (IsSpreadsheetImportRequest(requestPath))
|
|
{
|
|
// Importacoes de planilha nao geram historico detalhado por entidade.
|
|
return new List<AuditLog>();
|
|
}
|
|
|
|
var logs = new List<AuditLog>();
|
|
|
|
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 bool IsSpreadsheetImportRequest(string? requestPath)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(requestPath))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return requestPath.Contains("/import-excel", StringComparison.OrdinalIgnoreCase);
|
|
}
|
|
|
|
private static string ResolveAction(EntityState state)
|
|
=> state switch
|
|
{
|
|
EntityState.Added => "CREATE",
|
|
EntityState.Modified => "UPDATE",
|
|
EntityState.Deleted => "DELETE",
|
|
_ => "UNKNOWN"
|
|
};
|
|
|
|
private static List<AuditFieldChangeDto> BuildChanges(EntityEntry entry)
|
|
{
|
|
var changes = new List<AuditFieldChangeDto>();
|
|
|
|
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<string>();
|
|
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);
|
|
}
|
|
}
|