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