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, }) }() }) } }