using Microsoft.AspNetCore.Mvc.Filters; using Nuuru.Server.Data; using Nuuru.Server.Extensions; using Nuuru.Server.Models; namespace Nuuru.Server.Filters { public class AuditLogActionFilter : IAsyncActionFilter { private static readonly HashSet MutatingMethods = new(StringComparer.OrdinalIgnoreCase) { "POST", "PUT", "DELETE", "PATCH" }; private static readonly Dictionary ControllerCategoryMap = new(StringComparer.OrdinalIgnoreCase) { { "Auth", "Auth" }, { "Post", "Booru" }, { "Comment", "Booru" }, { "Tag", "Booru" }, { "TagCategory", "Booru" }, { "TagRelation", "Booru" }, { "Vote", "Booru" }, { "Favorite", "Booru" }, { "PostHistory", "Booru" }, { "Moderation", "Moderation" }, { "Admin", "Admin" }, { "Role", "Admin" }, { "Permission", "Admin" }, { "ForumCategory", "Forum" }, { "ForumThread", "Forum" }, { "ForumPost", "Forum" }, { "ForumAttachment", "Forum" }, { "Notification", "Admin" }, { "Conversation", "Messaging" }, { "Message", "Messaging" }, { "Reaction", "Booru" }, { "Members", "Admin" }, { "BBCode", "Booru" }, }; private static readonly HashSet TargetRouteKeys = new(StringComparer.OrdinalIgnoreCase) { "postId", "commentId", "userId", "roleId", "threadId", "categoryId", "id" }; public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { var executedContext = await next(); var httpMethod = context.HttpContext.Request.Method; if (!MutatingMethods.Contains(httpMethod)) return; try { var controllerName = context.RouteData.Values["controller"]?.ToString() ?? "Unknown"; var actionName = context.RouteData.Values["action"]?.ToString() ?? "Unknown"; var category = ControllerCategoryMap.GetValueOrDefault(controllerName, "Other"); // Check HttpContext.Items for explicitly set category if (context.HttpContext.Items.TryGetValue(AuditLog.TargetCategoryKey, out var explicitCategory) && explicitCategory != null) { category = explicitCategory.ToString() ?? category; } var action = $"{controllerName}.{actionName}"; // Check HttpContext.Items for explicitly set action if (context.HttpContext.Items.TryGetValue(AuditLog.ActionKey, out var explicitAction) && explicitAction != null) { action = explicitAction.ToString() ?? action; } var userId = context.HttpContext.User?.GetUserId(); // Check HttpContext.Items for explicitly set userId (e.g. from registration) if (context.HttpContext.Items.TryGetValue(AuditLog.UserIdKey, out var explicitUserId) && explicitUserId != null) { if (Guid.TryParse(explicitUserId.ToString(), out var parsedUserId)) { userId = parsedUserId; } } var ipAddress = context.HttpContext.Connection.RemoteIpAddress?.ToString(); Guid? assessmentId = null; if (context.HttpContext.Items.TryGetValue(AuditLog.AssessmentIdKey, out var explicitAssessmentId) && explicitAssessmentId is Guid parsedAssessmentId) { assessmentId = parsedAssessmentId; } string? browserHash = null; if (context.HttpContext.Items.TryGetValue(AuditLog.BrowserHashKey, out var explicitBrowserHash) && explicitBrowserHash != null) { browserHash = explicitBrowserHash.ToString(); } var requestPath = context.HttpContext.Request.Path.ToString(); var statusCode = executedContext.HttpContext.Response.StatusCode; string? targetType = null; string? targetId = null; // Check HttpContext.Items for explicitly set targets first if (context.HttpContext.Items.TryGetValue(AuditLog.TargetIdKey, out var explicitId) && explicitId != null) { targetId = explicitId.ToString(); if (context.HttpContext.Items.TryGetValue(AuditLog.TargetTypeKey, out var explicitType) && explicitType != null) { targetType = explicitType.ToString(); } } if (string.IsNullOrEmpty(targetId)) { // Extract target IDs from route values foreach (var key in TargetRouteKeys) { if (context.RouteData.Values.TryGetValue(key, out var value) && value != null) { targetId = value.ToString(); targetType = key.Replace("Id", "", StringComparison.OrdinalIgnoreCase); if (string.IsNullOrEmpty(targetType)) targetType = controllerName; break; } } } // Also check action arguments for common ID patterns if (string.IsNullOrEmpty(targetId)) { foreach (var arg in context.ActionArguments) { if (TargetRouteKeys.Contains(arg.Key) && arg.Value != null) { targetId = arg.Value.ToString(); if (string.IsNullOrEmpty(targetType)) { targetType = arg.Key.Replace("Id", "", StringComparison.OrdinalIgnoreCase); if (string.IsNullOrEmpty(targetType)) targetType = controllerName; } break; } } } var auditLog = new AuditLog { Action = action, Category = category, TargetType = targetType, TargetId = targetId, IpAddress = ipAddress, AssessmentId = assessmentId, BrowserHash = browserHash, HttpMethod = httpMethod, RequestPath = requestPath, ResponseStatusCode = statusCode, UserId = userId, }; var dbContext = context.HttpContext.RequestServices.GetRequiredService(); dbContext.AuditLogs.Add(auditLog); await dbContext.SaveChangesAsync(); } catch (Exception ex) { // Never let audit logging break the response var logger = context.HttpContext.RequestServices.GetService>(); logger?.LogError(ex, "Failed to save audit log entry"); } } } }