120 lines
4.1 KiB
C#
120 lines
4.1 KiB
C#
using Management.Data;
|
|
using Management.Security;
|
|
using System.Text.Json;
|
|
|
|
namespace Management.Security;
|
|
|
|
/// <summary>
|
|
/// Logs all mutating (non-GET) requests to tbAdminActivity when a staff JWT is present.
|
|
///
|
|
/// Enables request body buffering so a compact filter summary can be extracted
|
|
/// after the controller has run. Fire-and-forget — never delays the response.
|
|
/// Registered after ClientAuthMiddleware so ClientContext is populated.
|
|
/// </summary>
|
|
public sealed class ActivityLoggingMiddleware
|
|
{
|
|
private readonly RequestDelegate _next;
|
|
private readonly ILogger<ActivityLoggingMiddleware> _logger;
|
|
|
|
private static readonly string[] _skipPrefixes =
|
|
{
|
|
"/api/help", "/api/test", "/swagger", "/health"
|
|
};
|
|
|
|
// Body keys treated as filter context worth surfacing in the activity log.
|
|
// Pagination keys (page, pageSize) are intentionally excluded.
|
|
private static readonly string[] _filterKeys =
|
|
[
|
|
"search", "status", "clientId", "category", "network",
|
|
"dateFrom", "dateTo", "action", "ruleId", "initiativeId",
|
|
];
|
|
|
|
public ActivityLoggingMiddleware(RequestDelegate next, ILogger<ActivityLoggingMiddleware> logger)
|
|
{
|
|
_next = next;
|
|
_logger = logger;
|
|
}
|
|
|
|
public async Task InvokeAsync(HttpContext context, ClientContext clientContext, SqlService sql)
|
|
{
|
|
var method = context.Request.Method;
|
|
var isRead = HttpMethods.IsGet(method) || HttpMethods.IsHead(method) || HttpMethods.IsOptions(method);
|
|
|
|
// Buffer request body before calling next so we can re-read it after
|
|
if (!isRead)
|
|
context.Request.EnableBuffering();
|
|
|
|
await _next(context);
|
|
|
|
if (isRead) return;
|
|
|
|
var oid = clientContext.ClientId;
|
|
if (string.IsNullOrWhiteSpace(oid) || clientContext.IsDevBypass) return;
|
|
|
|
var path = context.Request.Path.Value ?? "";
|
|
if (_skipPrefixes.Any(p => path.StartsWith(p, StringComparison.OrdinalIgnoreCase))) return;
|
|
|
|
var statusCode = context.Response.StatusCode;
|
|
var filterSummary = await ReadFilterSummaryAsync(context.Request);
|
|
|
|
_ = Task.Run(async () =>
|
|
{
|
|
try
|
|
{
|
|
var rqst = JsonSerializer.Serialize(new
|
|
{
|
|
oid,
|
|
email = clientContext.Email,
|
|
displayName = clientContext.ClientName,
|
|
method,
|
|
path,
|
|
statusCode,
|
|
filter = filterSummary, // null for bodies with no recognised filter keys
|
|
});
|
|
|
|
await sql.ExecProcAsync("dbo.spAdminActivity", "log", rqst);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning("[Activity] Log failed: {Message}", ex.Message);
|
|
}
|
|
});
|
|
}
|
|
|
|
private async Task<string?> ReadFilterSummaryAsync(HttpRequest request)
|
|
{
|
|
if (request.ContentLength is null or 0) return null;
|
|
if (request.ContentType?.Contains("application/json", StringComparison.OrdinalIgnoreCase) != true) return null;
|
|
|
|
try
|
|
{
|
|
request.Body.Seek(0, SeekOrigin.Begin);
|
|
using var reader = new StreamReader(request.Body, leaveOpen: true);
|
|
var body = await reader.ReadToEndAsync();
|
|
if (string.IsNullOrWhiteSpace(body)) return null;
|
|
|
|
using var doc = JsonDocument.Parse(body);
|
|
var parts = new List<string>();
|
|
|
|
foreach (var key in _filterKeys)
|
|
{
|
|
if (!doc.RootElement.TryGetProperty(key, out var val)) continue;
|
|
if (val.ValueKind is JsonValueKind.Null or JsonValueKind.Undefined) continue;
|
|
|
|
var str = val.ValueKind == JsonValueKind.String
|
|
? val.GetString()
|
|
: val.ToString();
|
|
|
|
if (!string.IsNullOrWhiteSpace(str))
|
|
parts.Add($"{key}={str}");
|
|
}
|
|
|
|
return parts.Count > 0 ? string.Join(" ", parts) : null;
|
|
}
|
|
catch
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
}
|