add girIgnore

This commit is contained in:
jrojas
2026-06-23 19:03:17 +02:00
parent 52335cc5fa
commit 95c9039c78
321 changed files with 84 additions and 12748 deletions
+475
View File
@@ -0,0 +1,475 @@
using MongoDB.Bson.IO;
using MongoDB.Bson.Serialization;
using JsonConvert = Newtonsoft.Json.JsonConvert;
namespace audit_logs.Services;
using System.Security.Claims;
using System.Text.RegularExpressions;
using audit_logs.Models;
using audit_logs.Models.AppSettings;
using audit_logs.Models.DTO;
using audit_logs.Services.Interfaces;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using audit_logs.Repositories;
using MongoDB.Driver;
using audit_logs.Utils;
using audit.Model;
using audit_logs.Repositories.Interfaces;
using Serilog;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
public class AuditService : IAuditService
{
private readonly IAuditRecordRepository _auditLogRepository;
public AuditService(IAuditRecordRepository auditLogRepository)
{
_auditLogRepository = auditLogRepository;
}
public AuditService(IMongoDatabase database)
{
_auditLogRepository = new AuditRecordRepository(database);
}
public AuditService(IOptions<MongoDbSettingsAudit> dbSettings)
{
_auditLogRepository = new AuditRecordRepository(dbSettings);
}
public IAuditRecordRepository AuditLogRepository => _auditLogRepository;
/// <summary>
/// Records a detailed audit log entry.
/// </summary>
/// <param name="auditRecord">The data required to create an audit record.</param>
public async Task RecordAuditAsync(AuditRecord auditRecord)
{
if (auditRecord == null) throw new ArgumentNullException(nameof(auditRecord));
if (string.IsNullOrWhiteSpace(auditRecord.EntityType))
throw new ArgumentNullException(nameof(auditRecord.EntityType));
if (string.IsNullOrWhiteSpace(auditRecord.RecordId))
throw new ArgumentNullException(nameof(auditRecord.RecordId));
if (string.IsNullOrWhiteSpace(auditRecord.UserId))
throw new ArgumentNullException(nameof(auditRecord.UserId));
if (string.IsNullOrWhiteSpace(auditRecord.ActionType))
throw new ArgumentNullException(nameof(auditRecord.ActionType));
if (auditRecord.Changes == null) throw new ArgumentNullException(nameof(auditRecord.Changes));
if (string.IsNullOrWhiteSpace(auditRecord.UserIpAddress))
throw new ArgumentNullException(nameof(auditRecord.UserIpAddress));
await _auditLogRepository.InsertOneAsync(auditRecord);
}
public Task<T> JsonDeepCopyAsync<T>(T source)
{
return Task.Run(() =>
{
var serialized = JsonConvert.SerializeObject(source);
var deserialized = JsonConvert.DeserializeObject<T>(serialized);
if (deserialized == null)
throw new InvalidOperationException("Deserialization resulted in null");
return deserialized;
});
}
public Task<T> DeepCopyAsync<T>(T source)
{
T copy;
using (var stream = new MemoryStream())
{
// Serializa el objeto al stream en formato BSON
using (var writer = new BsonBinaryWriter(stream))
{
BsonSerializer.Serialize(writer, source);
}
// Reinicia la posición del stream para leer desde el inicio
stream.Seek(0, SeekOrigin.Begin);
// Deserializa el objeto desde el stream
using (var reader = new BsonBinaryReader(stream))
{
copy = BsonSerializer.Deserialize<T>(reader);
}
}
return Task.FromResult(copy);
}
#region Documentacioòn para el metodo CreateAuditLogAsync
/// <summary>
/// Creates an audit log record based on the provided original and modified data.
/// This method generates an <see cref="AuditRecord"/> object to encapsulate all necessary details for the audit log entry.
/// </summary>
/// <param name="user">
/// The <see cref="ClaimsPrincipal"/> representing the user performing the action.
/// This parameter is required and is used to populate the <c>UserId</c> and optionally other user-related fields.
/// </param>
/// <param name="dataOriginal">
/// The original object before the change. This parameter can be null for new records (when <c>ActionType</c> is "create").
/// </param>
/// <param name="dataModified">
/// The modified object after the change. This parameter can be null for deleted records (when <c>ActionType</c> is "delete").
/// </param>
/// <param name="reason">
/// The reason for the change. This parameter is optional; if null or empty, it defaults to "Not specified".
/// </param>
/// <exception cref="ArgumentNullException">
/// Thrown if <paramref name="user"/> is null.
/// </exception>
/// <remarks>
/// This method constructs and populates an <see cref="AuditRecord"/> object that contains the following fields:
/// <list type="bullet">
/// <item>
/// <description><c>EntityType</c>: The type of entity being audited (e.g., "Patient").</description>
/// </item>
/// <item>
/// <description><c>RecordId</c>: The unique identifier of the record being modified within the entity (e.g. patient.id, nhc,etc) <c>EntityType</c>.</description>
/// </item>
/// <item>
/// <description><c>UserId</c>: The ID of the user who performed the action that triggered the audit entry.</description>
/// </item>
/// <item>
/// <description><c>ActionType</c>: The type of action performed, which can be one of the following:
/// <list type="bullet">
/// <item><description><c>"create"</c>: Indicates a new record was created.</description></item>
/// <item><description><c>"update"</c>: Indicates an existing record was updated.</description></item>
/// <item><description><c>"delete"</c>: Indicates a record was removed.</description></item>
/// </list>
/// </description>
/// </item>
/// <item>
/// <description><c>Reason</c>: (Optional) A description or justification for the action being logged.</description>
/// </item>
/// <item>
/// <description><c>ActionTime</c>: The timestamp of when the action occurred, represented in UTC.</description>
/// </item>
/// <item>
/// <description><c>Changes</c>: A list detailing the differences between the original and modified data. Each change includes:
/// <list type="bullet">
/// <item><description><c>Field</c>: The name of the field that was changed.</description></item>
/// <item><description><c>OldValue</c>: The value of the field before the change.</description></item>
/// <item><description><c>NewValue</c>: The value of the field after the change.</description></item>
/// <item><description><c>ValueType</c>: The data type of the field (e.g., "string", "int", "bool").</description></item>
/// </list>
/// </description>
/// </item>
/// <item>
/// <description><c>UserIpAddress</c>: (Optional) The IP address of the user performing the action. Useful for tracking location-based information.</description>
/// </item>
/// </list>
/// Once the <see cref="AuditRecord"/> is constructed, it is asynchronously saved using <c>RecordAuditAsync</c>.
/// </remarks>
/// <returns>
/// A task representing the asynchronous operation of recording the audit log. The task completes when the audit record is successfully stored.
/// </returns>
/// <example>
/// Here is an example of how to use this method:
/// <code>
/// await CreateAuditLogAsync(
/// user: HttpContext.User,
/// dataOriginal: oldData,
/// dataModified: newData,
/// reason: "Updated user details"
/// );
/// </code>
/// </example>
#endregion
public async Task CreateAuditLogAsync(ClaimsPrincipal? user, Object? dataOriginal, Object? dataModified,
string? reason)
{
if (user == null)
{
Log.Error("An error occurred: User is null");
return;
}
AuditRecord auditLogData = new AuditRecord();
JsonComparer jsonComparer = new();
if (dataOriginal != null)
{
auditLogData.EntityType =
IsAnonymousType(dataOriginal.GetType()) ? "AnonymousType" : dataOriginal.GetType().Name;
var idProperty = dataOriginal.GetType().GetProperty("Id");
if (idProperty != null && idProperty.PropertyType == typeof(ObjectId))
{
ObjectId id = (ObjectId)idProperty.GetValue(dataOriginal);
auditLogData.RecordId = id.ToString();
}else if (idProperty != null && idProperty.PropertyType == typeof(string))
{
auditLogData.RecordId = idProperty.GetValue(dataOriginal).ToString();
}
}
else if (dataModified != null)
{
auditLogData.EntityType =
IsAnonymousType(dataModified.GetType()) ? "AnonymousType" : dataModified.GetType().Name;
var idProperty = dataModified.GetType().GetProperty("Id");
if (idProperty != null && idProperty.PropertyType == typeof(ObjectId))
{
ObjectId id = (ObjectId)idProperty.GetValue(dataModified);
//TODO verificar si esta bien obtener el valor asi:
auditLogData.RecordId = id.ToString();
}else if (idProperty != null && idProperty.PropertyType == typeof(string))
{
auditLogData.RecordId = idProperty.GetValue(dataModified).ToString();
}
}
auditLogData.ActionTime = DateTime.Now;
auditLogData.Reason = string.IsNullOrEmpty(reason) ? "Not specified" : reason;
auditLogData.UserIpAddress = string.IsNullOrEmpty(user.FindFirst("IpAddress")?.Value)
? "Not specified"
: user.FindFirst("IpAddress")?.Value ?? "Not specified";
auditLogData.UserId = user.FindFirst(ClaimTypes.Name)?.Value ?? "Not specified";
// Asignar ActionType segun las condiciones
if (dataOriginal == null || string.IsNullOrEmpty(dataOriginal.ToString()))
{
auditLogData.ActionType = "create";
}
else if (dataModified == null || string.IsNullOrEmpty(dataModified.ToString()))
{
auditLogData.ActionType = "delete";
}
else
{
auditLogData.ActionType = "update";
}
// dataOriginal="{\n \"Id\": \"PV8\",\n \"Items\": [\n {\n \"Code\": \"Temperature\",\n \"CodingSystem\": \"SENSOR\",\n \"OriginalName\": \"Room_Temperature\",\n \"ParentCode\": \"T\",\n \"ParentCodingSystem\": \"Room_Temperature\",\n \"ParentName\": null,\n \"Name\": \"Room_Temperature\",\n \"Units\": \"T\",\n \"ArrowType\": 1,\n \"ShowArrow\": true,\n \"ForceUnits\": false,\n \"MinAlert\": null,\n \"MaxAlert\": null,\n \"MaxWarn\": null,\n \"MinWarn\": null,\n \"WarnColor\": null,\n \"AlertColor\": null,\n \"AlertValues\": null,\n \"WarningValues\": null,\n \"ForceWarn\": false,\n \"ForceAlert\": false,\n \"Alert\": null,\n \"Expires\": null,\n \"ShowOnExpired\": false,\n \"Persist\": null,\n \"ColorOnExpired\": null,\n \"RetentionPolicy\": null,\n \"RetentionPolicyValue\": null,\n \"LevelCondition\": null,\n \"UiConfiguration\": {\n \"screenLabel\": \"screenLabel\"\n },\n \"Alarm\": null,\n \"Description\": null,\n \"RequiredValue\": null,\n \"Preconditions\": null,\n \"CheckObservations\": false,\n \"CreateObservation\": null,\n \"TimeFromMessageTime\": false\n }\n ]\n}"
//dataModified = "{\n \"Id\": \"PV8\",\n \"Items\": [\n {\n \"Code\": \"Temperature\",\n \"CodingSystem\": \"SENSOR\",\n \"OriginalName\": \"Room_Temperature\",\n \"ParentCode\": \"T\",\n \"ParentCodingSystem\": \"Room_Temperature\",\n \"ParentName\": null,\n \"Name\": \"Room_Temperature\",\n \"Units\": \"T\",\n \"ArrowType\": 1,\n \"ShowArrow\": true,\n \"ForceUnits\": false,\n \"MinAlert\": null,\n \"MaxAlert\": null,\n \"MaxWarn\": null,\n \"MinWarn\": null,\n \"WarnColor\": null,\n \"AlertColor\": null,\n \"AlertValues\": null,\n \"WarningValues\": null,\n \"ForceWarn\": false,\n \"ForceAlert\": false,\n \"Alert\": null,\n \"Expires\": null,\n \"ShowOnExpired\": false,\n \"Persist\": null,\n \"ColorOnExpired\": null,\n \"RetentionPolicy\": null,\n \"RetentionPolicyValue\": null,\n \"LevelCondition\": null,\n \"UiConfiguration\": {\n \"screenLabel\": \"screenLabel\"\n },\n \"Alarm\": null,\n \"Description\": null,\n \"RequiredValue\": null,\n \"Preconditions\": null,\n \"CheckObservations\": false,\n \"CreateObservation\": null,\n \"TimeFromMessageTime\": false\n },\n {\n \"Code\": \"88\",\n \"CodingSystem\": \"88\",\n \"OriginalName\": \"88\",\n \"ParentCode\": \"88\",\n \"ParentCodingSystem\": \"88\",\n \"ParentName\": null,\n \"Name\": \"88\",\n \"Units\": \"88\",\n \"ArrowType\": 1,\n \"ShowArrow\": true,\n \"ForceUnits\": false,\n \"MinAlert\": null,\n \"MaxAlert\": null,\n \"MaxWarn\": null,\n \"MinWarn\": null,\n \"WarnColor\": null,\n \"AlertColor\": null,\n \"AlertValues\": null,\n \"WarningValues\": null,\n \"ForceWarn\": false,\n \"ForceAlert\": false,\n \"Alert\": null,\n \"Expires\": null,\n \"ShowOnExpired\": false,\n \"Persist\": null,\n \"ColorOnExpired\": null,\n \"RetentionPolicy\": null,\n \"RetentionPolicyValue\": null,\n \"LevelCondition\": null,\n \"UiConfiguration\": {\n \"screenLabel\": \"88\"\n },\n \"Alarm\": null,\n \"Description\": null,\n \"RequiredValue\": null,\n \"Preconditions\": null,\n \"CheckObservations\": false,\n \"CreateObservation\": null,\n \"TimeFromMessageTime\": false\n }\n ]\n}";
auditLogData.Changes = jsonComparer.GetDifferences(dataOriginal, dataModified);
await RecordAuditAsync(auditLogData);
}
// Método auxiliar para verificar si un tipo es anonimo, solo es necesario para pruebas
private bool IsAnonymousType(Type type)
{
return type.IsGenericType && type.Name.StartsWith("<>f__AnonymousType");
}
/// <summary>
/// Creates and records an audit log entry based on the differences between the original and modified data provided in an <see cref="AuditLogData"/> object.
/// </summary>
/// <param name="auditLogData">An object containing all necessary details for the audit log entry, including:</param>
/// <param name="auditLogData.EntityType">The type of entity being modified.</param>
/// <param name="auditLogData.RecordId">The unique ID of the record being changed.</param>
/// <param name="auditLogData.UserId">The ID of the user performing the action.</param>
/// <param name="auditLogData.ActionType">The type of action performed.</param>
/// <param name="auditLogData.Reason">The reason for the action.</param>
/// <param name="auditLogData.ActionTime">The timestamp of the action.</param>
/// <param name="auditLogData.OriginalJson">The original JSON data.</param>
/// <param name="auditLogData.ModifiedJson">The modified JSON data.</param>
/// <param name="auditLogData.UserIpAddress">The IP address of the user.</param>
public async Task CreateAuditLogAsync(AuditLogData auditLogData)
{
if (auditLogData == null) throw new ArgumentNullException(nameof(auditLogData));
AuditRecord auditRecord = new AuditRecord();
auditRecord.EntityType = auditLogData.EntityType;
auditRecord.RecordId = auditLogData.RecordId;
auditRecord.UserId = auditLogData.UserId;
auditRecord.ActionTime = auditLogData.ActionTime;
auditRecord.ActionType = auditLogData.ActionType;
auditRecord.Reason = auditLogData.Reason;
auditRecord.UserIpAddress = auditLogData.UserIpAddress;
JsonComparer jsonComparer = new();
auditRecord.Changes = jsonComparer.GetDifferences(auditLogData.OriginalJson, auditLogData.ModifiedJson);
await RecordAuditAsync(auditRecord);
}
/// <summary>
/// Retrieves paginated audit logs.
/// </summary>
/// <param name="filterDto.pageNumber">The page number to retrieve (1-based).</param>
/// <param name="filterDto.pageSize">The number of records per page.</param>
/// <param name="filterDto.userId">filter taking into account the user who made the modification.</param>
/// <param name="filterDto.startDate">filter by the date the modification was made.</param>
/// <param name="filterDto.endDate">filter by the date the modification was made.</param>
/// <param name="filterDto.recordId">filter by modified record id in the collection.</param>
/// <returns>A paginated list of audit records.</returns>
public async Task<PaginatedLogsResultDto<AuditRecordDto>> GetAuditLogsBySpecificFilterAsync(
SpecificAuditLogFilterDto filterDto)
{
if (filterDto.pageNumber <= 0)
throw new ArgumentException("Page number must be greater than 0.", nameof(filterDto.pageNumber));
if (filterDto.pageSize <= 0)
throw new ArgumentException("Page size must be greater than 0.", nameof(filterDto.pageSize));
// Construir filtro dinámico
var filter = BuildAuditLogFilter(filterDto.userId, filterDto.recordId, filterDto.entityType,
filterDto.startDate, filterDto.endDate, filterDto.actionTime);
var totalRecords = await _auditLogRepository.CountDocumentsAsync(filter);
var totalPages = (int)Math.Ceiling((double)totalRecords / filterDto.pageSize);
var sort = Builders<AuditRecord>.Sort.Descending(x => x.ActionTime); // Ordenar por fecha más reciente
var records = await _auditLogRepository.GetPaginatedAsync(filterDto.pageNumber, filterDto.pageSize, filter, sort);
// var records = await _auditLogRepository.GetPaginatedAsync(filterDto.pageNumber, filterDto.pageSize, filter);
var recordsDto = records.Select(record => AuditRecordMapper.ToDto(record)).ToList();
return new PaginatedLogsResultDto<AuditRecordDto>
{
CurrentPage = filterDto.pageNumber,
PageSize = filterDto.pageSize,
TotalRecords = totalRecords,
TotalPages = totalPages,
Records = recordsDto
};
}
/// <summary>
/// Retrieves paginated audit logs with optional full-text search across multiple fields.
/// </summary>
/// <param name="filtertTextOrDateDto.pageNumber">The page number to retrieve (1-based).</param>
/// <param name="filtertTextOrDateDto.pageSize">The number of records per page.</param>
/// <param name="filtertTextOrDateDto.text_to_search">Text to search in any field.</param>
/// /// <param name="filtertTextOrDateDto.startDate">filter by the date the modification was made.</param>
/// <param name="filtertTextOrDateDto.endDate">filter by the date the modification was made.</param>
/// <returns>A paginated list of audit records.</returns>
public async Task<PaginatedLogsResultDto<AuditRecordDto>> GetAuditLogsMatchingTextOrDateAsync(
AuditLogTextOrDateSearchDTO filtertTextOrDateDto)
{
if (filtertTextOrDateDto.pageNumber <= 0)
throw new ArgumentException("Page number must be greater than 0.", nameof(filtertTextOrDateDto.pageNumber));
if (filtertTextOrDateDto.pageSize <= 0)
throw new ArgumentException("Page size must be greater than 0.", nameof(filtertTextOrDateDto.pageSize));
// Construct the text search filter if text_to_search is provided
FilterDefinition<AuditRecord> filter;
if (!string.IsNullOrWhiteSpace(filtertTextOrDateDto.searchText) || filtertTextOrDateDto.startDate.HasValue ||
filtertTextOrDateDto.endDate.HasValue || filtertTextOrDateDto.actionTime.HasValue)
{
filter = BuildAuditLogFilterByText(filtertTextOrDateDto.searchText, filtertTextOrDateDto.startDate,
filtertTextOrDateDto.endDate, filtertTextOrDateDto.actionTime);
}
else
{
filter = Builders<AuditRecord>.Filter.Empty;
}
var totalRecords = await _auditLogRepository.CountDocumentsAsync(filter);
var totalPages = (int)Math.Ceiling((double)totalRecords / filtertTextOrDateDto.pageSize);
var sort = Builders<AuditRecord>.Sort.Descending(x => x.ActionTime); // Ordenar por fecha más reciente
var records = await _auditLogRepository.GetPaginatedAsync(filtertTextOrDateDto.pageNumber,
filtertTextOrDateDto.pageSize, filter,sort);
var recordsDto = records.Select(record => AuditRecordMapper.ToDto(record)).ToList();
return new PaginatedLogsResultDto<AuditRecordDto>
{
CurrentPage = filtertTextOrDateDto.pageNumber,
PageSize = filtertTextOrDateDto.pageSize,
TotalRecords = totalRecords,
TotalPages = totalPages,
Records = recordsDto
};
}
private static FilterDefinition<AuditRecord> BuildAuditLogFilter(
string? userId,
string? recordId,
string? entityType,
DateTime? startDate,
DateTime? endDate,
DateTime? actionTime)
{
var builder = Builders<AuditRecord>.Filter;
var filters = new List<FilterDefinition<AuditRecord>>();
if (!string.IsNullOrEmpty(userId))
{
filters.Add(builder.Eq(x => x.UserId, userId));
}
if (!string.IsNullOrEmpty(recordId))
{
filters.Add(builder.Eq(x => x.RecordId, recordId));
}
if (startDate.HasValue)
{
filters.Add(builder.Gte(x => x.ActionTime, startDate.Value));
}
if (endDate.HasValue)
{
filters.Add(builder.Lte(x => x.ActionTime, endDate.Value));
}
if (actionTime.HasValue)
{
filters.Add(builder.Eq(x => x.ActionTime, actionTime));
}
if (!string.IsNullOrEmpty(entityType))
{
filters.Add(builder.Eq(x => x.EntityType, entityType));
}
return filters.Count > 0 ? builder.And(filters) : builder.Empty;
}
private static FilterDefinition<AuditRecord> BuildAuditLogFilterByText(
string? searchText,
DateTime? startDate = null,
DateTime? endDate = null,
DateTime? actionTime = null)
{
var builder = Builders<AuditRecord>.Filter;
var filters = new List<FilterDefinition<AuditRecord>>();
// Filtro por rango de fechas
if (startDate.HasValue && endDate.HasValue)
{
filters.Add(builder.And(
builder.Gte(x => x.ActionTime, startDate.Value),
builder.Lte(x => x.ActionTime, endDate.Value)
));
}
else if (startDate.HasValue)
{
filters.Add(builder.Gte(x => x.ActionTime, startDate.Value));
}
else if (endDate.HasValue)
{
filters.Add(builder.Lte(x => x.ActionTime, endDate.Value));
}
else if (actionTime.HasValue)
{
filters.Add(builder.Eq(x => x.ActionTime, actionTime));
}
// Filtro por texto si se proporciona
if (!string.IsNullOrEmpty(searchText))
{
var regexFilter = new BsonRegularExpression(new Regex(searchText, RegexOptions.IgnoreCase));
filters.Add(builder.Or(
builder.Regex(x => x.UserId, regexFilter),
builder.Regex(x => x.RecordId, regexFilter),
builder.Regex(x => x.EntityType, regexFilter),
builder.Regex(x => x.ActionType, regexFilter)
));
}
// Combina todos los filtros con AND
if (filters.Any())
{
return builder.And(filters);
}
return builder.Empty; // Sin filtros, devuelve todos los documentos
}
}