Files
ForgeBucket/internal/api/middleware/audit.go
T

68 lines
1.8 KiB
Go

package middleware
import (
"net/http"
"time"
"xorm.io/xorm"
"github.com/forgeo/forgebucket/internal/events"
"github.com/forgeo/forgebucket/internal/models"
)
// statusRecorder wraps ResponseWriter to capture the written status code.
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
// AuditLog middleware records every state-mutating request (POST/PUT/PATCH/DELETE)
// to the audit_log table and publishes an audit.event to the event bus.
// It runs after auth middleware so the actor is available in context.
func AuditLog(db *xorm.Engine, bus events.EventBus) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodTrace:
next.ServeHTTP(w, r)
return
}
rec := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rec, r)
actorID, _ := UserIDFromContext(r.Context())
actorName, _ := r.Context().Value(ContextKeyUsername).(string)
entry := &models.AuditLog{
ActorID: actorID,
ActorName: actorName,
Method: r.Method,
Path: r.URL.Path,
StatusCode: rec.status,
IPAddress: r.RemoteAddr,
UserAgent: r.UserAgent(),
OccurredAt: time.Now().UTC(),
}
go func() {
db.Insert(entry) //nolint:errcheck
bus.Publish(events.SubjectAuditEvent, events.AuditEvent{ //nolint:errcheck
ActorID: entry.ActorID,
ActorName: entry.ActorName,
Action: entry.Method + " " + entry.Path,
ResourcePath: entry.Path,
IPAddress: entry.IPAddress,
StatusCode: entry.StatusCode,
At: entry.OccurredAt,
})
}()
})
}
}