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);
}
}