using System.Data; using System.Text.Json; using Microsoft.AspNetCore.Http; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using PowderCoating.Application.Interfaces; namespace PowderCoating.Infrastructure.Data; /// /// Writes manual audit log entries via raw SQL, matching the approach used by /// so neither path can trigger an infinite loop /// through the EF change tracker. /// public class AuditService : IAuditService { private readonly ApplicationDbContext _db; private readonly IHttpContextAccessor _http; public AuditService(ApplicationDbContext db, IHttpContextAccessor http) { _db = db; _http = http; } /// public async Task LogAsync(string action, string entityType, string? description = null, object? details = null, string? entityId = null) { var (userId, userName, companyId, ip) = GetRequestContext(); var newValues = details != null ? JsonSerializer.Serialize(details) : null; const string sql = """ INSERT INTO AuditLogs (UserId, UserName, CompanyId, CompanyName, Action, EntityType, EntityId, EntityDescription, OldValues, NewValues, IpAddress, Timestamp) VALUES (@userId, @userName, @companyId, @companyName, @action, @entityType, @entityId, @entityDescription, NULL, @newValues, @ipAddress, @timestamp) """; SqlParameter[] p = [ new("@userId", SqlDbType.NVarChar, -1) { Value = (object?)userId ?? DBNull.Value }, new("@userName", SqlDbType.NVarChar, -1) { Value = (object)userName }, new("@companyId", SqlDbType.Int) { Value = (object?)companyId ?? DBNull.Value }, new("@companyName", SqlDbType.NVarChar, -1) { Value = DBNull.Value }, new("@action", SqlDbType.NVarChar, -1) { Value = (object)action }, new("@entityType", SqlDbType.NVarChar, -1) { Value = (object)entityType }, new("@entityId", SqlDbType.NVarChar, -1) { Value = (object?)entityId ?? DBNull.Value }, new("@entityDescription", SqlDbType.NVarChar, -1) { Value = (object?)description ?? DBNull.Value }, new("@newValues", SqlDbType.NVarChar, -1) { Value = (object?)newValues ?? DBNull.Value }, new("@ipAddress", SqlDbType.NVarChar, -1) { Value = (object?)ip ?? DBNull.Value }, new("@timestamp", SqlDbType.DateTime2) { Value = DateTime.UtcNow }, ]; await _db.Database.ExecuteSqlRawAsync(sql, p); } private (string? userId, string userName, int? companyId, string? ip) GetRequestContext() { var ctx = _http.HttpContext; if (ctx == null) return (null, "System", null, null); var user = ctx.User; var userId = user.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value; var userName = user.Identity?.Name ?? user.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value ?? "Unknown"; var cidStr = user.FindFirst("CompanyId")?.Value; int? companyId = cidStr != null && int.TryParse(cidStr, out var c) ? c : null; var ip = ctx.Connection.RemoteIpAddress?.ToString(); return (userId, userName, companyId, ip); } }