package middleware import ( "context" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" "go.uber.org/zap" "veza-backend-api/internal/services" ) // skipAuditPaths are paths that should not be audited (health, metrics, swagger, etc.) var skipAuditPaths = []string{ "/health", "/healthz", "/readyz", "/health/deep", "/metrics", "/metrics/aggregated", "/system/metrics", "/swagger", "/docs", "/api/versions", } // AuditMiddleware returns a Gin middleware that logs POST/PUT/DELETE requests to the audit service. // Logging happens asynchronously after the request completes. func AuditMiddleware(auditService *services.AuditService, logger *zap.Logger) gin.HandlerFunc { if auditService == nil || logger == nil { return func(c *gin.Context) { c.Next() } } return func(c *gin.Context) { c.Next() method := c.Request.Method if method != "POST" && method != "PUT" && method != "DELETE" && method != "PATCH" { return } reqPath := c.Request.URL.Path if shouldSkipAudit(reqPath) { return } var userID *uuid.UUID if uid, exists := c.Get("user_id"); exists { if parsed, ok := uid.(uuid.UUID); ok && parsed != uuid.Nil { userID = &parsed } } action := mapMethodToAction(method) resource := extractResourceFromPath(reqPath) req := &services.AuditLogCreateRequest{ UserID: userID, Action: action, Resource: resource, IPAddress: c.ClientIP(), UserAgent: c.Request.UserAgent(), Metadata: map[string]interface{}{ "method": method, "path": reqPath, }, } go func() { ctx := context.Background() if err := auditService.LogAction(ctx, req); err != nil { logger.Warn("Audit middleware failed to log action", zap.Error(err), zap.String("action", action), zap.String("resource", resource), ) } }() } } func shouldSkipAudit(reqPath string) bool { reqPath = strings.TrimSuffix(reqPath, "/") for _, skip := range skipAuditPaths { if reqPath == skip || strings.HasPrefix(reqPath, skip+"/") { return true } } // Also skip /api/v1/health if strings.Contains(reqPath, "/health") { return true } return false } func mapMethodToAction(method string) string { switch method { case "POST": return "create" case "PUT", "PATCH": return "update" case "DELETE": return "delete" default: return strings.ToLower(method) } } // extractResourceFromPath derives a resource type from the API path. // e.g. /api/v1/tracks -> track, /api/v1/users/me -> user func extractResourceFromPath(reqPath string) string { // Remove /api/v1 prefix p := strings.TrimPrefix(reqPath, "/api/v1") p = strings.TrimPrefix(p, "/api") p = strings.Trim(p, "/") parts := strings.Split(p, "/") if len(parts) == 0 { return "unknown" } resource := parts[0] // Singularize common plurals if strings.HasSuffix(resource, "s") && len(resource) > 1 { resource = strings.TrimSuffix(resource, "s") } return resource }