123 lines
2.9 KiB
Go
123 lines
2.9 KiB
Go
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
|
|
}
|