Files
jizhi/backend/MiaoJiZhang.Api/Services/AdminAuditMiddleware.cs
T

72 lines
2.6 KiB
C#

using MiaoJiZhang.Domain.Entities;
using MiaoJiZhang.Infrastructure.Persistence;
namespace MiaoJiZhang.Api.Services;
public sealed class AdminAuditMiddleware(RequestDelegate next)
{
public async Task InvokeAsync(HttpContext context, AppDbContext db)
{
var isAdmin = context.Request.Path.StartsWithSegments("/api/admin");
var isAuth = context.Request.Path.StartsWithSegments("/api/admin/auth");
var shouldAudit = isAdmin && (isAuth ||
!AdminSessionService.IsSafeMethod(context.Request.Method));
if (!shouldAudit)
{
await next(context);
return;
}
Exception? failure = null;
try
{
await next(context);
}
catch (Exception exception)
{
failure = exception;
throw;
}
finally
{
try
{
var principal = AdminRequestContext.Principal(context);
var attemptedUsername = context.Items.TryGetValue(
AdminRequestContext.AuditUsernameKey,
out var attemptedValue)
? attemptedValue?.ToString()
: null;
var status = failure is null
? context.Response.StatusCode
: StatusCodes.Status500InternalServerError;
db.AdminAuditLogs.Add(new AdminAuditLog
{
AdminUserId = principal?.UserId,
Username = principal?.Username ?? attemptedUsername,
Action = ActionName(context),
Resource = context.Request.Path.Value ?? "/api/admin",
HttpMethod = context.Request.Method,
Path = (context.Request.Path + context.Request.QueryString).ToString(),
StatusCode = status,
Success = failure is null && status < 400,
Detail = failure?.GetType().Name,
IpAddress = context.Connection.RemoteIpAddress?.ToString(),
CreatedAt = DateTime.UtcNow,
});
await db.SaveChangesAsync(CancellationToken.None);
}
catch
{
// Audit persistence must not replace the original API result.
}
}
}
private static string ActionName(HttpContext context)
{
var path = context.Request.Path.Value?.Trim('/').Replace('/', '.') ?? "api.admin";
return $"{context.Request.Method.ToLowerInvariant()}.{path}";
}
}